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); } }