265 lines
7.9 KiB
TypeScript
265 lines
7.9 KiB
TypeScript
import { appRootPath } from '@nrwl/workspace/src/utils/app-root';
|
|
import {
|
|
DepConstraint,
|
|
findConstraintsFor,
|
|
findProjectUsingImport,
|
|
findSourceProject,
|
|
getSourceFilePath,
|
|
hasArchitectBuildBuilder,
|
|
hasNoneOfTheseTags,
|
|
isAbsoluteImportIntoAnotherProject,
|
|
isCircular,
|
|
isRelativeImportIntoAnotherProject,
|
|
matchImportWithWildcard,
|
|
onlyLoadChildren,
|
|
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
|
|
import { TSESTree } from '@typescript-eslint/experimental-utils';
|
|
import { createESLintRule } from '../utils/create-eslint-rule';
|
|
import { normalize } from '@angular-devkit/core';
|
|
import {
|
|
createProjectGraph,
|
|
ProjectGraph,
|
|
ProjectType,
|
|
} from '@nrwl/workspace/src/core/project-graph';
|
|
import {
|
|
readNxJson,
|
|
readWorkspaceJson,
|
|
} from '@nrwl/workspace/src/core/file-utils';
|
|
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
|
|
|
|
type Options = [
|
|
{
|
|
allow: string[];
|
|
depConstraints: DepConstraint[];
|
|
enforceBuildableLibDependency: boolean;
|
|
}
|
|
];
|
|
export type MessageIds =
|
|
| 'noRelativeOrAbsoluteImportsAcrossLibraries'
|
|
| 'noCircularDependencies'
|
|
| 'noImportsOfApps'
|
|
| 'noImportOfNonBuildableLibraries'
|
|
| 'noImportsOfLazyLoadedLibraries'
|
|
| 'projectWithoutTagsCannotHaveDependencies'
|
|
| 'tagConstraintViolation';
|
|
export const RULE_NAME = 'enforce-module-boundaries';
|
|
|
|
export default createESLintRule<Options, MessageIds>({
|
|
name: RULE_NAME,
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: `Ensure that module boundaries are respected within the monorepo`,
|
|
category: 'Best Practices',
|
|
recommended: 'error',
|
|
},
|
|
fixable: 'code',
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
enforceBuildableLibDependency: { type: 'boolean' },
|
|
allow: [{ type: 'string' }],
|
|
depConstraints: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
sourceTag: { type: 'string' },
|
|
onlyDependOnLibsWithTags: [{ type: 'string' }],
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
messages: {
|
|
noRelativeOrAbsoluteImportsAcrossLibraries: `Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope`,
|
|
noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected`,
|
|
noImportsOfApps: 'Imports of apps are forbidden',
|
|
noImportOfNonBuildableLibraries:
|
|
'Buildable libraries cannot import non-buildable libraries',
|
|
noImportsOfLazyLoadedLibraries: `Imports of lazy-loaded libraries are forbidden`,
|
|
projectWithoutTagsCannotHaveDependencies: `A project without tags cannot depend on any libraries`,
|
|
tagConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{allowedTags}}`,
|
|
},
|
|
},
|
|
defaultOptions: [
|
|
{
|
|
allow: [],
|
|
depConstraints: [],
|
|
enforceBuildableLibDependency: false,
|
|
},
|
|
],
|
|
create(context, [{ allow, depConstraints, enforceBuildableLibDependency }]) {
|
|
/**
|
|
* Globally cached info about workspace
|
|
*/
|
|
const projectPath = normalize((global as any).projectPath || 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);
|
|
}
|
|
const npmScope = (global as any).npmScope;
|
|
const projectGraph = (global as any).projectGraph as ProjectGraph;
|
|
|
|
if (!(global as any).targetProjectLocator) {
|
|
(global as any).targetProjectLocator = new TargetProjectLocator(
|
|
projectGraph.nodes
|
|
);
|
|
}
|
|
const targetProjectLocator = (global as any)
|
|
.targetProjectLocator as TargetProjectLocator;
|
|
|
|
return {
|
|
ImportDeclaration(node: TSESTree.ImportDeclaration) {
|
|
const imp = (node.source as TSESTree.Literal).value as string;
|
|
|
|
const sourceFilePath = getSourceFilePath(
|
|
normalize(context.getFilename()),
|
|
projectPath
|
|
);
|
|
|
|
// whitelisted import
|
|
if (allow.some((a) => matchImportWithWildcard(a, imp))) {
|
|
return;
|
|
}
|
|
|
|
// check for relative and absolute imports
|
|
if (
|
|
isRelativeImportIntoAnotherProject(
|
|
imp,
|
|
projectPath,
|
|
projectGraph,
|
|
sourceFilePath
|
|
) ||
|
|
isAbsoluteImportIntoAnotherProject(imp)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: 'noRelativeOrAbsoluteImportsAcrossLibraries',
|
|
data: {
|
|
npmScope,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const sourceProject = findSourceProject(projectGraph, sourceFilePath);
|
|
const targetProject = findProjectUsingImport(
|
|
projectGraph,
|
|
targetProjectLocator,
|
|
sourceFilePath,
|
|
imp,
|
|
npmScope
|
|
);
|
|
|
|
// If source or target are not part of an nx workspace, return.
|
|
if (!sourceProject || !targetProject) {
|
|
return;
|
|
}
|
|
|
|
// same project => allow
|
|
if (sourceProject === targetProject) {
|
|
return;
|
|
}
|
|
|
|
// check constraints between libs and apps
|
|
// check for circular dependency
|
|
if (isCircular(projectGraph, sourceProject, targetProject)) {
|
|
context.report({
|
|
node,
|
|
messageId: 'noCircularDependencies',
|
|
data: {
|
|
sourceProjectName: sourceProject.name,
|
|
targetProjectName: targetProject.name,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// cannot import apps
|
|
if (targetProject.type !== ProjectType.lib) {
|
|
context.report({
|
|
node,
|
|
messageId: 'noImportsOfApps',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// buildable-lib is not allowed to import non-buildable-lib
|
|
if (
|
|
enforceBuildableLibDependency === true &&
|
|
sourceProject.type === ProjectType.lib &&
|
|
targetProject.type === ProjectType.lib
|
|
) {
|
|
if (
|
|
hasArchitectBuildBuilder(sourceProject) &&
|
|
!hasArchitectBuildBuilder(targetProject)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: 'noImportOfNonBuildableLibraries',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// if we import a library using loadChildren, we should not import it using es6imports
|
|
if (
|
|
onlyLoadChildren(
|
|
projectGraph,
|
|
sourceProject.name,
|
|
targetProject.name,
|
|
[]
|
|
)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: 'noImportsOfLazyLoadedLibraries',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// check that dependency constraints are satisfied
|
|
if (depConstraints.length > 0) {
|
|
const constraints = findConstraintsFor(depConstraints, sourceProject);
|
|
// when no constrains found => error. Force the user to provision them.
|
|
if (constraints.length === 0) {
|
|
context.report({
|
|
node,
|
|
messageId: 'projectWithoutTagsCannotHaveDependencies',
|
|
});
|
|
return;
|
|
}
|
|
|
|
for (let constraint of constraints) {
|
|
if (
|
|
hasNoneOfTheseTags(
|
|
targetProject,
|
|
constraint.onlyDependOnLibsWithTags || []
|
|
)
|
|
) {
|
|
const allowedTags = constraint.onlyDependOnLibsWithTags
|
|
.map((s) => `"${s}"`)
|
|
.join(', ');
|
|
context.report({
|
|
node,
|
|
messageId: 'tagConstraintViolation',
|
|
data: {
|
|
sourceTag: constraint.sourceTag,
|
|
allowedTags,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|