Skip to content

Commit b55c597

Browse files
authored
feat(core): support allSourceTags (#768) and wildcards in check-module-boundaries.js (#771)
Co-authored-by: Chris Leigh <chris.leigh@securitas.com>
1 parent 4451e8a commit b55c597

File tree

3 files changed

+245
-23
lines changed

3 files changed

+245
-23
lines changed

packages/core/src/tasks/check-module-boundaries.spec.ts

Lines changed: 175 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,34 @@ const MOCK_BOUNDARIES: ModuleBoundaries = [
2929
onlyDependOnLibsWithTags: ['shared'],
3030
sourceTag: 'shared',
3131
},
32+
{
33+
onlyDependOnLibsWithTags: ['z'],
34+
allSourceTags: ['x', 'only-z'],
35+
},
36+
{
37+
notDependOnLibsWithTags: ['z'],
38+
allSourceTags: ['x', 'not-z'],
39+
},
40+
{
41+
onlyDependOnLibsWithTags: [],
42+
sourceTag: 'no-deps',
43+
},
44+
{
45+
onlyDependOnLibsWithTags: ['*baz*'],
46+
allSourceTags: ['--f*o--'],
47+
},
48+
{
49+
notDependOnLibsWithTags: ['b*z'],
50+
sourceTag: '--f*o--',
51+
},
52+
{
53+
onlyDependOnLibsWithTags: ['/.*baz.*$/'],
54+
allSourceTags: ['/^==f[o]{2}==$/'],
55+
},
56+
{
57+
notDependOnLibsWithTags: ['/^b.*z$/'],
58+
sourceTag: '/^==f[o]{2}==$/',
59+
},
3260
];
3361

3462
describe('load-module-boundaries', () => {
@@ -117,7 +145,7 @@ describe('enforce-module-boundaries', () => {
117145
expect(results).toHaveLength(0);
118146
});
119147

120-
it('should find violations with onlyDependOnLibsWithTags', async () => {
148+
it('should find violations with sourceTag/onlyDependOnLibsWithTags', async () => {
121149
const globResults = ['libs/a/a.csproj'];
122150
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
123151

@@ -141,7 +169,7 @@ describe('enforce-module-boundaries', () => {
141169
expect(results).toHaveLength(1);
142170
});
143171

144-
it('should find violations with notDependOnLibsWithTags', async () => {
172+
it('should find violations with sourceTag/notDependOnLibsWithTags', async () => {
145173
const globResults = ['libs/b/b.csproj'];
146174
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
147175

@@ -165,7 +193,7 @@ describe('enforce-module-boundaries', () => {
165193
expect(results).toHaveLength(1);
166194
});
167195

168-
it('should pass without violations', async () => {
196+
it('should pass without violations with single source rule (sourceTag)', async () => {
169197
const globResults = ['libs/a/a.csproj'];
170198
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
171199

@@ -188,4 +216,148 @@ describe('enforce-module-boundaries', () => {
188216
});
189217
expect(results).toHaveLength(0);
190218
});
219+
220+
it('should find violations with allSourceTags/notDependOnLibsWithTags', async () => {
221+
const globResults = ['libs/x/x.csproj'];
222+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
223+
224+
vol.fromJSON({
225+
'libs/x/x.csproj':
226+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\z\\z.csproj" /></ItemGroup></Project>',
227+
});
228+
229+
const results = await checkModuleBoundariesForProject('x', {
230+
x: {
231+
tags: ['x', 'not-z'],
232+
targets: { z: {} },
233+
root: 'libs/x',
234+
},
235+
z: {
236+
tags: ['z'],
237+
targets: {},
238+
root: 'libs/z',
239+
},
240+
});
241+
expect(results).toHaveLength(1);
242+
});
243+
244+
it('should find violations with allSourceTags/onlyDependOnLibsWithTags', async () => {
245+
const globResults = ['libs/x/x.csproj'];
246+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
247+
248+
vol.fromJSON({
249+
'libs/x/x.csproj':
250+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
251+
});
252+
253+
const results = await checkModuleBoundariesForProject('x', {
254+
x: {
255+
tags: ['x', 'only-z'],
256+
targets: { a: {} },
257+
root: 'libs/x',
258+
},
259+
a: {
260+
tags: ['a'],
261+
targets: {},
262+
root: 'libs/a',
263+
},
264+
});
265+
expect(results).toHaveLength(1);
266+
});
267+
268+
it('should pass without violations with single source rule (allSourceTags)', async () => {
269+
const globResults = ['libs/x/x.csproj'];
270+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
271+
272+
vol.fromJSON({
273+
'libs/x/x.csproj':
274+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\z\\z.csproj" /></ItemGroup></Project>',
275+
});
276+
277+
const results = await checkModuleBoundariesForProject('x', {
278+
x: {
279+
tags: ['x', 'only-z'],
280+
targets: { z: {} },
281+
root: 'libs/x',
282+
},
283+
z: {
284+
tags: ['z'],
285+
targets: {},
286+
root: 'libs/z',
287+
},
288+
});
289+
expect(results).toHaveLength(0);
290+
});
291+
292+
it('should support { onlyDependOnLibsWithTags: [] } - no dependencies', async () => {
293+
const globResults = ['libs/x/x.csproj'];
294+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
295+
296+
vol.fromJSON({
297+
'libs/x/x.csproj':
298+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
299+
});
300+
301+
const results = await checkModuleBoundariesForProject('x', {
302+
x: {
303+
tags: ['no-deps'],
304+
targets: { a: {} },
305+
root: 'libs/x',
306+
},
307+
a: {
308+
tags: ['a'],
309+
targets: {},
310+
root: 'libs/a',
311+
},
312+
});
313+
expect(results).toHaveLength(1);
314+
});
315+
316+
it('should support glob wildcards', async () => {
317+
const globResults = ['libs/x/x.csproj'];
318+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
319+
320+
vol.fromJSON({
321+
'libs/x/x.csproj':
322+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
323+
});
324+
325+
const results = await checkModuleBoundariesForProject('x', {
326+
x: {
327+
tags: ['--foo--'],
328+
targets: { a: {} },
329+
root: 'libs/x',
330+
},
331+
a: {
332+
tags: ['biz'],
333+
targets: {},
334+
root: 'libs/a',
335+
},
336+
});
337+
expect(results).toHaveLength(2);
338+
});
339+
340+
it('should support regexp', async () => {
341+
const globResults = ['libs/x/x.csproj'];
342+
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);
343+
344+
vol.fromJSON({
345+
'libs/x/x.csproj':
346+
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
347+
});
348+
349+
const results = await checkModuleBoundariesForProject('x', {
350+
x: {
351+
tags: ['==foo=='],
352+
targets: { a: {} },
353+
root: 'libs/x',
354+
},
355+
a: {
356+
tags: ['biz'],
357+
targets: {},
358+
root: 'libs/a',
359+
},
360+
});
361+
expect(results).toHaveLength(2);
362+
});
191363
});

packages/core/src/tasks/check-module-boundaries.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { relative } from 'path';
1212
import {
1313
getDependantProjectsForNxProject,
1414
ModuleBoundaries,
15+
ModuleBoundary,
1516
readConfig,
1617
} from '@nx-dotnet/utils';
1718

@@ -24,14 +25,8 @@ export async function checkModuleBoundariesForProject(
2425
if (!tags.length) {
2526
return [];
2627
}
27-
const configuredConstraints = await loadModuleBoundaries(projectRoot);
28-
const relevantConstraints = configuredConstraints.filter(
29-
(x) =>
30-
tags.includes(x.sourceTag) &&
31-
(!x.onlyDependOnLibsWithTags?.includes('*') ||
32-
x.notDependOnLibsWithTags?.length),
33-
);
34-
if (!relevantConstraints.length) {
28+
const constraints = await getProjectConstraints(projectRoot, tags);
29+
if (!constraints.length) {
3530
return [];
3631
}
3732

@@ -42,15 +37,8 @@ export async function checkModuleBoundariesForProject(
4237
(configuration, name, implicit) => {
4338
if (implicit) return;
4439
const dependencyTags = configuration?.tags ?? [];
45-
for (const constraint of relevantConstraints) {
46-
if (
47-
!dependencyTags.some((x) =>
48-
constraint.onlyDependOnLibsWithTags?.includes(x),
49-
) ||
50-
dependencyTags.some((x) =>
51-
constraint.notDependOnLibsWithTags?.includes(x),
52-
)
53-
) {
40+
for (const constraint of constraints) {
41+
if (hasConstraintViolation(constraint, dependencyTags)) {
5442
violations.push(
5543
`${project} cannot depend on ${name}. Project tag ${JSON.stringify(
5644
constraint,
@@ -63,6 +51,30 @@ export async function checkModuleBoundariesForProject(
6351
return violations;
6452
}
6553

54+
async function getProjectConstraints(root: string, tags: string[]) {
55+
const configuredConstraints = await loadModuleBoundaries(root);
56+
return configuredConstraints.filter(
57+
(x) =>
58+
((x.sourceTag && hasMatch(tags, x.sourceTag)) ||
59+
x.allSourceTags?.every((tag) => hasMatch(tags, tag))) &&
60+
(!x.onlyDependOnLibsWithTags?.includes('*') ||
61+
x.notDependOnLibsWithTags?.length),
62+
);
63+
}
64+
65+
function hasConstraintViolation(
66+
constraint: ModuleBoundary,
67+
dependencyTags: string[],
68+
) {
69+
return (
70+
!dependencyTags.some((x) =>
71+
hasMatch(constraint.onlyDependOnLibsWithTags ?? [], x),
72+
) ||
73+
dependencyTags.some((x) =>
74+
hasMatch(constraint.notDependOnLibsWithTags ?? [], x),
75+
)
76+
);
77+
}
6678
/**
6779
* Loads module boundaries from eslintrc or .nx-dotnet.rc.json
6880
* @param root Which file should be used when pulling from eslint
@@ -120,6 +132,41 @@ function findProjectGivenRoot(
120132
}
121133
}
122134

135+
const regexMap = new Map<string, RegExp>();
136+
137+
function hasMatch(input: string[], pattern: string): boolean {
138+
if (pattern === '*') return true;
139+
140+
// if the pattern is a regex, check if any of the input strings match the regex
141+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
142+
let regex = regexMap.get(pattern);
143+
if (!regex) {
144+
regex = new RegExp(pattern.substring(1, pattern.length - 1));
145+
regexMap.set(pattern, regex);
146+
}
147+
return input.some((t) => regex?.test(t));
148+
}
149+
150+
// if the pattern is a glob, check if any of the input strings match the glob prefix
151+
if (pattern.includes('*')) {
152+
const regex = mapGlobToRegExp(pattern);
153+
return input.some((t) => regex.test(t));
154+
}
155+
156+
return input.indexOf(pattern) > -1;
157+
}
158+
159+
/**
160+
* Maps import with wildcards to regex pattern
161+
* @param importDefinition
162+
* @returns
163+
*/
164+
function mapGlobToRegExp(importDefinition: string): RegExp {
165+
// we replace all instances of `*`, `**..*` and `.*` with `.*`
166+
const mappedWildcards = importDefinition.split(/(?:\.\*)|\*+/).join('.*');
167+
return new RegExp(`^${new RegExp(mappedWildcards).source}$`);
168+
}
169+
123170
async function main() {
124171
const parser = await import('yargs-parser');
125172
const { project, projectRoot } = parser(process.argv.slice(2), {
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
export type ModuleBoundaries = {
2-
sourceTag: '*' | string;
1+
export type ModuleBoundary = {
2+
sourceTag?: '*' | string;
3+
allSourceTags?: string[];
34
onlyDependOnLibsWithTags?: string[];
45
notDependOnLibsWithTags?: string[];
5-
}[];
6+
};
7+
8+
export type ModuleBoundaries = ModuleBoundary[];

0 commit comments

Comments
 (0)