diff --git a/e2e/schematics/lint.test.ts b/e2e/schematics/lint.test.ts new file mode 100644 index 0000000000000..6851c0ed8b9e2 --- /dev/null +++ b/e2e/schematics/lint.test.ts @@ -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'); + } + }); +}); diff --git a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts new file mode 100644 index 0000000000000..506a5e5235a28 --- /dev/null +++ b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts @@ -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); +} diff --git a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts new file mode 100644 index 0000000000000..d061fcf28361e --- /dev/null +++ b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts @@ -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('.'); + } +} diff --git a/packages/schematics/src/workspace/files/__directory__/tslint.json b/packages/schematics/src/workspace/files/__directory__/tslint.json index 34f59facc5e5f..89228ccd1fbe4 100644 --- a/packages/schematics/src/workspace/files/__directory__/tslint.json +++ b/packages/schematics/src/workspace/files/__directory__/tslint.json @@ -1,6 +1,7 @@ { "rulesDirectory": [ - "node_modules/codelyzer" + "node_modules/codelyzer", + "node_modules/@nrwl/schematics/src/tslint" ], "rules": { "arrow-return-shorthand": true, @@ -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": [] + } + ] } }