From 2618b5d5722035f3b20916ea4baf92d04e417ede Mon Sep 17 00:00:00 2001 From: AgentEnder Date: Thu, 19 Aug 2021 20:08:29 -0500 Subject: [PATCH] feat(core): ability to load module boundaries from nx-dotnet config Signed-off-by: AgentEnder --- docs/core/index.md | 28 ++++++ packages/core/README.md | 28 ++++++ .../src/generators/utils/generate-project.ts | 9 +- .../src/tasks/check-module-boundaries.spec.ts | 91 +++++++++++++++++++ .../core/src/tasks/check-module-boundaries.ts | 39 ++++++-- packages/utils/src/lib/models/index.ts | 1 + .../lib/models/nx-dotnet-config.interface.ts | 3 + packages/utils/src/lib/models/nx.ts | 4 + .../utils/src/lib/utility-functions/config.ts | 8 +- tools/scripts/hooks/documentation.check.ts | 2 +- 10 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/tasks/check-module-boundaries.spec.ts create mode 100644 packages/utils/src/lib/models/nx.ts diff --git a/docs/core/index.md b/docs/core/index.md index 346e7936..a4d03ebb 100644 --- a/docs/core/index.md +++ b/docs/core/index.md @@ -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 diff --git a/packages/core/README.md b/packages/core/README.md index 8236acad..4afe4090 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -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": {} +} +``` diff --git a/packages/core/src/generators/utils/generate-project.ts b/packages/core/src/generators/utils/generate-project.ts index f7e07725..bc6b0951 100644 --- a/packages/core/src/generators/utils/generate-project.ts +++ b/packages/core/src/generators/utils/generate-project.ts @@ -3,6 +3,7 @@ import { formatFiles, getWorkspaceLayout, names, + normalizePath, NxJsonProjectConfiguration, ProjectConfiguration, ProjectType, @@ -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(` diff --git a/packages/core/src/tasks/check-module-boundaries.spec.ts b/packages/core/src/tasks/check-module-boundaries.spec.ts new file mode 100644 index 00000000..0a3538ae --- /dev/null +++ b/packages/core/src/tasks/check-module-boundaries.spec.ts @@ -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(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(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); + }); +}); diff --git a/packages/core/src/tasks/check-module-boundaries.ts b/packages/core/src/tasks/check-module-boundaries.ts index 4ef51161..f0ed3a78 100644 --- a/packages/core/src/tasks/check-module-boundaries.ts +++ b/packages/core/src/tasks/check-module-boundaries.ts @@ -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 & { @@ -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('*'), @@ -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 { + 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), { diff --git a/packages/utils/src/lib/models/index.ts b/packages/utils/src/lib/models/index.ts index a4cab70b..96c7ed70 100644 --- a/packages/utils/src/lib/models/index.ts +++ b/packages/utils/src/lib/models/index.ts @@ -1,2 +1,3 @@ export * from './cmd-line-parameter'; export * from './nx-dotnet-config.interface'; +export * from './nx'; diff --git a/packages/utils/src/lib/models/nx-dotnet-config.interface.ts b/packages/utils/src/lib/models/nx-dotnet-config.interface.ts index 669d2915..9fb87415 100644 --- a/packages/utils/src/lib/models/nx-dotnet-config.interface.ts +++ b/packages/utils/src/lib/models/nx-dotnet-config.interface.ts @@ -1,3 +1,5 @@ +import { ModuleBoundaries } from './nx'; + export interface NxDotnetConfig { /** * Map of package -> version, used for Single Version Principle. @@ -5,4 +7,5 @@ export interface NxDotnetConfig { nugetPackages: { [key: string]: string | undefined; }; + moduleBoundaries?: ModuleBoundaries; } diff --git a/packages/utils/src/lib/models/nx.ts b/packages/utils/src/lib/models/nx.ts new file mode 100644 index 00000000..47dbb95c --- /dev/null +++ b/packages/utils/src/lib/models/nx.ts @@ -0,0 +1,4 @@ +export type ModuleBoundaries = { + sourceTag: '*' | string; + onlyDependOnLibsWithTags: string[]; +}[]; diff --git a/packages/utils/src/lib/utility-functions/config.ts b/packages/utils/src/lib/utility-functions/config.ts index 99aba37f..1adb8d50 100644 --- a/packages/utils/src/lib/utility-functions/config.ts +++ b/packages/utils/src/lib/utility-functions/config.ts @@ -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) { diff --git a/tools/scripts/hooks/documentation.check.ts b/tools/scripts/hooks/documentation.check.ts index bb4ad086..5e9d1f5d 100644 --- a/tools/scripts/hooks/documentation.check.ts +++ b/tools/scripts/hooks/documentation.check.ts @@ -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) => {