From 0f6800b8c648684c6eb6f48b1d38c07cffcded82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Thu, 13 Aug 2026 10:59:34 +0200 Subject: [PATCH 01/10] fix(linter): flag banned external imports reached through internal projects With checkNestedExternalImports enabled, importing an internal project whose transitive dependencies include a package listed in bannedExternalImports never produced a violation. The subpath-matching guard added to isConstraintBanningProject for deep import bans compares the original import specifier against the external package name, and on the nested path that specifier names the imported internal project, so the guard never matched and the nested check was dead. Match transitive external dependencies against the external package's own name instead of the original import specifier, restoring the behavior the nested check had before the guard was introduced. The direct import path keeps the specifier-based subpath matching. --- .../rules/enforce-module-boundaries.spec.ts | 68 +++++++++++++++++++ .../src/rules/enforce-module-boundaries.ts | 3 +- .../src/utils/runtime-lint-utils.spec.ts | 43 +++--------- .../src/utils/runtime-lint-utils.ts | 5 +- 4 files changed, 81 insertions(+), 38 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts index 82a0ff6afe1..85992978d69 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts @@ -664,6 +664,74 @@ describe('Enforce Module Boundaries (eslint)', () => { expect(failures[1].message).toEqual(message); }); + it('should error when importing a project that transitively depends on a banned external package', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', bannedExternalImports: ['npm-package'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + import('@mycompany/impl'); + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-package', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + const message = + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import found at implName'; + expect(failures.length).toEqual(2); + expect(failures[0].message).toEqual(message); + expect(failures[1].message).toEqual(message); + }); + + it('should not error when importing a project whose transitive external dependencies are not banned', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', bannedExternalImports: ['npm-package'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + import('@mycompany/impl'); + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-awesome-package', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + expect(failures.length).toEqual(0); + }); + it('should error when importing transitive npm packages', () => { const failures = runRule( { diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts index 45c64aa67a7..f081e452b1d 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts @@ -760,8 +760,7 @@ export default ESLintUtils.RuleCreator( const matches = hasBannedDependencies( transitiveExternalDeps, projectGraph, - constraint, - imp + constraint ); if (matches.length > 0) { matches.forEach(([target, violatingSource, constraint]) => { diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts index 3b6a07fdc6f..85e17e896c4 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts @@ -312,15 +312,10 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () = it("should return empty array if any dependents don't have banned import", () => { expect( - hasBannedDependencies( - externalDependencies.slice(1), - graph, - { - sourceTag: 'a', - bannedExternalImports: ['angular'], - }, - 'react-native' - ) + hasBannedDependencies(externalDependencies.slice(1), graph, { + sourceTag: 'a', + bannedExternalImports: ['angular'], + }) ).toStrictEqual([]); }); @@ -331,12 +326,7 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () = }; expect( - hasBannedDependencies( - externalDependencies.slice(1), - graph, - constraint, - 'react-native' - ) + hasBannedDependencies(externalDependencies.slice(1), graph, constraint) ).toStrictEqual([[bannedTarget, d, constraint]]); }); @@ -347,12 +337,7 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () = }; expect( - hasBannedDependencies( - externalDependencies.slice(1), - graph, - constraint, - 'react' - ) + hasBannedDependencies(externalDependencies.slice(1), graph, constraint) ).toStrictEqual([ [nonBannedTarget, target, constraint], [nonBannedTarget, c, constraint], @@ -366,20 +351,12 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () = }; expect( - hasBannedDependencies( - externalDependencies.slice(1), - graph, - constraint, - 'react-native' - ).length + hasBannedDependencies(externalDependencies.slice(1), graph, constraint) + .length ).toBe(0); expect( - hasBannedDependencies( - externalDependencies.slice(1), - graph, - constraint, - 'react' - ).length + hasBannedDependencies(externalDependencies.slice(1), graph, constraint) + .length ).toBe(0); }); }); diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts index 5cba263bb9b..ddd8d09a285 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts @@ -363,8 +363,7 @@ export function findTransitiveExternalDependencies( export function hasBannedDependencies( externalDependencies: ProjectGraphDependency[], graph: ProjectGraph, - depConstraint: DepConstraint, - imp: string + depConstraint: DepConstraint ): | Array<[ProjectGraphExternalNode, ProjectGraphProjectNode, DepConstraint]> | undefined { @@ -373,7 +372,7 @@ export function hasBannedDependencies( isConstraintBanningProject( graph.externalNodes[dependency.target], depConstraint, - imp + graph.externalNodes[dependency.target].data.packageName ) ) .map((dep) => [ From 64ecc8d1e8b23317eef8fea811308548be6e0908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 24 Aug 2026 16:30:10 +0200 Subject: [PATCH 02/10] fix(linter): apply allowed external imports to nested dependencies The nested external-import check only ran for constraints with a bannedExternalImports list. That gate predates allowedExternalImports, so an allowed-only constraint never checked the transitive external dependencies of imported projects, while a direct import of a package outside the list was reported. The gate now accepts an allowed list too, including an empty one, which bans every external package. --- .../docs/kb/enforce-module-boundaries.mdoc | 20 ++--- .../rules/enforce-module-boundaries.spec.ts | 85 +++++++++++++++++++ .../src/rules/enforce-module-boundaries.ts | 4 +- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc index 365b514a114..5c742ca27d9 100644 --- a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc +++ b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc @@ -73,16 +73,16 @@ export default [ ## Options -| Property | Type | Default | Description | -| ---------------------------------- | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | -| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | -| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | -| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | -| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | -| checkNestedExternalImports | _boolean_ | _false_ | Enable to enforce the check for banned external imports in the nested packages. Check [Dependency constraints](#dependency-constraints) for more information | -| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | -| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | +| Property | Type | Default | Description | +| ---------------------------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | +| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | +| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | +| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | +| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | +| checkNestedExternalImports | _boolean_ | _false_ | Enable to also apply the `bannedExternalImports` and `allowedExternalImports` constraints to the external packages that imported projects depend on, transitively. Check [Dependency constraints](#dependency-constraints) for more information | +| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | +| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | ### Dependency constraints diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts index 85992978d69..bb5d5dce841 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts @@ -732,6 +732,91 @@ describe('Enforce Module Boundaries (eslint)', () => { expect(failures.length).toEqual(0); }); + describe.each([ + { + name: 'a mixed banned and allowed constraint', + constraint: { + sourceTag: 'api', + bannedExternalImports: ['npm-package'], + allowedExternalImports: ['npm-package2'], + }, + }, + { + name: 'an allowed-only constraint', + constraint: { + sourceTag: 'api', + allowedExternalImports: ['npm-package2'], + }, + }, + { + name: 'an empty allowed constraint', + constraint: { sourceTag: 'api', allowedExternalImports: [] }, + }, + ])('nested external imports with $name', ({ constraint }) => { + it('should error when importing a project that transitively depends on an external package outside the allowed list', () => { + const failures = runRule( + { depConstraints: [constraint], checkNestedExternalImports: true }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + import('@mycompany/impl'); + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-awesome-package', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + const message = + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import found at implName'; + expect(failures.length).toEqual(2); + expect(failures[0].message).toEqual(message); + expect(failures[1].message).toEqual(message); + }); + }); + + it('should not error when importing a project whose transitive external dependencies are in the allowed list', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', allowedExternalImports: ['npm-awesome-*'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + import('@mycompany/impl'); + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-awesome-package', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + expect(failures.length).toEqual(0); + }); + it('should error when importing transitive npm packages', () => { const failures = runRule( { diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts index f081e452b1d..006673a7530 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts @@ -754,8 +754,8 @@ export default ESLintUtils.RuleCreator( } if ( checkNestedExternalImports && - constraint.bannedExternalImports && - constraint.bannedExternalImports.length + (constraint.bannedExternalImports?.length || + constraint.allowedExternalImports) ) { const matches = hasBannedDependencies( transitiveExternalDeps, From f92e7a02a877a88bab6f8a628345bcddf03f584c Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:14:25 +0000 Subject: [PATCH 03/10] fix(linter): apply allowed external imports to nested dependencies [Self-Healing CI Rerun] From da7e71d99aaba813500fed9c8054bbf19a84f954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 31 Aug 2026 10:47:21 +0200 Subject: [PATCH 04/10] fix(linter): name the violating package in nested import violations The nested external-import violation only showed the internal import and the project owning the nested dependency. Under an allowed-only constraint the blamed import is itself permitted, and the package that matched the constraint never appeared in the error. The report now includes the matched package name in the message. --- .../rules/enforce-module-boundaries.spec.ts | 45 ++++++++++++++++++- .../src/rules/enforce-module-boundaries.ts | 3 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts index bb5d5dce841..d6c4b16fcbe 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts @@ -694,7 +694,7 @@ describe('Enforce Module Boundaries (eslint)', () => { ); const message = - 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import found at implName'; + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import of "npm-package" found at implName'; expect(failures.length).toEqual(2); expect(failures[0].message).toEqual(message); expect(failures[1].message).toEqual(message); @@ -778,7 +778,7 @@ describe('Enforce Module Boundaries (eslint)', () => { ); const message = - 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import found at implName'; + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import of "npm-awesome-package" found at implName'; expect(failures.length).toEqual(2); expect(failures[0].message).toEqual(message); expect(failures[1].message).toEqual(message); @@ -817,6 +817,47 @@ describe('Enforce Module Boundaries (eslint)', () => { expect(failures.length).toEqual(0); }); + it('should report the project owning the nested import when the banned package is more than one hop away', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', bannedExternalImports: ['npm-package'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'impl2Name', + type: DependencyType.static, + }, + ], + impl2Name: [ + { + source: 'impl2Name', + target: 'npm:npm-package', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + expect(failures.length).toEqual(1); + expect(failures[0].message).toEqual( + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import of "npm-package" found at impl2Name' + ); + }); + it('should error when importing transitive npm packages', () => { const failures = runRule( { diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts index 006673a7530..5ed36db3f26 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts @@ -189,7 +189,7 @@ export default ESLintUtils.RuleCreator( noImportsOfLazyLoadedLibraries: `Static imports of lazy-loaded libraries are forbidden.\n\nLibrary "{{targetProjectName}}" is lazy-loaded in these files:\n{{filePaths}}`, projectWithoutTagsCannotHaveDependencies: `A project without tags matching at least one constraint cannot depend on any libraries`, bannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import "{{imp}}"`, - nestedBannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import "{{imp}}". Nested import found at {{childProjectName}}`, + nestedBannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import "{{imp}}". Nested import of "{{packageName}}" found at {{childProjectName}}`, noTransitiveDependencies: `Only packages defined in the "package.json" can be imported. Transitive or unresolvable dependencies are not allowed.`, onlyTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{tags}}`, emptyOnlyTagsConstraintViolation: @@ -771,6 +771,7 @@ export default ESLintUtils.RuleCreator( sourceTag: isComboDepConstraint(constraint) ? constraint.allSourceTags.join('" and "') : constraint.sourceTag, + packageName: target.data.packageName, childProjectName: violatingSource.name, imp, }, From 12e2edad16147c68c02e7d9da9b9650a48081567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 31 Aug 2026 10:47:38 +0200 Subject: [PATCH 05/10] fix(linter): report nested import violations once per transitive package hasBannedDependencies returned one tuple per dependency edge, so an internal project reaching the same package through several edges (static and dynamic, or several resolved versions of the same package) produced byte-identical errors for a single import statement. findTransitiveExternalDependencies now keeps one edge per project and package, matching the uniqueness its jsdoc already promised. --- .../rules/enforce-module-boundaries.spec.ts | 89 +++++++++++++++++++ .../src/utils/runtime-lint-utils.ts | 7 +- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts index d6c4b16fcbe..8b912132de9 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts @@ -858,6 +858,95 @@ describe('Enforce Module Boundaries (eslint)', () => { ); }); + it('should report a single violation when multiple dependency edges reach the same banned package', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', bannedExternalImports: ['npm-package'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + `, + { + ...graph, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-package', + type: DependencyType.static, + }, + { + source: 'implName', + target: 'npm:npm-package', + type: DependencyType.dynamic, + }, + ], + }, + }, + fileMap + ); + + expect(failures.length).toEqual(1); + expect(failures[0].message).toEqual( + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import of "npm-package" found at implName' + ); + }); + + it('should report a single violation when multiple versions of the same banned package are reached', () => { + const failures = runRule( + { + depConstraints: [ + { sourceTag: 'api', bannedExternalImports: ['npm-package'] }, + ], + checkNestedExternalImports: true, + }, + `${process.cwd()}/proj/libs/api/src/index.ts`, + ` + import '@mycompany/impl'; + `, + { + ...graph, + externalNodes: { + ...graph.externalNodes, + 'npm:npm-package@2.0.0': { + name: 'npm:npm-package@2.0.0', + type: 'npm', + data: { + packageName: 'npm-package', + version: '2.0.0', + }, + }, + }, + dependencies: { + ...graph.dependencies, + implName: [ + { + source: 'implName', + target: 'npm:npm-package', + type: DependencyType.static, + }, + { + source: 'implName', + target: 'npm:npm-package@2.0.0', + type: DependencyType.static, + }, + ], + }, + }, + fileMap + ); + + expect(failures.length).toEqual(1); + expect(failures[0].message).toEqual( + 'A project tagged with "api" is not allowed to import "@mycompany/impl". Nested import of "npm-package" found at implName' + ); + }); + it('should error when importing transitive npm packages', () => { const failures = runRule( { diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts index ddd8d09a285..3fcdfb98009 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts @@ -338,13 +338,18 @@ export function findTransitiveExternalDependencies( } const externalDependencies = []; + const seen = new Set(); for (let i = 0; i < allReachableProjects.length; i++) { const dependencies = graph.dependencies[allReachableProjects[i]]; if (dependencies) { for (let d = 0; d < dependencies.length; d++) { const dependency = dependencies[d]; if (graph.externalNodes[dependency.target]) { - externalDependencies.push(dependency); + const key = `${dependency.source}|${graph.externalNodes[dependency.target].data.packageName}`; + if (!seen.has(key)) { + seen.add(key); + externalDependencies.push(dependency); + } } } } From fb6c9d973820b3a352e0623ad59c67ed205980e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 31 Aug 2026 10:47:44 +0200 Subject: [PATCH 06/10] cleanup(linter): remove a duplicated lint-utils test and clarify docs Dropping the imp parameter from hasBannedDependencies left two tests with identical inputs and expectations, so one is removed. The isConstraintBanningProject parameter is renamed to importSpecifier since it receives the bare package name on the nested path. The checkNestedExternalImports docs now state that nested matching is package-granular. --- .../docs/kb/enforce-module-boundaries.mdoc | 20 +++++++++---------- .../src/utils/runtime-lint-utils.spec.ts | 16 --------------- .../src/utils/runtime-lint-utils.ts | 13 +++++++----- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc index 5c742ca27d9..d1a79b42a9f 100644 --- a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc +++ b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc @@ -73,16 +73,16 @@ export default [ ## Options -| Property | Type | Default | Description | -| ---------------------------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | -| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | -| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | -| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | -| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | -| checkNestedExternalImports | _boolean_ | _false_ | Enable to also apply the `bannedExternalImports` and `allowedExternalImports` constraints to the external packages that imported projects depend on, transitively. Check [Dependency constraints](#dependency-constraints) for more information | -| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | -| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | +| Property | Type | Default | Description | +| ---------------------------------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | +| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | +| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | +| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | +| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | +| checkNestedExternalImports | _boolean_ | _false_ | Enable to also apply the `bannedExternalImports` and `allowedExternalImports` constraints to the external packages that imported projects depend on, transitively. The nested check matches whole package names, so subpath entries such as `lodash/fp` only apply to direct imports. Check [Dependency constraints](#dependency-constraints) for more information | +| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | +| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | ### Dependency constraints diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts index 85e17e896c4..b7a83378b5b 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts @@ -343,22 +343,6 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () = [nonBannedTarget, c, constraint], ]); }); - - it('should return undefined if no baneed external imports found', () => { - const constraint: DepConstraint = { - sourceTag: 'a', - bannedExternalImports: ['angular'], - }; - - expect( - hasBannedDependencies(externalDependencies.slice(1), graph, constraint) - .length - ).toBe(0); - expect( - hasBannedDependencies(externalDependencies.slice(1), graph, constraint) - .length - ).toBe(0); - }); }); describe('is terminal run', () => { diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts index 3fcdfb98009..a441ec0539c 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts @@ -267,19 +267,22 @@ export function getSourceFilePath(sourceFileName: string, projectPath: string) { function isConstraintBanningProject( externalProject: ProjectGraphExternalNode, constraint: DepConstraint, - imp: string + importSpecifier: string ): boolean { const { allowedExternalImports, bannedExternalImports } = constraint; const { packageName } = externalProject.data; - if (imp !== packageName && !imp.startsWith(`${packageName}/`)) { + if ( + importSpecifier !== packageName && + !importSpecifier.startsWith(`${packageName}/`) + ) { return false; } /* Check if import is banned... */ if ( bannedExternalImports?.some((importDefinition) => - mapGlobToRegExp(importDefinition).test(imp) + mapGlobToRegExp(importDefinition).test(importSpecifier) ) ) { return true; @@ -288,8 +291,8 @@ function isConstraintBanningProject( /* ... then check if there is a whitelist and if there is a match in the whitelist. */ return allowedExternalImports?.every( (importDefinition) => - !imp.startsWith(packageName) || - !mapGlobToRegExp(importDefinition).test(imp) + !importSpecifier.startsWith(packageName) || + !mapGlobToRegExp(importDefinition).test(importSpecifier) ); } From 601de4a5ce28e00faa7eaf36bacba8e0ed83d995 Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:04:38 +0000 Subject: [PATCH 07/10] cleanup(linter): remove a duplicated lint-utils test and clarify docs [Self-Healing CI Rerun] From f923abfa4a474532f2861ad37b281b4f23a8f356 Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:12:43 +0000 Subject: [PATCH 08/10] cleanup(linter): remove a duplicated lint-utils test and clarify docs [Self-Healing CI Rerun] From 28804df5b99337f72e8b2edc996f9abb45be1a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 1 Sep 2026 10:17:26 +0200 Subject: [PATCH 09/10] docs(linter): split the nested-check subpath note by constraint type A subpath-only allowedExternalImports entry does not whitelist its package for the nested check, so the nested dependency is reported. The previous wording claimed subpath entries were inert on the nested path for both constraint types, which only holds for bannedExternalImports. --- .../docs/kb/enforce-module-boundaries.mdoc | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc index d1a79b42a9f..6dac20b21ae 100644 --- a/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc +++ b/astro-docs/src/content/docs/kb/enforce-module-boundaries.mdoc @@ -73,16 +73,16 @@ export default [ ## Options -| Property | Type | Default | Description | -| ---------------------------------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | -| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | -| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | -| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | -| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | -| checkNestedExternalImports | _boolean_ | _false_ | Enable to also apply the `bannedExternalImports` and `allowedExternalImports` constraints to the external packages that imported projects depend on, transitively. The nested check matches whole package names, so subpath entries such as `lodash/fp` only apply to direct imports. Check [Dependency constraints](#dependency-constraints) for more information | -| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | -| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | +| Property | Type | Default | Description | +| ---------------------------------- | ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allow | _Array_ | _[]_ | List of imports that should be allowed without any checks | +| allowCircularSelfDependency | _boolean_ | _false_ | Disable check for self circular dependency when project imports from itself via alias path | +| banTransitiveDependencies | _boolean_ | _false_ | Ban import of dependencies that were not specified in the root or project's `package.json` | +| ignoredCircularDependencies | _Array<[string, string]>_ | _[]_ | List of project pairs that should be skipped from `Circular dependencies` checks, including the self-circular dependency check. E.g. `['feature-project-a', 'myapp']`. Project name can be replaced by catch all `*` for more generic matches. | +| checkDynamicDependenciesExceptions | _Array_ | _[]_ | List of imports that should be skipped for `Imports of lazy-loaded libraries forbidden` checks. E.g. `['@myorg/lazy-project/component/*', '@myorg/other-project']` | +| checkNestedExternalImports | _boolean_ | _false_ | Enable to also apply the `bannedExternalImports` and `allowedExternalImports` constraints to the external packages that imported projects depend on, transitively. The nested check compares whole package names. In `bannedExternalImports`, a subpath entry such as `lodash/fp` only applies to direct imports. In `allowedExternalImports`, a subpath entry does not allow the package for the nested check, so the rule reports the nested dependency. Check [Dependency constraints](#dependency-constraints) for more information | +| enforceBuildableLibDependency | _boolean_ | _false_ | Enable to restrict the buildable libs from importing non-buildable libraries | +| depConstraints | _Array_ | _[]_ | List of dependency constraints between projects | ### Dependency constraints From bd0187cfc92295096b2aa79e8f467adcd71b1389 Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:21:06 +0000 Subject: [PATCH 10/10] docs(linter): split the nested-check subpath note by constraint type [Self-Healing CI Rerun]