feat(linter): allow banning of deep/secondary paths (#17755)
This commit is contained in:
parent
50d01d1567
commit
6b82a2ff59
@ -465,7 +465,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
|
||||
);
|
||||
|
||||
const message =
|
||||
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
||||
'A project tagged with "api" is not allowed to import "npm-package"';
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message);
|
||||
expect(failures[1].message).toEqual(message);
|
||||
@ -507,7 +507,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
|
||||
);
|
||||
|
||||
const message =
|
||||
'A project tagged with "api" is not allowed to import the "npm-awesome-package" package';
|
||||
'A project tagged with "api" is not allowed to import "npm-awesome-package"';
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message);
|
||||
expect(failures[1].message).toEqual(message);
|
||||
@ -549,7 +549,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
|
||||
);
|
||||
|
||||
const message =
|
||||
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
||||
'A project tagged with "api" is not allowed to import "npm-package"';
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message);
|
||||
expect(failures[1].message).toEqual(message);
|
||||
@ -570,7 +570,57 @@ describe('Enforce Module Boundaries (eslint)', () => {
|
||||
);
|
||||
|
||||
const message =
|
||||
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
||||
'A project tagged with "api" is not allowed to import "npm-package"';
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message);
|
||||
expect(failures[1].message).toEqual(message);
|
||||
});
|
||||
|
||||
it('should not error when importing package nested allowed route', () => {
|
||||
const failures = runRule(
|
||||
{
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'api',
|
||||
allowedExternalImports: ['npm-package/*'],
|
||||
bannedExternalImports: ['npm-package/testing'],
|
||||
},
|
||||
],
|
||||
},
|
||||
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
||||
`
|
||||
import 'npm-package/allowed';
|
||||
import('npm-package/allowed');
|
||||
`,
|
||||
graph,
|
||||
fileMap
|
||||
);
|
||||
|
||||
expect(failures.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should error when importing package nested forbidden route', () => {
|
||||
const failures = runRule(
|
||||
{
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'api',
|
||||
allowedExternalImports: ['npm-package/*'],
|
||||
bannedExternalImports: ['npm-package/testing'],
|
||||
},
|
||||
],
|
||||
},
|
||||
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
||||
`
|
||||
import 'npm-package/testing';
|
||||
import('npm-package/testing');
|
||||
`,
|
||||
graph,
|
||||
fileMap
|
||||
);
|
||||
|
||||
const message =
|
||||
'A project tagged with "api" is not allowed to import "npm-package/testing"';
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message);
|
||||
expect(failures[1].message).toEqual(message);
|
||||
@ -637,7 +687,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
|
||||
);
|
||||
|
||||
const message = (packageName) =>
|
||||
`A project tagged with "api" is not allowed to import the "${packageName}" package`;
|
||||
`A project tagged with "api" is not allowed to import "${packageName}"`;
|
||||
expect(failures.length).toEqual(2);
|
||||
expect(failures[0].message).toEqual(message('npm-package'));
|
||||
expect(failures[1].message).toEqual(message('npm-awesome-package'));
|
||||
|
||||
@ -128,8 +128,8 @@ export default createESLintRule<Options, MessageIds>({
|
||||
'Buildable libraries cannot import or export from non-buildable libraries',
|
||||
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 the "{{package}}" package`,
|
||||
nestedBannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import the "{{package}}" package. Nested import found at {{childProjectName}}`,
|
||||
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}}`,
|
||||
noTransitiveDependencies: `Transitive dependencies are not allowed. Only packages defined in the "package.json" can be imported`,
|
||||
onlyTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{tags}}`,
|
||||
emptyOnlyTagsConstraintViolation:
|
||||
@ -405,7 +405,8 @@ export default createESLintRule<Options, MessageIds>({
|
||||
const constraint = hasBannedImport(
|
||||
sourceProject,
|
||||
targetProject,
|
||||
depConstraints
|
||||
depConstraints,
|
||||
imp
|
||||
);
|
||||
if (constraint) {
|
||||
context.report({
|
||||
@ -415,7 +416,7 @@ export default createESLintRule<Options, MessageIds>({
|
||||
sourceTag: isComboDepConstraint(constraint)
|
||||
? constraint.allSourceTags.join('" and "')
|
||||
: constraint.sourceTag,
|
||||
package: targetProject.data.packageName,
|
||||
imp,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -624,19 +625,20 @@ export default createESLintRule<Options, MessageIds>({
|
||||
const matches = hasBannedDependencies(
|
||||
transitiveExternalDeps,
|
||||
projectGraph,
|
||||
constraint
|
||||
constraint,
|
||||
imp
|
||||
);
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(([target, violatingSource, constraint]) => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'bannedExternalImportsViolation',
|
||||
messageId: 'nestedBannedExternalImportsViolation',
|
||||
data: {
|
||||
sourceTag: isComboDepConstraint(constraint)
|
||||
? constraint.allSourceTags.join('" and "')
|
||||
: constraint.sourceTag,
|
||||
childProjectName: violatingSource.name,
|
||||
package: target.data.packageName,
|
||||
imp,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -138,7 +138,9 @@ describe('hasBannedImport', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(hasBannedImport(source, target, constraints)).toBe(constraints[1]);
|
||||
expect(hasBannedImport(source, target, constraints, 'react-native')).toBe(
|
||||
constraints[1]
|
||||
);
|
||||
});
|
||||
|
||||
it('should return just first DepConstraint banning given target', () => {
|
||||
@ -156,7 +158,9 @@ describe('hasBannedImport', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(hasBannedImport(source, target, constraints)).toBe(constraints[1]);
|
||||
expect(hasBannedImport(source, target, constraints, 'react-native')).toBe(
|
||||
constraints[1]
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null if tag does not match', () => {
|
||||
@ -174,7 +178,9 @@ describe('hasBannedImport', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(hasBannedImport(source, target, constraints)).toBe(undefined);
|
||||
expect(hasBannedImport(source, target, constraints, 'react-native')).toBe(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null if packages does not match', () => {
|
||||
@ -188,7 +194,9 @@ describe('hasBannedImport', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(hasBannedImport(source, target, constraints)).toBe(undefined);
|
||||
expect(hasBannedImport(source, target, constraints, 'react-native')).toBe(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -298,12 +306,17 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () =
|
||||
);
|
||||
});
|
||||
|
||||
it('should return target and constraint pair if any dependents have banned import', () => {
|
||||
it("should return empty array if any dependents don't have banned import", () => {
|
||||
expect(
|
||||
hasBannedDependencies(externalDependencies.slice(1), graph, {
|
||||
hasBannedDependencies(
|
||||
externalDependencies.slice(1),
|
||||
graph,
|
||||
{
|
||||
sourceTag: 'a',
|
||||
bannedExternalImports: ['angular'],
|
||||
})
|
||||
},
|
||||
'react-native'
|
||||
)
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
@ -314,7 +327,12 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () =
|
||||
};
|
||||
|
||||
expect(
|
||||
hasBannedDependencies(externalDependencies.slice(1), graph, constraint)
|
||||
hasBannedDependencies(
|
||||
externalDependencies.slice(1),
|
||||
graph,
|
||||
constraint,
|
||||
'react-native'
|
||||
)
|
||||
).toStrictEqual([[bannedTarget, d, constraint]]);
|
||||
});
|
||||
|
||||
@ -325,7 +343,12 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () =
|
||||
};
|
||||
|
||||
expect(
|
||||
hasBannedDependencies(externalDependencies.slice(1), graph, constraint)
|
||||
hasBannedDependencies(
|
||||
externalDependencies.slice(1),
|
||||
graph,
|
||||
constraint,
|
||||
'react'
|
||||
)
|
||||
).toStrictEqual([
|
||||
[nonBannedTarget, target, constraint],
|
||||
[nonBannedTarget, c, constraint],
|
||||
@ -339,8 +362,20 @@ describe('dependentsHaveBannedImport + findTransitiveExternalDependencies', () =
|
||||
};
|
||||
|
||||
expect(
|
||||
hasBannedDependencies(externalDependencies.slice(1), graph, constraint)
|
||||
.length
|
||||
hasBannedDependencies(
|
||||
externalDependencies.slice(1),
|
||||
graph,
|
||||
constraint,
|
||||
'react-native'
|
||||
).length
|
||||
).toBe(0);
|
||||
expect(
|
||||
hasBannedDependencies(
|
||||
externalDependencies.slice(1),
|
||||
graph,
|
||||
constraint,
|
||||
'react'
|
||||
).length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -234,15 +234,20 @@ export function getSourceFilePath(sourceFileName: string, projectPath: string) {
|
||||
*/
|
||||
function isConstraintBanningProject(
|
||||
externalProject: ProjectGraphExternalNode,
|
||||
constraint: DepConstraint
|
||||
constraint: DepConstraint,
|
||||
imp: string
|
||||
): boolean {
|
||||
const { allowedExternalImports, bannedExternalImports } = constraint;
|
||||
const { packageName } = externalProject.data;
|
||||
|
||||
if (imp !== packageName && !imp.startsWith(`${packageName}/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check if import is banned... */
|
||||
if (
|
||||
bannedExternalImports?.some((importDefinition) =>
|
||||
mapGlobToRegExp(importDefinition).test(packageName)
|
||||
mapGlobToRegExp(importDefinition).test(imp)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
@ -250,14 +255,17 @@ function isConstraintBanningProject(
|
||||
|
||||
/* ... then check if there is a whitelist and if there is a match in the whitelist. */
|
||||
return allowedExternalImports?.every(
|
||||
(importDefinition) => !mapGlobToRegExp(importDefinition).test(packageName)
|
||||
(importDefinition) =>
|
||||
!imp.startsWith(packageName) ||
|
||||
!mapGlobToRegExp(importDefinition).test(imp)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasBannedImport(
|
||||
source: ProjectGraphProjectNode,
|
||||
target: ProjectGraphExternalNode,
|
||||
depConstraints: DepConstraint[]
|
||||
depConstraints: DepConstraint[],
|
||||
imp: string
|
||||
): DepConstraint | undefined {
|
||||
// return those constraints that match source project
|
||||
depConstraints = depConstraints.filter((c) => {
|
||||
@ -271,7 +279,7 @@ export function hasBannedImport(
|
||||
return tags.every((t) => hasTag(source, t));
|
||||
});
|
||||
return depConstraints.find((constraint) =>
|
||||
isConstraintBanningProject(target, constraint)
|
||||
isConstraintBanningProject(target, constraint, imp)
|
||||
);
|
||||
}
|
||||
|
||||
@ -323,7 +331,8 @@ export function findTransitiveExternalDependencies(
|
||||
export function hasBannedDependencies(
|
||||
externalDependencies: ProjectGraphDependency[],
|
||||
graph: ProjectGraph,
|
||||
depConstraint: DepConstraint
|
||||
depConstraint: DepConstraint,
|
||||
imp: string
|
||||
):
|
||||
| Array<[ProjectGraphExternalNode, ProjectGraphProjectNode, DepConstraint]>
|
||||
| undefined {
|
||||
@ -331,7 +340,8 @@ export function hasBannedDependencies(
|
||||
.filter((dependency) =>
|
||||
isConstraintBanningProject(
|
||||
graph.externalNodes[dependency.target],
|
||||
depConstraint
|
||||
depConstraint,
|
||||
imp
|
||||
)
|
||||
)
|
||||
.map((dep) => [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user