Skip to content

Commit

Permalink
feat(core): support allSourceTags (#768) and wildcards in check-modul…
Browse files Browse the repository at this point in the history
…e-boundaries.js (#771)

Co-authored-by: Chris Leigh <chris.leigh@securitas.com>
  • Loading branch information
Tungsten78 and Tungsten78 authored Oct 11, 2023
1 parent 4451e8a commit b55c597
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 23 deletions.
178 changes: 175 additions & 3 deletions packages/core/src/tasks/check-module-boundaries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ const MOCK_BOUNDARIES: ModuleBoundaries = [
onlyDependOnLibsWithTags: ['shared'],
sourceTag: 'shared',
},
{
onlyDependOnLibsWithTags: ['z'],
allSourceTags: ['x', 'only-z'],
},
{
notDependOnLibsWithTags: ['z'],
allSourceTags: ['x', 'not-z'],
},
{
onlyDependOnLibsWithTags: [],
sourceTag: 'no-deps',
},
{
onlyDependOnLibsWithTags: ['*baz*'],
allSourceTags: ['--f*o--'],
},
{
notDependOnLibsWithTags: ['b*z'],
sourceTag: '--f*o--',
},
{
onlyDependOnLibsWithTags: ['/.*baz.*$/'],
allSourceTags: ['/^==f[o]{2}==$/'],
},
{
notDependOnLibsWithTags: ['/^b.*z$/'],
sourceTag: '/^==f[o]{2}==$/',
},
];

describe('load-module-boundaries', () => {
Expand Down Expand Up @@ -117,7 +145,7 @@ describe('enforce-module-boundaries', () => {
expect(results).toHaveLength(0);
});

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

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

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

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

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

Expand All @@ -188,4 +216,148 @@ describe('enforce-module-boundaries', () => {
});
expect(results).toHaveLength(0);
});

it('should find violations with allSourceTags/notDependOnLibsWithTags', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\z\\z.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['x', 'not-z'],
targets: { z: {} },
root: 'libs/x',
},
z: {
tags: ['z'],
targets: {},
root: 'libs/z',
},
});
expect(results).toHaveLength(1);
});

it('should find violations with allSourceTags/onlyDependOnLibsWithTags', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['x', 'only-z'],
targets: { a: {} },
root: 'libs/x',
},
a: {
tags: ['a'],
targets: {},
root: 'libs/a',
},
});
expect(results).toHaveLength(1);
});

it('should pass without violations with single source rule (allSourceTags)', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\z\\z.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['x', 'only-z'],
targets: { z: {} },
root: 'libs/x',
},
z: {
tags: ['z'],
targets: {},
root: 'libs/z',
},
});
expect(results).toHaveLength(0);
});

it('should support { onlyDependOnLibsWithTags: [] } - no dependencies', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['no-deps'],
targets: { a: {} },
root: 'libs/x',
},
a: {
tags: ['a'],
targets: {},
root: 'libs/a',
},
});
expect(results).toHaveLength(1);
});

it('should support glob wildcards', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['--foo--'],
targets: { a: {} },
root: 'libs/x',
},
a: {
tags: ['biz'],
targets: {},
root: 'libs/a',
},
});
expect(results).toHaveLength(2);
});

it('should support regexp', async () => {
const globResults = ['libs/x/x.csproj'];
jest.spyOn(fastGlob, 'sync').mockImplementation(() => globResults);

vol.fromJSON({
'libs/x/x.csproj':
'<Project Sdk="Microsoft.NET.Sdk.Web"><ItemGroup><ProjectReference Include="..\\..\\libs\\a\\a.csproj" /></ItemGroup></Project>',
});

const results = await checkModuleBoundariesForProject('x', {
x: {
tags: ['==foo=='],
targets: { a: {} },
root: 'libs/x',
},
a: {
tags: ['biz'],
targets: {},
root: 'libs/a',
},
});
expect(results).toHaveLength(2);
});
});
81 changes: 64 additions & 17 deletions packages/core/src/tasks/check-module-boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { relative } from 'path';
import {
getDependantProjectsForNxProject,
ModuleBoundaries,
ModuleBoundary,
readConfig,
} from '@nx-dotnet/utils';

Expand All @@ -24,14 +25,8 @@ export async function checkModuleBoundariesForProject(
if (!tags.length) {
return [];
}
const configuredConstraints = await loadModuleBoundaries(projectRoot);
const relevantConstraints = configuredConstraints.filter(
(x) =>
tags.includes(x.sourceTag) &&
(!x.onlyDependOnLibsWithTags?.includes('*') ||
x.notDependOnLibsWithTags?.length),
);
if (!relevantConstraints.length) {
const constraints = await getProjectConstraints(projectRoot, tags);
if (!constraints.length) {
return [];
}

Expand All @@ -42,15 +37,8 @@ export async function checkModuleBoundariesForProject(
(configuration, name, implicit) => {
if (implicit) return;
const dependencyTags = configuration?.tags ?? [];
for (const constraint of relevantConstraints) {
if (
!dependencyTags.some((x) =>
constraint.onlyDependOnLibsWithTags?.includes(x),
) ||
dependencyTags.some((x) =>
constraint.notDependOnLibsWithTags?.includes(x),
)
) {
for (const constraint of constraints) {
if (hasConstraintViolation(constraint, dependencyTags)) {
violations.push(
`${project} cannot depend on ${name}. Project tag ${JSON.stringify(
constraint,
Expand All @@ -63,6 +51,30 @@ export async function checkModuleBoundariesForProject(
return violations;
}

async function getProjectConstraints(root: string, tags: string[]) {
const configuredConstraints = await loadModuleBoundaries(root);
return configuredConstraints.filter(
(x) =>
((x.sourceTag && hasMatch(tags, x.sourceTag)) ||
x.allSourceTags?.every((tag) => hasMatch(tags, tag))) &&
(!x.onlyDependOnLibsWithTags?.includes('*') ||
x.notDependOnLibsWithTags?.length),
);
}

function hasConstraintViolation(
constraint: ModuleBoundary,
dependencyTags: string[],
) {
return (
!dependencyTags.some((x) =>
hasMatch(constraint.onlyDependOnLibsWithTags ?? [], x),
) ||
dependencyTags.some((x) =>
hasMatch(constraint.notDependOnLibsWithTags ?? [], x),
)
);
}
/**
* Loads module boundaries from eslintrc or .nx-dotnet.rc.json
* @param root Which file should be used when pulling from eslint
Expand Down Expand Up @@ -120,6 +132,41 @@ function findProjectGivenRoot(
}
}

const regexMap = new Map<string, RegExp>();

function hasMatch(input: string[], pattern: string): boolean {
if (pattern === '*') return true;

// if the pattern is a regex, check if any of the input strings match the regex
if (pattern.startsWith('/') && pattern.endsWith('/')) {
let regex = regexMap.get(pattern);
if (!regex) {
regex = new RegExp(pattern.substring(1, pattern.length - 1));
regexMap.set(pattern, regex);
}
return input.some((t) => regex?.test(t));
}

// if the pattern is a glob, check if any of the input strings match the glob prefix
if (pattern.includes('*')) {
const regex = mapGlobToRegExp(pattern);
return input.some((t) => regex.test(t));
}

return input.indexOf(pattern) > -1;
}

/**
* Maps import with wildcards to regex pattern
* @param importDefinition
* @returns
*/
function mapGlobToRegExp(importDefinition: string): RegExp {
// we replace all instances of `*`, `**..*` and `.*` with `.*`
const mappedWildcards = importDefinition.split(/(?:\.\*)|\*+/).join('.*');
return new RegExp(`^${new RegExp(mappedWildcards).source}$`);
}

async function main() {
const parser = await import('yargs-parser');
const { project, projectRoot } = parser(process.argv.slice(2), {
Expand Down
9 changes: 6 additions & 3 deletions packages/utils/src/lib/models/nx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type ModuleBoundaries = {
sourceTag: '*' | string;
export type ModuleBoundary = {
sourceTag?: '*' | string;
allSourceTags?: string[];
onlyDependOnLibsWithTags?: string[];
notDependOnLibsWithTags?: string[];
}[];
};

export type ModuleBoundaries = ModuleBoundary[];

0 comments on commit b55c597

Please sign in to comment.