Skip to content

Commit 49dcacf

Browse files
JamesHenryvsavkin
authored andcommitted
feat(eslint-plugin-nx): new package, ESLint enforce-module-boundaries rule
1 parent 8218914 commit 49dcacf

File tree

17 files changed

+1477
-190
lines changed

17 files changed

+1477
-190
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@types/webpack": "^4.4.24",
6969
"@types/yargs": "^11.0.0",
7070
"@typescript-eslint/eslint-plugin": "2.0.0-alpha.4",
71+
"@typescript-eslint/experimental-utils": "2.0.0-alpha.4",
7172
"@typescript-eslint/parser": "2.0.0-alpha.4",
7273
"angular": "1.6.6",
7374
"app-root-path": "^2.0.1",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@nrwl/eslint-plugin-nx",
3+
"version": "0.0.1",
4+
"description": "ESLint Plugin for Nx",
5+
"repository": {
6+
"type": "git",
7+
"url": "git+https://github.com/nrwl/nx.git"
8+
},
9+
"keywords": [
10+
"Monorepo",
11+
"Web",
12+
"Lint",
13+
"ESLint",
14+
"CLI"
15+
],
16+
"files": [
17+
"src",
18+
"package.json",
19+
"README.md",
20+
"LICENSE"
21+
],
22+
"main": "src/index.js",
23+
"types": "src/index.d.ts",
24+
"author": "Victor Savkin",
25+
"license": "MIT",
26+
"bugs": {
27+
"url": "https://github.com/nrwl/nx/issues"
28+
},
29+
"homepage": "https://nx.dev",
30+
"peerDependencies": {
31+
"@nrwl/workspace": "*",
32+
"@typescript-eslint/parser": "^2.0.0-alpha.4"
33+
},
34+
"dependencies": {
35+
"@typescript-eslint/experimental-utils": "2.0.0-alpha.4"
36+
}
37+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import enforceModuleBoundaries, {
2+
RULE_NAME as enforceModuleBoundariesRuleName
3+
} from './rules/enforce-module-boundaries';
4+
5+
module.exports = {
6+
rules: {
7+
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries
8+
}
9+
};
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { ProjectType } from '@nrwl/workspace/src/command-line/affected-apps';
2+
import { readDependencies } from '@nrwl/workspace/src/command-line/deps-calculator';
3+
import {
4+
getProjectNodes,
5+
normalizedProjectRoot,
6+
readNxJson,
7+
readWorkspaceJson
8+
} from '@nrwl/workspace/src/command-line/shared';
9+
import { appRootPath } from '@nrwl/workspace/src/utils/app-root';
10+
import {
11+
DepConstraint,
12+
findConstraintsFor,
13+
findProjectUsingImport,
14+
findSourceProject,
15+
getSourceFilePath,
16+
hasNoneOfTheseTags,
17+
isAbsoluteImportIntoAnotherProject,
18+
isCircular,
19+
isRelativeImportIntoAnotherProject,
20+
matchImportWithWildcard,
21+
onlyLoadChildren
22+
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
23+
import { TSESTree } from '@typescript-eslint/experimental-utils';
24+
import { createESLintRule } from '../utils/create-eslint-rule';
25+
26+
type Options = [
27+
{
28+
allow: string[];
29+
depConstraints: DepConstraint[];
30+
}
31+
];
32+
export type MessageIds =
33+
| 'noRelativeOrAbsoluteImportsAcrossLibraries'
34+
| 'noCircularDependencies'
35+
| 'noImportsOfApps'
36+
| 'noDeepImportsIntoLibraries'
37+
| 'noImportsOfLazyLoadedLibraries'
38+
| 'projectWithoutTagsCannotHaveDependencies'
39+
| 'tagConstraintViolation';
40+
export const RULE_NAME = 'enforce-module-boundaries';
41+
42+
export default createESLintRule<Options, MessageIds>({
43+
name: RULE_NAME,
44+
meta: {
45+
type: 'suggestion',
46+
docs: {
47+
description: `Ensure that module boundaries are respected within the monorepo`,
48+
category: 'Best Practices',
49+
recommended: 'error'
50+
},
51+
fixable: 'code',
52+
schema: [
53+
{
54+
type: 'object',
55+
properties: {
56+
allow: [{ type: 'string' }],
57+
depConstraints: [
58+
{
59+
type: 'object',
60+
properties: {
61+
sourceTag: { type: 'string' },
62+
onlyDependOnLibsWithTags: [{ type: 'string' }]
63+
},
64+
additionalProperties: false
65+
}
66+
]
67+
},
68+
additionalProperties: false
69+
}
70+
],
71+
messages: {
72+
noRelativeOrAbsoluteImportsAcrossLibraries: `Library imports must start with @{{npmScope}}/`,
73+
noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected`,
74+
noImportsOfApps: 'Imports of apps are forbidden',
75+
noDeepImportsIntoLibraries: 'Deep imports into libraries are forbidden',
76+
noImportsOfLazyLoadedLibraries: `Imports of lazy-loaded libraries are forbidden`,
77+
projectWithoutTagsCannotHaveDependencies: `A project without tags cannot depend on any libraries`,
78+
tagConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{allowedTags}}`
79+
}
80+
},
81+
defaultOptions: [
82+
{
83+
allow: [],
84+
depConstraints: []
85+
}
86+
],
87+
create(context, [{ allow, depConstraints }]) {
88+
/**
89+
* Globally cached info about workspace
90+
*/
91+
const projectPath = (global as any).projectPath || appRootPath;
92+
if (!(global as any).projectNodes) {
93+
const workspaceJson = readWorkspaceJson();
94+
const nxJson = readNxJson();
95+
(global as any).npmScope = nxJson.npmScope;
96+
(global as any).projectNodes = getProjectNodes(workspaceJson, nxJson);
97+
(global as any).deps = readDependencies(
98+
(global as any).npmScope,
99+
(global as any).projectNodes
100+
);
101+
}
102+
const npmScope = (global as any).npmScope;
103+
const projectNodes = (global as any).projectNodes;
104+
const deps = (global as any).deps;
105+
106+
projectNodes.sort((a, b) => {
107+
if (!a.root) return -1;
108+
if (!b.root) return -1;
109+
return a.root.length > b.root.length ? -1 : 1;
110+
});
111+
112+
return {
113+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
114+
const imp = (node.source as TSESTree.Literal).value as string;
115+
116+
const sourceFilePath = getSourceFilePath(
117+
context.getFilename(),
118+
projectPath
119+
);
120+
121+
// whitelisted import
122+
if (allow.some(a => matchImportWithWildcard(a, imp))) {
123+
return;
124+
}
125+
126+
// check for relative and absolute imports
127+
if (
128+
isRelativeImportIntoAnotherProject(
129+
imp,
130+
projectPath,
131+
projectNodes,
132+
sourceFilePath
133+
) ||
134+
isAbsoluteImportIntoAnotherProject(imp)
135+
) {
136+
context.report({
137+
node,
138+
messageId: 'noRelativeOrAbsoluteImportsAcrossLibraries',
139+
data: {
140+
npmScope
141+
}
142+
});
143+
return;
144+
}
145+
146+
// check constraints between libs and apps
147+
if (imp.startsWith(`@${npmScope}/`)) {
148+
// we should find the name
149+
const sourceProject = findSourceProject(projectNodes, sourceFilePath);
150+
// findProjectUsingImport to take care of same prefix
151+
const targetProject = findProjectUsingImport(
152+
projectNodes,
153+
npmScope,
154+
imp
155+
);
156+
157+
// something went wrong => return.
158+
if (!sourceProject || !targetProject) {
159+
return;
160+
}
161+
162+
// check for circular dependency
163+
if (isCircular(deps, sourceProject, targetProject)) {
164+
context.report({
165+
node,
166+
messageId: 'noCircularDependencies',
167+
data: {
168+
sourceProjectName: sourceProject.name,
169+
targetProjectName: targetProject.name
170+
}
171+
});
172+
return;
173+
}
174+
175+
// same project => allow
176+
if (sourceProject === targetProject) {
177+
return;
178+
}
179+
180+
// cannot import apps
181+
if (targetProject.type !== ProjectType.lib) {
182+
context.report({
183+
node,
184+
messageId: 'noImportsOfApps'
185+
});
186+
return;
187+
}
188+
189+
// deep imports aren't allowed
190+
if (imp !== `@${npmScope}/${normalizedProjectRoot(targetProject)}`) {
191+
context.report({
192+
node,
193+
messageId: 'noDeepImportsIntoLibraries'
194+
});
195+
return;
196+
}
197+
198+
// if we import a library using loadChildren, we should not import it using es6imports
199+
if (
200+
onlyLoadChildren(deps, sourceProject.name, targetProject.name, [])
201+
) {
202+
context.report({
203+
node,
204+
messageId: 'noImportsOfLazyLoadedLibraries'
205+
});
206+
return;
207+
}
208+
209+
// check that dependency constraints are satisfied
210+
if (depConstraints.length > 0) {
211+
const constraints = findConstraintsFor(
212+
depConstraints,
213+
sourceProject
214+
);
215+
// when no constrains found => error. Force the user to provision them.
216+
if (constraints.length === 0) {
217+
context.report({
218+
node,
219+
messageId: 'projectWithoutTagsCannotHaveDependencies'
220+
});
221+
return;
222+
}
223+
224+
for (let constraint of constraints) {
225+
if (
226+
hasNoneOfTheseTags(
227+
targetProject,
228+
constraint.onlyDependOnLibsWithTags || []
229+
)
230+
) {
231+
const allowedTags = constraint.onlyDependOnLibsWithTags
232+
.map(s => `"${s}"`)
233+
.join(', ');
234+
context.report({
235+
node,
236+
messageId: 'tagConstraintViolation',
237+
data: {
238+
sourceTag: constraint.sourceTag,
239+
allowedTags
240+
}
241+
});
242+
return;
243+
}
244+
}
245+
}
246+
}
247+
}
248+
};
249+
}
250+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
2+
3+
export const createESLintRule = ESLintUtils.RuleCreator(() => ``);

0 commit comments

Comments
 (0)