Skip to content

Commit

Permalink
feat(linter): allow globs in onlyDependOnLibsWithTags eslint-plugin c…
Browse files Browse the repository at this point in the history
…onfiguration option (#17530)

Co-authored-by: Miroslav Jonas <[email protected]>
  • Loading branch information
ericyd and meeroslav authored Jun 30, 2023
1 parent 9bb60a7 commit 576262b
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 12 deletions.
57 changes: 49 additions & 8 deletions docs/shared/core-features/enforce-project-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ First, use your project configuration (in `project.json` or `package.json`) to a
{% /tab %}
{% /tabs %}

Next you should update your root lint configuration:
Next, you should update your root lint configuration:

- If you are using **ESLint** you should look for an existing rule entry in your root `.eslintrc.json` called `"@nx/enforce-module-boundaries"` and you should update the `"depConstraints"`:

Expand Down Expand Up @@ -129,20 +129,61 @@ Next you should update your root lint configuration:
}
```

With these constraints in place, `scope:client` projects can only depend on other `scope:client` projects or on `scope:shared` projects. And `scope:admin` projects can only depend on other `scope:admin` projects or on `scope:shared` projects. So `scope:client` and `scope:admin` cannot depend on each other.
With these constraints in place, `scope:client` projects can only depend on projects with `scope:client` or `scope:shared`. And `scope:admin` projects can only depend on projects with `scope:admin` or `scope:shared`. So `scope:client` and `scope:admin` cannot depend on each other.

Projects without any tags cannot depend on any other projects. If you add the following, projects without any tags will be able to depend on any other project.
Projects without any tags cannot depend on any other projects. If you try to violate the constraints, you will get an error when linting:

```json
```shell
A project tagged with "scope:admin" can only depend on projects
tagged with "scoped:shared" or "scope:admin".
```

The exception to this rule is by explicitly allowing all tags (see below).

### Tag formats

- `*`: allow all tags

Example: projects with any tags (including untagged) can depend on any other project.

```jsonc
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
```

If you try to violate the constraints, you will get an error when linting:
- `string`: allow exact tags

```shell
A project tagged with "scope:admin" can only depend on projects
tagged with "scoped:shared" or "scope:admin".
Example: projects tagged with `scope:client` can only depend on projects tagged with `scope:util`.

```jsonc
{
"sourceTag": "scope:client",
"onlyDependOnLibsWithTags": ["scope:util"]
}
```

- `regex`: allow tags matching the regular expression

Example: projects tagged with `scope:client` can depend on projects with a tag matching the regular expression `/^scope.*/`. In this case, the `scope:util`, `scope:client`, etc. are all allowed tags for dependencies.

```json
{
"sourceTag": "scope:client",
"onlyDependOnLibsWithTags": ["/^scope.*/"]
}
```

- `glob`: allow tags matching the glob

Example: projects with a tag starting with `scope:` can depend on projects with a tag that starts with `scope:*`. In this case `scope:a`, `scope:b`, etc are all allowed tags for dependencies.

```json
{
"sourceTag": "scope:*",
"onlyDependOnLibsWithTags": ["scope:*"]
}
```

Globbing supports only the basic use of `*`. For more complex scenarios use the `regex` above.
26 changes: 26 additions & 0 deletions packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,32 @@ Violation detected in:
expect(failures.length).toEqual(0);
});

it('should support globs', () => {
const failures = runRule(
{
depConstraints: [
{
sourceTag: 'p*',
onlyDependOnLibsWithTags: ['domain*'],
},
],
},
`${process.cwd()}/proj/libs/public/src/index.ts`,
`
import '@mycompany/impl-domain2';
import('@mycompany/impl-domain2');
import '@mycompany/impl-both-domains';
import('@mycompany/impl-both-domains');
import '@mycompany/impl';
import('@mycompany/impl');
`,
graph,
fileMap
);

expect(failures.length).toEqual(0);
});

it('should report errors for combo source tags', () => {
const failures = runRule(
{
Expand Down
49 changes: 49 additions & 0 deletions packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
findTransitiveExternalDependencies,
hasBannedDependencies,
hasBannedImport,
hasNoneOfTheseTags,
isAngularSecondaryEntrypoint,
isTerminalRun,
} from './runtime-lint-utils';
Expand Down Expand Up @@ -80,6 +81,28 @@ describe('findConstraintsFor', () => {
})
).toEqual([{ sourceTag: '/a|b/', onlyDependOnLibsWithTags: ['c'] }]);
});

it('should find constraints matching glob', () => {
const constriants: DepConstraint[] = [
{ sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] },
{ sourceTag: 'b:*', onlyDependOnLibsWithTags: ['c:*'] },
{ sourceTag: 'c:*', onlyDependOnLibsWithTags: ['a:*'] },
];
expect(
findConstraintsFor(constriants, {
type: 'lib',
name: 'someLib',
data: { root: '.', tags: ['a:a'] },
})
).toEqual([{ sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] }]);
expect(
findConstraintsFor(constriants, {
type: 'lib',
name: 'someLib',
data: { root: '.', tags: ['a:abc'] },
})
).toEqual([{ sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] }]);
});
});

describe('hasBannedImport', () => {
Expand Down Expand Up @@ -474,3 +497,29 @@ describe('isAngularSecondaryEntrypoint', () => {
).toBe(false);
});
});

describe('hasNoneOfTheseTags', () => {
const source: ProjectGraphProjectNode = {
type: 'lib',
name: 'aLib',
data: {
tags: ['abc'],
} as any,
};

it.each([
[true, ['a']],
[true, ['b']],
[true, ['c']],
[true, ['az*']],
[true, ['/[A-Z]+/']],
[false, ['ab*']],
[false, ['*']],
[false, ['/[a-z]*/']],
])(
'should return %s when project has tags ["abc"] and requested tags are %s',
(expected, tags) => {
expect(hasNoneOfTheseTags(source, tags)).toBe(expected);
}
);
});
13 changes: 9 additions & 4 deletions packages/eslint-plugin/src/utils/runtime-lint-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ function hasTag(proj: ProjectGraphProjectNode, tag: string): boolean {
return (proj.data.tags || []).some((t) => regex.test(t));
}

// if the tag is a glob, check if the project matches the glob prefix
if (tag.includes('*')) {
const regex = mapGlobToRegExp(tag);
return (proj.data.tags || []).some((t) => regex.test(t));
}

return (proj.data.tags || []).indexOf(tag) > -1;
}

Expand Down Expand Up @@ -236,16 +242,15 @@ function isConstraintBanningProject(
/* Check if import is banned... */
if (
bannedExternalImports?.some((importDefinition) =>
parseImportWildcards(importDefinition).test(packageName)
mapGlobToRegExp(importDefinition).test(packageName)
)
) {
return true;
}

/* ... then check if there is a whitelist and if there is a match in the whitelist. */
return allowedExternalImports?.every(
(importDefinition) =>
!parseImportWildcards(importDefinition).test(packageName)
(importDefinition) => !mapGlobToRegExp(importDefinition).test(packageName)
);
}

Expand Down Expand Up @@ -375,7 +380,7 @@ function packageExistsInPackageJson(
* @param importDefinition
* @returns
*/
function parseImportWildcards(importDefinition: string): RegExp {
function mapGlobToRegExp(importDefinition: string): RegExp {
// we replace all instances of `*`, `**..*` and `.*` with `.*`
const mappedWildcards = importDefinition.split(/(?:\.\*)|\*+/).join('.*');
return new RegExp(`^${new RegExp(mappedWildcards).source}$`);
Expand Down

1 comment on commit 576262b

@vercel
Copy link

@vercel vercel bot commented on 576262b Jun 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-five.vercel.app
nx.dev
nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app

Please sign in to comment.