diff --git a/packages/eslint-plugin-nx/package.json b/packages/eslint-plugin-nx/package.json index b57d561fea..7760f8010d 100644 --- a/packages/eslint-plugin-nx/package.json +++ b/packages/eslint-plugin-nx/package.json @@ -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", diff --git a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts index e31d20aaf8..ae2dd11542 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts @@ -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( { diff --git a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts index 18b4e8a1fd..be867858a7 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts @@ -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({ @@ -70,6 +72,7 @@ export default createESLintRule({ properties: { enforceBuildableLibDependency: { type: 'boolean' }, allowCircularSelfDependency: { type: 'boolean' }, + banTransitiveDependencies: { type: 'boolean' }, allow: [{ type: 'string' }], depConstraints: [ { @@ -98,6 +101,7 @@ export default createESLintRule({ 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({ depConstraints: [], enforceBuildableLibDependency: false, allowCircularSelfDependency: false, + banTransitiveDependencies: false, }, ], create( @@ -116,6 +121,7 @@ export default createESLintRule({ depConstraints, enforceBuildableLibDependency, allowCircularSelfDependency, + banTransitiveDependencies, }, ] ) { @@ -238,6 +244,12 @@ export default createESLintRule({ // project => npm package if (targetProject.type === 'npm') { + if (banTransitiveDependencies && !isDirectDependency(targetProject)) { + context.report({ + node, + messageId: 'noTransitiveDependencies', + }); + } const constraint = hasBannedImport( sourceProject, targetProject, diff --git a/packages/workspace/src/utils/runtime-lint-utils.ts b/packages/workspace/src/utils/runtime-lint-utils.ts index 8b0f93b227..7c774f34c9 100644 --- a/packages/workspace/src/utils/runtime-lint-utils.ts +++ b/packages/workspace/src/utils/runtime-lint-utils.ts @@ -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 = ProjectGraphProjectNode & { 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 diff --git a/tsconfig.base.json b/tsconfig.base.json index 4637f80991..2277cca56d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"],