129 lines
4.3 KiB
TypeScript
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('.');
|
|
}
|
|
}
|