feat(linter): allow banning of transitive dependencies from imports (#7906)
This commit is contained in:
parent
3add9caacf
commit
64d388e607
@ -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",
|
||||||
|
|||||||
@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user