Skip to content

Commit

Permalink
feat(core): ability to load module boundaries from nx-dotnet config
Browse files Browse the repository at this point in the history
Signed-off-by: AgentEnder <craigorycoppola@gmail.com>
  • Loading branch information
AgentEnder committed Aug 20, 2021
1 parent 3fb8ba4 commit 2618b5d
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 16 deletions.
28 changes: 28 additions & 0 deletions docs/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ Run my-api locally
npx nx serve my-api
```

## nrwl/nx/enforce-module-boundaries support

Nrwl publishes an eslint rule for enforcing module boundaries based on tags in a library. We recently added similar support to nx-dotnet.

To avoid duplicating the rules configuration, if your workspace already has it, nx-dotnet can read the dependency constraints from your workspace's eslint files. It does this by looking at what is configured for typescript files.

If your workspace does not currently contain eslint, do not worry! You do not have to install eslint just for its configuration. The same dependency constraints can be placed inside of your .nx-dotnet.rc.json file at workspace root. This should look something like below:

```json
{
"moduleBoundaries": [
{
"onlyDependOnLibsWithTags": ["a", "shared"],
"sourceTag": "a"
},
{
"onlyDependOnLibsWithTags": ["b", "shared"],
"sourceTag": "b"
},
{
"onlyDependOnLibsWithTags": ["shared"],
"sourceTag": "shared"
}
],
"nugetPackages": {}
}
```

# API Reference

## Generators
Expand Down
28 changes: 28 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,31 @@ Run my-api locally
```shell
npx nx serve my-api
```

## nrwl/nx/enforce-module-boundaries support

Nrwl publishes an eslint rule for enforcing module boundaries based on tags in a library. We recently added similar support to nx-dotnet.

To avoid duplicating the rules configuration, if your workspace already has it, nx-dotnet can read the dependency constraints from your workspace's eslint files. It does this by looking at what is configured for typescript files.

If your workspace does not currently contain eslint, do not worry! You do not have to install eslint just for its configuration. The same dependency constraints can be placed inside of your .nx-dotnet.rc.json file at workspace root. This should look something like below:

```json
{
"moduleBoundaries": [
{
"onlyDependOnLibsWithTags": ["a", "shared"],
"sourceTag": "a"
},
{
"onlyDependOnLibsWithTags": ["b", "shared"],
"sourceTag": "b"
},
{
"onlyDependOnLibsWithTags": ["shared"],
"sourceTag": "shared"
}
],
"nugetPackages": {}
}
```
9 changes: 6 additions & 3 deletions packages/core/src/generators/utils/generate-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
formatFiles,
getWorkspaceLayout,
names,
normalizePath,
NxJsonProjectConfiguration,
ProjectConfiguration,
ProjectType,
Expand Down Expand Up @@ -197,9 +198,11 @@ export function addPrebuildMsbuildTask(
options: { projectRoot: string; name: string },
xml: XmlDocument,
) {
const scriptPath = relative(
options.projectRoot,
require.resolve('@nx-dotnet/core/src/tasks/check-module-boundaries'),
const scriptPath = normalizePath(
relative(
options.projectRoot,
require.resolve('@nx-dotnet/core/src/tasks/check-module-boundaries'),
),
);

const fragment = new XmlDocument(`
Expand Down
91 changes: 91 additions & 0 deletions packages/core/src/tasks/check-module-boundaries.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Tree, writeJson } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
CONFIG_FILE_PATH,
ModuleBoundaries,
NxDotnetConfig,
} from '@nx-dotnet/utils';

import {
checkModuleBoundariesForProject,
loadModuleBoundaries,
} from './check-module-boundaries';
import * as checkModule from './check-module-boundaries';
import * as ESLintNamespace from 'eslint';

const MOCK_BOUNDARIES: ModuleBoundaries = [
{
onlyDependOnLibsWithTags: ['a', 'shared'],
sourceTag: 'a',
},
{
onlyDependOnLibsWithTags: ['b', 'shared'],
sourceTag: 'b',
},
{
onlyDependOnLibsWithTags: ['shared'],
sourceTag: 'shared',
},
];

describe('load-module-boundaries', () => {
let appTree: Tree;

beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
});

afterEach(() => {
jest.resetAllMocks();
});

it('should not load eslint if boundaries in config', async () => {
const eslintConstructorSpy = jest.spyOn(ESLintNamespace, 'ESLint');
writeJson<NxDotnetConfig>(appTree, CONFIG_FILE_PATH, {
moduleBoundaries: MOCK_BOUNDARIES,
nugetPackages: {},
});
const boundaries = await loadModuleBoundaries('', appTree);
expect(eslintConstructorSpy).not.toHaveBeenCalled();
expect(boundaries).toEqual(MOCK_BOUNDARIES);
});

it('should load from eslint if boundaries not in config', async () => {
const eslintConfigSpy = jest
.spyOn(ESLintNamespace, 'ESLint')
.mockReturnValue({
calculateConfigForFile: jest.fn().mockResolvedValue({
rules: {
'@nrwl/nx/enforce-module-boundaries': [
1,
{ depConstraints: MOCK_BOUNDARIES },
],
},
}),
} as unknown as ESLintNamespace.ESLint);
writeJson<NxDotnetConfig>(appTree, CONFIG_FILE_PATH, {
nugetPackages: {},
});
const boundaries = await loadModuleBoundaries('', appTree);
expect(eslintConfigSpy).toHaveBeenCalledTimes(1);
expect(boundaries).toEqual(MOCK_BOUNDARIES);
});
});

describe('enforce-module-boundaries', () => {
it('should exit early if no tags on project', async () => {
const spy = jest.spyOn(checkModule, 'loadModuleBoundaries');
const results = await checkModuleBoundariesForProject('a', {
version: 2,
projects: {
a: {
tags: [],
targets: {},
root: '',
},
},
});
expect(spy).not.toHaveBeenCalled();
expect(results).toHaveLength(0);
});
});
39 changes: 29 additions & 10 deletions packages/core/src/tasks/check-module-boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import {

import { ESLint } from 'eslint';

import { getDependantProjectsForNxProject } from '@nx-dotnet/utils';
import {
getDependantProjectsForNxProject,
ModuleBoundaries,
readConfig,
} from '@nx-dotnet/utils';
import {
NxJsonConfiguration,
NxJsonProjectConfiguration,
readJsonFile,
Tree,
} from '@nrwl/devkit';

type ExtendedWorkspaceJson = WorkspaceJsonConfiguration & {
Expand All @@ -28,15 +33,7 @@ export async function checkModuleBoundariesForProject(
//
return [];
}

const { rules } = await new ESLint().calculateConfigForFile(
`${projectRoot}/non-existant.ts`,
);
const [, moduleBoundaryConfig] = rules['@nrwl/nx/enforce-module-boundaries'];
const configuredConstraints: {
sourceTag: '*' | string;
onlyDependOnLibsWithTags: string[];
}[] = moduleBoundaryConfig?.depConstraints ?? [];
const configuredConstraints = await loadModuleBoundaries(projectRoot);
const relevantConstraints = configuredConstraints.filter(
(x) =>
tags.includes(x.sourceTag) && !x.onlyDependOnLibsWithTags.includes('*'),
Expand Down Expand Up @@ -67,6 +64,28 @@ export async function checkModuleBoundariesForProject(
return violations;
}

/**
* Loads module boundaries from eslintrc or .nx-dotnet.rc.json
* @param root Which file should be used when pulling from eslint
* @returns List of module boundaries
*/
export async function loadModuleBoundaries(
root: string,
host?: Tree,
): Promise<ModuleBoundaries> {
const configured = readConfig(host).moduleBoundaries;
if (!configured) {
const result = await new ESLint().calculateConfigForFile(
`${root}/non-existant.ts`,
);
const [, moduleBoundaryConfig] =
result.rules['@nrwl/nx/enforce-module-boundaries'];
return moduleBoundaryConfig?.depConstraints ?? [];
} else {
return configured;
}
}

async function main() {
const parser = await import('yargs-parser');
const { project } = parser(process.argv.slice(2), {
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './cmd-line-parameter';
export * from './nx-dotnet-config.interface';
export * from './nx';
3 changes: 3 additions & 0 deletions packages/utils/src/lib/models/nx-dotnet-config.interface.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { ModuleBoundaries } from './nx';

export interface NxDotnetConfig {
/**
* Map of package -> version, used for Single Version Principle.
*/
nugetPackages: {
[key: string]: string | undefined;
};
moduleBoundaries?: ModuleBoundaries;
}
4 changes: 4 additions & 0 deletions packages/utils/src/lib/models/nx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ModuleBoundaries = {
sourceTag: '*' | string;
onlyDependOnLibsWithTags: string[];
}[];
8 changes: 6 additions & 2 deletions packages/utils/src/lib/utility-functions/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { readJson, Tree, writeJson } from '@nrwl/devkit';
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import { readJsonSync } from 'fs-extra';

import { CONFIG_FILE_PATH } from '../constants';
import { NxDotnetConfig } from '../models';

export function readConfig(host: Tree): NxDotnetConfig {
return readJson(host, CONFIG_FILE_PATH);
export function readConfig(host?: Tree): NxDotnetConfig {
return host
? readJson(host, CONFIG_FILE_PATH)
: readJsonSync(`${appRootPath}/${CONFIG_FILE_PATH}`);
}

export function updateConfig(host: Tree, value: NxDotnetConfig) {
Expand Down
2 changes: 1 addition & 1 deletion tools/scripts/hooks/documentation.check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function getChangedFiles(base = 'master', directory = '.'): string[] {

console.log(`📖 Checking for documentation changes`);
execSync('nx workspace-generator generate-docs');
const changes = getChangedFiles('master', 'docs');
const changes = getChangedFiles('HEAD', 'docs');
if (changes.length) {
console.log(`❌ Found changes in docs files`);
changes.forEach((file) => {
Expand Down

0 comments on commit 2618b5d

Please sign in to comment.