Adds logic to sort app names from longest to shortest so deep import check of startsWith will work when more than one lib has the same starting string. Also adds spec to confirm the case.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 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 path?: string, private npmScope?: string, private appNames?: string[]) {
|
|
super(options);
|
|
if (!path) {
|
|
const cliConfig = this.readCliConfig();
|
|
this.path = process.cwd();
|
|
this.npmScope = cliConfig.project.npmScope;
|
|
this.appNames = cliConfig.apps.map(a => a.name);
|
|
}
|
|
}
|
|
|
|
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
|
return this.applyWithWalker(
|
|
new EnforceModuleBoundariesWalker(sourceFile, this.getOptions(), this.path, this.npmScope, this.appNames)
|
|
);
|
|
}
|
|
|
|
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 appNames: string[]
|
|
) {
|
|
super(sourceFile, options);
|
|
}
|
|
|
|
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(a => imp.startsWith(`@${this.npmScope}/${a}`))[0];
|
|
if (lazyLoaded) {
|
|
this.addFailureAt(node.getStart(), node.getWidth(), 'import of lazy-loaded libraries are forbidden');
|
|
return;
|
|
}
|
|
|
|
if (this.isRelative(imp) && this.isRelativeImportIntoAnotherProject(imp)) {
|
|
this.addFailureAt(node.getStart(), node.getWidth(), 'relative imports of libraries are forbidden');
|
|
return;
|
|
}
|
|
|
|
const deepImport = this.appNames.filter(a => imp.startsWith(`@${this.npmScope}/${a}/`))[0];
|
|
if (deepImport) {
|
|
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden');
|
|
return;
|
|
}
|
|
|
|
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('.');
|
|
}
|
|
}
|