nx/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts

129 lines
4.3 KiB
TypeScript

import * as path from 'path';
import * as Lint from 'tslint';
import { IOptions } from 'tslint';
import * as ts from 'typescript';
import { readFileSync } from 'fs';
export class Rule extends Lint.Rules.AbstractRule {
constructor(
options: IOptions,
private readonly projectPath?: string,
private readonly npmScope?: string,
private readonly libNames?: string[],
private readonly appNames?: string[],
private readonly roots?: string[]
) {
super(options);
if (!projectPath) {
const cliConfig = this.readCliConfig();
this.projectPath = process.cwd();
this.npmScope = cliConfig.project.npmScope;
this.libNames = cliConfig.apps.filter(p => p.root.startsWith('libs/')).map(a => a.name);
this.appNames = cliConfig.apps.filter(p => p.root.startsWith('apps/')).map(a => a.name);
this.roots = cliConfig.apps.map(a => path.dirname(a.root));
}
}
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new EnforceModuleBoundariesWalker(
sourceFile,
this.getOptions(),
this.projectPath,
this.npmScope,
this.libNames,
this.appNames,
this.roots
)
);
}
private readCliConfig(): any {
return JSON.parse(readFileSync(`.angular-cli.json`, 'UTF-8'));
}
}
class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
constructor(
sourceFile: ts.SourceFile,
options: IOptions,
private projectPath: string,
private npmScope: string,
private libNames: string[],
private appNames: string[],
private roots: string[]
) {
super(sourceFile, options);
this.roots = [...roots].sort((a, b) => a.length - b.length);
}
public visitImportDeclaration(node: ts.ImportDeclaration) {
const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1);
const allow: string[] = Array.isArray(this.getOptions()[0].allow)
? this.getOptions()[0].allow.map(a => `${a}`)
: [];
const lazyLoad: string[] = Array.isArray(this.getOptions()[0].lazyLoad)
? this.getOptions()[0].lazyLoad.map(a => `${a}`)
: [];
// whitelisted import => return
if (allow.indexOf(imp) > -1) {
super.visitImportDeclaration(node);
return;
}
const lazyLoaded = lazyLoad.filter(l => imp.startsWith(`@${this.npmScope}/${l}`))[0];
if (lazyLoaded) {
this.addFailureAt(node.getStart(), node.getWidth(), 'imports of lazy-loaded libraries are forbidden');
return;
}
if (this.libNames.filter(l => imp === `@${this.npmScope}/${l}`).length > 0) {
super.visitImportDeclaration(node);
return;
}
if (this.isRelativeImportIntoAnotherProject(imp) || this.isAbsoluteImportIntoAnotherProject(imp)) {
this.addFailureAt(node.getStart(), node.getWidth(), `library imports must start with @${this.npmScope}/`);
return;
}
const deepImport = this.libNames.filter(l => imp.startsWith(`@${this.npmScope}/${l}/`))[0];
if (deepImport) {
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden');
return;
}
const appImport = this.appNames.filter(
a => imp.startsWith(`@${this.npmScope}/${a}/`) || imp === `@${this.npmScope}/${a}`
)[0];
if (appImport) {
this.addFailureAt(node.getStart(), node.getWidth(), 'imports of apps are forbidden');
return;
}
super.visitImportDeclaration(node);
}
private isRelativeImportIntoAnotherProject(imp: string): boolean {
if (!this.isRelative(imp)) return false;
const sourceFile = this.getSourceFile().fileName.substring(this.projectPath.length);
const targetFile = path.resolve(path.dirname(sourceFile), imp).substring(1); // remove leading slash
if (!this.libraryRoot()) return false;
return !(targetFile.startsWith(`${this.libraryRoot()}/`) || targetFile === this.libraryRoot());
}
private libraryRoot(): string {
const sourceFile = this.getSourceFile().fileName.substring(this.projectPath.length + 1);
return this.roots.filter(r => sourceFile.startsWith(r))[0];
}
private isAbsoluteImportIntoAnotherProject(imp: string): boolean {
return imp.startsWith('libs/') || (imp.startsWith('/libs/') && imp.startsWith('apps/')) || imp.startsWith('/apps/');
}
private isRelative(s: string): boolean {
return s.startsWith('.');
}
}