feat(linter): allow banning of transitive dependencies from imports (#7906)

This commit is contained in:
Miroslav Jonaš 2021-11-30 06:34:32 -06:00 committed by GitHub
parent 3add9caacf
commit 64d388e607
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 11 deletions

View File

@ -14,12 +14,6 @@
"ESLint",
"CLI"
],
"files": [
"src",
"package.json",
"README.md",
"LICENSE"
],
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"author": "Victor Savkin",

View File

@ -49,6 +49,15 @@ const tsconfig = {
include: ['**/*.ts'],
};
const packageJson = {
dependencies: {
'npm-package': '2.3.4',
},
devDependencies: {
'npm-awesome-package': '1.2.3',
},
};
const fileSys = {
'./libs/impl/src/index.ts': '',
'./libs/untagged/src/index.ts': '',
@ -70,6 +79,7 @@ const fileSys = {
'./libs/buildableLib/src/main.ts': '',
'./libs/nonBuildableLib/src/main.ts': '',
'./tsconfig.base.json': JSON.stringify(tsconfig),
'./package.json': JSON.stringify(packageJson),
};
describe('Enforce Module Boundaries (eslint)', () => {
@ -257,7 +267,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
type: 'npm',
data: {
packageName: 'npm-package',
version: '0.0.0',
version: '2.3.4',
},
},
'npm:npm-package2': {
@ -281,7 +291,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
type: 'npm',
data: {
packageName: 'npm-awesome-package',
version: '0.0.0',
version: '1.2.3',
},
},
},
@ -355,6 +365,46 @@ describe('Enforce Module Boundaries (eslint)', () => {
expect(failures[1].message).toEqual(message);
});
it('should error when importing transitive npm packages', () => {
const failures = runRule(
{
...depConstraints,
banTransitiveDependencies: true,
},
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import 'npm-package2';
import('npm-package2');
`,
graph
);
const message =
'Transitive dependencies are not allowed. Only packages defined in the "package.json" can be imported';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should not error when importing direct npm dependencies', () => {
const failures = runRule(
{
...depConstraints,
banTransitiveDependencies: true,
},
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import 'npm-package';
import('npm-package');
import 'npm-awesome-package';
import('npm-awesome-package');
`,
graph
);
expect(failures.length).toEqual(0);
});
it('should allow wildcards for defining forbidden npm packages', () => {
const failures = runRule(
{

View File

@ -14,6 +14,7 @@ import {
onlyLoadChildren,
MappedProjectGraph,
hasBannedImport,
isDirectDependency,
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
import {
AST_NODE_TYPES,
@ -22,7 +23,6 @@ import {
import { createESLintRule } from '../utils/create-eslint-rule';
import { normalizePath } from '@nrwl/devkit';
import {
isNpmProject,
ProjectType,
readCachedProjectGraph,
} from '@nrwl/workspace/src/core/project-graph';
@ -40,6 +40,7 @@ type Options = [
depConstraints: DepConstraint[];
enforceBuildableLibDependency: boolean;
allowCircularSelfDependency: boolean;
banTransitiveDependencies: boolean;
}
];
export type MessageIds =
@ -52,7 +53,8 @@ export type MessageIds =
| 'noImportsOfLazyLoadedLibraries'
| 'projectWithoutTagsCannotHaveDependencies'
| 'tagConstraintViolation'
| 'bannedExternalImportsViolation';
| 'bannedExternalImportsViolation'
| 'noTransitiveDependencies';
export const RULE_NAME = 'enforce-module-boundaries';
export default createESLintRule<Options, MessageIds>({
@ -70,6 +72,7 @@ export default createESLintRule<Options, MessageIds>({
properties: {
enforceBuildableLibDependency: { type: 'boolean' },
allowCircularSelfDependency: { type: 'boolean' },
banTransitiveDependencies: { type: 'boolean' },
allow: [{ type: 'string' }],
depConstraints: [
{
@ -98,6 +101,7 @@ export default createESLintRule<Options, MessageIds>({
projectWithoutTagsCannotHaveDependencies: `A project without tags matching at least one constraint cannot depend on any libraries`,
tagConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{allowedTags}}`,
bannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import the "{{package}}" package`,
noTransitiveDependencies: `Transitive dependencies are not allowed. Only packages defined in the "package.json" can be imported`,
},
},
defaultOptions: [
@ -106,6 +110,7 @@ export default createESLintRule<Options, MessageIds>({
depConstraints: [],
enforceBuildableLibDependency: false,
allowCircularSelfDependency: false,
banTransitiveDependencies: false,
},
],
create(
@ -116,6 +121,7 @@ export default createESLintRule<Options, MessageIds>({
depConstraints,
enforceBuildableLibDependency,
allowCircularSelfDependency,
banTransitiveDependencies,
},
]
) {
@ -238,6 +244,12 @@ export default createESLintRule<Options, MessageIds>({
// project => npm package
if (targetProject.type === 'npm') {
if (banTransitiveDependencies && !isDirectDependency(targetProject)) {
context.report({
node,
messageId: 'noTransitiveDependencies',
});
}
const constraint = hasBannedImport(
sourceProject,
targetProject,

View File

@ -1,5 +1,5 @@
import * as path from 'path';
import { FileData } from '../core/file-utils';
import { FileData, readFileIfExisting } from '../core/file-utils';
import {
ProjectGraph,
ProjectGraphDependency,
@ -7,8 +7,12 @@ import {
ProjectGraphProjectNode,
normalizePath,
DependencyType,
parseJson,
ProjectGraphExternalNode,
} from '@nrwl/devkit';
import { TargetProjectLocator } from '../core/target-project-locator';
import { join } from 'path';
import { appRootPath } from './app-root';
export type MappedProjectGraphNode<T = any> = ProjectGraphProjectNode<T> & {
data: {
@ -193,6 +197,27 @@ export function hasBannedImport(
);
}
export function isDirectDependency(target: ProjectGraphExternalNode): boolean {
const fileName = 'package.json';
const content = readFileIfExisting(join(appRootPath, fileName));
if (content) {
const { dependencies, devDependencies, peerDependencies } =
parseJson(content);
if (dependencies && dependencies[target.data.packageName]) {
return true;
}
if (peerDependencies && peerDependencies[target.data.packageName]) {
return true;
}
if (devDependencies && devDependencies[target.data.packageName]) {
return true;
}
return false;
}
return true;
}
/**
* Maps import with wildcards to regex pattern
* @param importDefinition

View File

@ -28,6 +28,7 @@
"@nrwl/devkit/testing": ["./packages/devkit/testing"],
"@nrwl/e2e/cli": ["./e2e/cli"],
"@nrwl/e2e/utils": ["./e2e/utils"],
"@nrwl/eslint-plugin-nx": ["./packages/eslint-plugin-nx/src"],
"@nrwl/express": ["./packages/express"],
"@nrwl/gatsby": ["./packages/gatsby"],
"@nrwl/jest": ["./packages/jest"],