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", "ESLint",
"CLI" "CLI"
], ],
"files": [
"src",
"package.json",
"README.md",
"LICENSE"
],
"main": "./src/index.js", "main": "./src/index.js",
"typings": "./src/index.d.ts", "typings": "./src/index.d.ts",
"author": "Victor Savkin", "author": "Victor Savkin",

View File

@ -49,6 +49,15 @@ const tsconfig = {
include: ['**/*.ts'], include: ['**/*.ts'],
}; };
const packageJson = {
dependencies: {
'npm-package': '2.3.4',
},
devDependencies: {
'npm-awesome-package': '1.2.3',
},
};
const fileSys = { const fileSys = {
'./libs/impl/src/index.ts': '', './libs/impl/src/index.ts': '',
'./libs/untagged/src/index.ts': '', './libs/untagged/src/index.ts': '',
@ -70,6 +79,7 @@ const fileSys = {
'./libs/buildableLib/src/main.ts': '', './libs/buildableLib/src/main.ts': '',
'./libs/nonBuildableLib/src/main.ts': '', './libs/nonBuildableLib/src/main.ts': '',
'./tsconfig.base.json': JSON.stringify(tsconfig), './tsconfig.base.json': JSON.stringify(tsconfig),
'./package.json': JSON.stringify(packageJson),
}; };
describe('Enforce Module Boundaries (eslint)', () => { describe('Enforce Module Boundaries (eslint)', () => {
@ -257,7 +267,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
type: 'npm', type: 'npm',
data: { data: {
packageName: 'npm-package', packageName: 'npm-package',
version: '0.0.0', version: '2.3.4',
}, },
}, },
'npm:npm-package2': { 'npm:npm-package2': {
@ -281,7 +291,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
type: 'npm', type: 'npm',
data: { data: {
packageName: 'npm-awesome-package', 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); 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', () => { it('should allow wildcards for defining forbidden npm packages', () => {
const failures = runRule( const failures = runRule(
{ {

View File

@ -14,6 +14,7 @@ import {
onlyLoadChildren, onlyLoadChildren,
MappedProjectGraph, MappedProjectGraph,
hasBannedImport, hasBannedImport,
isDirectDependency,
} from '@nrwl/workspace/src/utils/runtime-lint-utils'; } from '@nrwl/workspace/src/utils/runtime-lint-utils';
import { import {
AST_NODE_TYPES, AST_NODE_TYPES,
@ -22,7 +23,6 @@ import {
import { createESLintRule } from '../utils/create-eslint-rule'; import { createESLintRule } from '../utils/create-eslint-rule';
import { normalizePath } from '@nrwl/devkit'; import { normalizePath } from '@nrwl/devkit';
import { import {
isNpmProject,
ProjectType, ProjectType,
readCachedProjectGraph, readCachedProjectGraph,
} from '@nrwl/workspace/src/core/project-graph'; } from '@nrwl/workspace/src/core/project-graph';
@ -40,6 +40,7 @@ type Options = [
depConstraints: DepConstraint[]; depConstraints: DepConstraint[];
enforceBuildableLibDependency: boolean; enforceBuildableLibDependency: boolean;
allowCircularSelfDependency: boolean; allowCircularSelfDependency: boolean;
banTransitiveDependencies: boolean;
} }
]; ];
export type MessageIds = export type MessageIds =
@ -52,7 +53,8 @@ export type MessageIds =
| 'noImportsOfLazyLoadedLibraries' | 'noImportsOfLazyLoadedLibraries'
| 'projectWithoutTagsCannotHaveDependencies' | 'projectWithoutTagsCannotHaveDependencies'
| 'tagConstraintViolation' | 'tagConstraintViolation'
| 'bannedExternalImportsViolation'; | 'bannedExternalImportsViolation'
| 'noTransitiveDependencies';
export const RULE_NAME = 'enforce-module-boundaries'; export const RULE_NAME = 'enforce-module-boundaries';
export default createESLintRule<Options, MessageIds>({ export default createESLintRule<Options, MessageIds>({
@ -70,6 +72,7 @@ export default createESLintRule<Options, MessageIds>({
properties: { properties: {
enforceBuildableLibDependency: { type: 'boolean' }, enforceBuildableLibDependency: { type: 'boolean' },
allowCircularSelfDependency: { type: 'boolean' }, allowCircularSelfDependency: { type: 'boolean' },
banTransitiveDependencies: { type: 'boolean' },
allow: [{ type: 'string' }], allow: [{ type: 'string' }],
depConstraints: [ 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`, 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}}`, 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`, 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: [ defaultOptions: [
@ -106,6 +110,7 @@ export default createESLintRule<Options, MessageIds>({
depConstraints: [], depConstraints: [],
enforceBuildableLibDependency: false, enforceBuildableLibDependency: false,
allowCircularSelfDependency: false, allowCircularSelfDependency: false,
banTransitiveDependencies: false,
}, },
], ],
create( create(
@ -116,6 +121,7 @@ export default createESLintRule<Options, MessageIds>({
depConstraints, depConstraints,
enforceBuildableLibDependency, enforceBuildableLibDependency,
allowCircularSelfDependency, allowCircularSelfDependency,
banTransitiveDependencies,
}, },
] ]
) { ) {
@ -238,6 +244,12 @@ export default createESLintRule<Options, MessageIds>({
// project => npm package // project => npm package
if (targetProject.type === 'npm') { if (targetProject.type === 'npm') {
if (banTransitiveDependencies && !isDirectDependency(targetProject)) {
context.report({
node,
messageId: 'noTransitiveDependencies',
});
}
const constraint = hasBannedImport( const constraint = hasBannedImport(
sourceProject, sourceProject,
targetProject, targetProject,

View File

@ -1,5 +1,5 @@
import * as path from 'path'; import * as path from 'path';
import { FileData } from '../core/file-utils'; import { FileData, readFileIfExisting } from '../core/file-utils';
import { import {
ProjectGraph, ProjectGraph,
ProjectGraphDependency, ProjectGraphDependency,
@ -7,8 +7,12 @@ import {
ProjectGraphProjectNode, ProjectGraphProjectNode,
normalizePath, normalizePath,
DependencyType, DependencyType,
parseJson,
ProjectGraphExternalNode,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { TargetProjectLocator } from '../core/target-project-locator'; import { TargetProjectLocator } from '../core/target-project-locator';
import { join } from 'path';
import { appRootPath } from './app-root';
export type MappedProjectGraphNode<T = any> = ProjectGraphProjectNode<T> & { export type MappedProjectGraphNode<T = any> = ProjectGraphProjectNode<T> & {
data: { 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 * Maps import with wildcards to regex pattern
* @param importDefinition * @param importDefinition

View File

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