-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(schematics): add a lint check to enforce module boundaries
- Loading branch information
Showing
4 changed files
with
153 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
}); | ||
}); |
44 changes: 44 additions & 0 deletions
44
packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
67
packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters