nx/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts
2020-01-09 19:13:53 -05:00

235 lines
6.7 KiB
TypeScript

import * as Lint from 'tslint';
import { IOptions } from 'tslint';
import * as ts from 'typescript';
import {
createProjectGraph,
ProjectGraph,
ProjectGraphNode
} from '../core/project-graph';
import { appRootPath } from '../utils/app-root';
import {
DepConstraint,
Deps,
findConstraintsFor,
findProjectUsingImport,
findSourceProject,
getSourceFilePath,
hasNoneOfTheseTags,
isAbsoluteImportIntoAnotherProject,
isCircular,
isRelativeImportIntoAnotherProject,
matchImportWithWildcard,
onlyLoadChildren
} from '../utils/runtime-lint-utils';
import { normalize } from '@angular-devkit/core';
import { ProjectType } from '../core/project-graph';
import {
normalizedProjectRoot,
readNxJson,
readWorkspaceJson
} from '@nrwl/workspace/src/core/file-utils';
import { TargetProjectLocator } from '../core/project-graph/build-dependencies/target-project-locator';
export class Rule extends Lint.Rules.AbstractRule {
constructor(
options: IOptions,
private readonly projectPath?: string,
private readonly npmScope?: string,
private readonly projectGraph?: ProjectGraph,
private readonly targetProjectLocator?: TargetProjectLocator
) {
super(options);
if (!projectPath) {
this.projectPath = normalize(appRootPath);
if (!(global as any).projectGraph) {
const workspaceJson = readWorkspaceJson();
const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = createProjectGraph(
workspaceJson,
nxJson
);
}
this.npmScope = (global as any).npmScope;
this.projectGraph = (global as any).projectGraph;
if (!(global as any).targetProjectLocator) {
(global as any).targetProjectLocator = new TargetProjectLocator(
this.projectGraph.nodes
);
}
this.targetProjectLocator = (global as any).targetProjectLocator;
}
}
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new EnforceModuleBoundariesWalker(
sourceFile,
this.getOptions(),
this.projectPath,
this.npmScope,
this.projectGraph,
this.targetProjectLocator
)
);
}
}
class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
private readonly allow: string[];
private readonly depConstraints: DepConstraint[];
constructor(
sourceFile: ts.SourceFile,
options: IOptions,
private readonly projectPath: string,
private readonly npmScope: string,
private readonly projectGraph: ProjectGraph,
private readonly targetProjectLocator: TargetProjectLocator
) {
super(sourceFile, options);
this.allow = Array.isArray(this.getOptions()[0].allow)
? this.getOptions()[0].allow.map(a => `${a}`)
: [];
this.depConstraints = Array.isArray(this.getOptions()[0].depConstraints)
? this.getOptions()[0].depConstraints
: [];
}
public visitImportDeclaration(node: ts.ImportDeclaration) {
const imp = node.moduleSpecifier
.getText()
.substring(1, node.moduleSpecifier.getText().length - 1);
// whitelisted import
if (this.allow.some(a => matchImportWithWildcard(a, imp))) {
super.visitImportDeclaration(node);
return;
}
// check for relative and absolute imports
if (
isRelativeImportIntoAnotherProject(
imp,
this.projectPath,
this.projectGraph,
getSourceFilePath(
normalize(this.getSourceFile().fileName),
this.projectPath
)
) ||
isAbsoluteImportIntoAnotherProject(imp)
) {
this.addFailureAt(
node.getStart(),
node.getWidth(),
`library imports must start with @${this.npmScope}/`
);
return;
}
// check constraints between libs and apps
if (imp.startsWith(`@${this.npmScope}/`)) {
// we should find the name
const filePath = getSourceFilePath(
this.getSourceFile().fileName,
this.projectPath
);
const sourceProject = findSourceProject(this.projectGraph, filePath);
// findProjectUsingImport to take care of same prefix
const targetProject = findProjectUsingImport(
this.projectGraph,
this.targetProjectLocator,
filePath,
imp,
this.npmScope
);
// something went wrong => return.
if (!sourceProject || !targetProject) {
super.visitImportDeclaration(node);
return;
}
// check for circular dependency
if (isCircular(this.projectGraph, sourceProject, targetProject)) {
const error = `Circular dependency between "${sourceProject.name}" and "${targetProject.name}" detected`;
this.addFailureAt(node.getStart(), node.getWidth(), error);
return;
}
// same project => allow
if (sourceProject === targetProject) {
super.visitImportDeclaration(node);
return;
}
// cannot import apps
if (targetProject.type !== ProjectType.lib) {
this.addFailureAt(
node.getStart(),
node.getWidth(),
'imports of apps are forbidden'
);
return;
}
// if we import a library using loadChildre, we should not import it using es6imports
if (
onlyLoadChildren(
this.projectGraph,
sourceProject.name,
targetProject.name,
[]
)
) {
this.addFailureAt(
node.getStart(),
node.getWidth(),
'imports of lazy-loaded libraries are forbidden'
);
return;
}
// check that dependency constraints are satisfied
if (this.depConstraints.length > 0) {
const constraints = findConstraintsFor(
this.depConstraints,
sourceProject
);
// when no constrains found => error. Force the user to provision them.
if (constraints.length === 0) {
this.addFailureAt(
node.getStart(),
node.getWidth(),
`A project without tags cannot depend on any libraries`
);
return;
}
for (let constraint of constraints) {
if (
hasNoneOfTheseTags(
targetProject,
constraint.onlyDependOnLibsWithTags || []
)
) {
const allowedTags = constraint.onlyDependOnLibsWithTags
.map(s => `"${s}"`)
.join(', ');
const error = `A project tagged with "${constraint.sourceTag}" can only depend on libs tagged with ${allowedTags}`;
this.addFailureAt(node.getStart(), node.getWidth(), error);
return;
}
}
}
}
super.visitImportDeclaration(node);
}
}