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 3e872108c7..fbd9ab220b 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts @@ -19,28 +19,27 @@ import { import { createESLintRule } from '../utils/create-eslint-rule'; import { normalizePath } from '@nrwl/devkit'; import { - createProjectGraph, isNpmProject, ProjectGraph, ProjectType, } from '@nrwl/workspace/src/core/project-graph'; -import { - readNxJson, - readWorkspaceJson, -} from '@nrwl/workspace/src/core/file-utils'; +import { readNxJson } from '@nrwl/workspace/src/core/file-utils'; import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator'; import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils'; import { readCurrentProjectGraph } from '@nrwl/workspace/src/core/project-graph/project-graph'; +import { isRelativePath } from '@nrwl/workspace/src/utilities/fileutils'; type Options = [ { allow: string[]; depConstraints: DepConstraint[]; enforceBuildableLibDependency: boolean; + allowCircularSelfDependency: boolean; } ]; export type MessageIds = | 'noRelativeOrAbsoluteImportsAcrossLibraries' + | 'noSelfCircularDependencies' | 'noCircularDependencies' | 'noImportsOfApps' | 'noImportsOfE2e' @@ -83,6 +82,7 @@ export default createESLintRule({ messages: { noRelativeOrAbsoluteImportsAcrossLibraries: `Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope`, noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected: {{path}}`, + noSelfCircularDependencies: `Only relative imports are allowed within the project. Absolute import found: {{imp}}`, noImportsOfApps: 'Imports of apps are forbidden', noImportsOfE2e: 'Imports of e2e projects are forbidden', noImportOfNonBuildableLibraries: @@ -97,9 +97,20 @@ export default createESLintRule({ allow: [], depConstraints: [], enforceBuildableLibDependency: false, + allowCircularSelfDependency: false, }, ], - create(context, [{ allow, depConstraints, enforceBuildableLibDependency }]) { + create( + context, + [ + { + allow, + depConstraints, + enforceBuildableLibDependency, + allowCircularSelfDependency, + }, + ] + ) { /** * Globally cached info about workspace */ @@ -193,6 +204,16 @@ export default createESLintRule({ // same project => allow if (sourceProject === targetProject) { + // we only allow relative paths within the same project + if (!allowCircularSelfDependency && !isRelativePath(imp)) { + context.report({ + node, + messageId: 'noSelfCircularDependencies', + data: { + imp, + }, + }); + } return; } diff --git a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts index 5fe9910761..430676f37a 100644 --- a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts @@ -6,12 +6,11 @@ import { import { TSESLint } from '@typescript-eslint/experimental-utils'; import * as parser from '@typescript-eslint/parser'; import { vol } from 'memfs'; -import { extname, join } from 'path'; +import { extname } from 'path'; import enforceModuleBoundaries, { RULE_NAME as enforceModuleBoundariesRuleName, } from '../../src/rules/enforce-module-boundaries'; import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator'; -import { readFileSync } from 'fs'; jest.mock('fs', () => require('memfs').fs); jest.mock('../../../workspace/src/utilities/app-root', () => ({ appRootPath: '/root', @@ -794,6 +793,130 @@ describe('Enforce Module Boundaries', () => { expect(failures[1].message).toEqual(message); }); + it('should error when absolute path within project detected', () => { + const failures = runRule( + {}, + `${process.cwd()}/proj/libs/mylib/src/main.ts`, + ` + import '@mycompany/mylib'; + import('@mycompany/mylib'); + `, + { + nodes: { + mylibName: { + name: 'mylibName', + type: ProjectType.lib, + data: { + root: 'libs/mylib', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`libs/mylib/src/main.ts`)], + }, + }, + anotherlibName: { + name: 'anotherlibName', + type: ProjectType.lib, + data: { + root: 'libs/anotherlib', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`libs/anotherlib/src/main.ts`)], + }, + }, + myappName: { + name: 'myappName', + type: ProjectType.app, + data: { + root: 'apps/myapp', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`apps/myapp/src/index.ts`)], + }, + }, + }, + dependencies: { + mylibName: [ + { + source: 'mylibName', + target: 'anotherlibName', + type: DependencyType.static, + }, + ], + }, + } + ); + + const message = + 'Only relative imports are allowed within the project. Absolute import found: @mycompany/mylib'; + expect(failures.length).toEqual(2); + expect(failures[0].message).toEqual(message); + expect(failures[1].message).toEqual(message); + }); + + it('should ignore detected absolute path within project if allowCircularSelfDependency flag is set', () => { + const failures = runRule( + { + allowCircularSelfDependency: true, + }, + `${process.cwd()}/proj/libs/mylib/src/main.ts`, + ` + import '@mycompany/mylib'; + import('@mycompany/mylib'); + `, + { + nodes: { + mylibName: { + name: 'mylibName', + type: ProjectType.lib, + data: { + root: 'libs/mylib', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`libs/mylib/src/main.ts`)], + }, + }, + anotherlibName: { + name: 'anotherlibName', + type: ProjectType.lib, + data: { + root: 'libs/anotherlib', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`libs/anotherlib/src/main.ts`)], + }, + }, + myappName: { + name: 'myappName', + type: ProjectType.app, + data: { + root: 'apps/myapp', + tags: [], + implicitDependencies: [], + architect: {}, + files: [createFile(`apps/myapp/src/index.ts`)], + }, + }, + }, + dependencies: { + mylibName: [ + { + source: 'mylibName', + target: 'anotherlibName', + type: DependencyType.static, + }, + ], + }, + } + ); + + expect(failures.length).toBe(0); + }); + it('should error when circular dependency detected', () => { const failures = runRule( {},