Skip to content

Commit 3fc92fd

Browse files
committed
feat(core): #81 support for nx-enforce-module-boundaries
1 parent f652fc4 commit 3fc92fd

File tree

10 files changed

+265
-77
lines changed

10 files changed

+265
-77
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,16 @@
4545
"@types/xmldoc": "^1.1.5",
4646
"chokidar": "^3.5.1",
4747
"clsx": "^1.1.1",
48+
"eslint": "^7.22.0",
4849
"glob": "^7.1.6",
4950
"inquirer": "^8.0.0",
5051
"prism-react-renderer": "^1.2.1",
5152
"react": "^16.8.4",
5253
"react-dom": "^16.8.4",
5354
"rimraf": "^3.0.2",
5455
"rxjs": "^7.0.1",
55-
"xmldoc": "^1.1.2"
56+
"xmldoc": "^1.1.2",
57+
"yargs-parser": "^20.2.9"
5658
},
5759
"devDependencies": {
5860
"@commitlint/cli": "^12.1.1",
@@ -83,10 +85,10 @@
8385
"@types/react": "17",
8486
"@types/rimraf": "^3.0.0",
8587
"@types/tmp": "^0.2.0",
88+
"@types/yargs-parser": "^20.2.1",
8689
"@typescript-eslint/eslint-plugin": "4.19.0",
8790
"@typescript-eslint/parser": "4.19.0",
8891
"dotenv": "8.2.0",
89-
"eslint": "7.22.0",
9092
"eslint-config-prettier": "8.1.0",
9193
"fs-extra": "^10.0.0",
9294
"husky": "^6.0.0",

packages/core/migrations.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"description": "Adds lint target to all existing dotnet projects",
66
"cli": "nx",
77
"implementation": "./src/migrations/add-lint-target/add-lint-target"
8+
},
9+
"1.2.0-add-module-boundaries-check": {
10+
"version": "1.2.0",
11+
"description": "1.2.0-add-module-boundaries-check",
12+
"cli": "nx",
13+
"implementation": "./src/migrations/1.2.0/add-module-boundaries-check/migrate"
814
}
915
}
1016
}

packages/core/src/generators/utils/generate-project.ts

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
Tree,
1111
} from '@nrwl/devkit';
1212

13+
// Files generated via `dotnet` are not available in the virtual fs
1314
import { readFileSync, writeFileSync } from 'fs';
15+
1416
import { dirname, relative } from 'path';
15-
import { XmlDocument, XmlNode, XmlTextNode } from 'xmldoc';
17+
import { XmlDocument } from 'xmldoc';
1618

1719
import { DotNetClient, dotnetNewOptions } from '@nx-dotnet/dotnet';
1820
import {
@@ -84,41 +86,18 @@ export function normalizeOptions(
8486
};
8587
}
8688

87-
export function SetOutputPath(
89+
export async function manipulateXmlProjectFile(
8890
host: Tree,
89-
projectRootPath: string,
90-
projectFilePath: string,
91-
): void {
91+
options: NormalizedSchema,
92+
): Promise<void> {
93+
const projectFilePath = await findProjectFileInPath(options.projectRoot);
94+
9295
const xml: XmlDocument = new XmlDocument(
9396
readFileSync(projectFilePath).toString(),
9497
);
9598

96-
let outputPath = `${relative(
97-
dirname(projectFilePath),
98-
process.cwd(),
99-
)}/dist/${projectRootPath}`;
100-
outputPath = outputPath.replace('\\', '/'); // Forward slash works on windows, backslash does not work on mac/linux
101-
102-
const textNode: Partial<XmlTextNode> = {
103-
text: outputPath,
104-
type: 'text',
105-
};
106-
textNode.toString = () => textNode.text ?? '';
107-
textNode.toStringWithIndent = () => textNode.text ?? '';
108-
109-
const el: Partial<XmlNode> = {
110-
name: 'OutputPath',
111-
attr: {},
112-
type: 'element',
113-
children: [textNode as XmlTextNode],
114-
firstChild: null,
115-
lastChild: null,
116-
};
117-
118-
el.toStringWithIndent = xml.toStringWithIndent.bind(el);
119-
el.toString = xml.toString.bind(el);
120-
121-
xml.childNamed('PropertyGroup')?.children.push(el as XmlNode);
99+
setOutputPath(xml, options.projectRoot, projectFilePath);
100+
addPrebuildMsbuildTask(host, options, xml);
122101

123102
writeFileSync(projectFilePath, xml.toString());
124103
}
@@ -180,12 +159,10 @@ export async function GenerateProject(
180159

181160
if (options['testTemplate'] !== 'none') {
182161
await GenerateTestProject(host, normalizedOptions, dotnetClient);
183-
} else if (!options.skipOutputPathManipulation) {
184-
SetOutputPath(
185-
host,
186-
normalizedOptions.projectRoot,
187-
await findProjectFileInPath(normalizedOptions.projectRoot),
188-
);
162+
}
163+
164+
if (!options.skipOutputPathManipulation && !isDryRun()) {
165+
await manipulateXmlProjectFile(host, normalizedOptions);
189166
}
190167

191168
await formatFiles(host);
@@ -197,3 +174,37 @@ export function addDryRunParameter(parameters: dotnetNewOptions): void {
197174
value: true,
198175
});
199176
}
177+
178+
export function setOutputPath(
179+
xml: XmlDocument,
180+
projectRootPath: string,
181+
projectFilePath: string,
182+
) {
183+
let outputPath = `${relative(
184+
dirname(projectFilePath),
185+
process.cwd(),
186+
)}/dist/${projectRootPath}`;
187+
outputPath = outputPath.replace('\\', '/'); // Forward slash works on windows, backslash does not work on mac/linux
188+
189+
const fragment = new XmlDocument(`<OutputPath>${outputPath}</OutputPath>`);
190+
xml.childNamed('PropertyGroup')?.children.push(fragment);
191+
}
192+
193+
export function addPrebuildMsbuildTask(
194+
host: Tree,
195+
options: { projectRoot: string; name: string },
196+
xml: XmlDocument,
197+
) {
198+
const scriptPath = relative(
199+
options.projectRoot,
200+
require.resolve('@nx-dotnet/core/src/tasks/check-module-boundaries'),
201+
);
202+
203+
const fragment = new XmlDocument(`
204+
<Target Name="CheckNxModuleBoundaries" BeforeTargets="Build">
205+
<Exec Command="node ${scriptPath} -p ${options.name}"/>
206+
</Target>
207+
`);
208+
209+
xml.children.push(fragment);
210+
}

packages/core/src/generators/utils/generate-test-project.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
addDryRunParameter,
1414
NormalizedSchema,
1515
normalizeOptions,
16-
SetOutputPath,
16+
manipulateXmlProjectFile,
1717
} from './generate-project';
1818

1919
export async function GenerateTestProject(
@@ -68,10 +68,9 @@ export async function GenerateTestProject(
6868
dotnetClient.new(schema.testTemplate, newParams);
6969

7070
if (!isDryRun() && !schema.skipOutputPathManipulation) {
71+
await manipulateXmlProjectFile(host, { ...schema, projectRoot: testRoot });
7172
const testCsProj = await findProjectFileInPath(testRoot);
72-
SetOutputPath(host, testRoot, testCsProj);
7373
const baseCsProj = await findProjectFileInPath(schema.projectRoot);
74-
SetOutputPath(host, schema.projectRoot, baseCsProj);
7574
dotnetClient.addProjectReference(testCsProj, baseCsProj);
7675
}
7776
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './graph/process-project-graph';
2+
export * from './tasks/check-module-boundaries';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { Tree } from '@nrwl/devkit';
3+
import {
4+
getNxDotnetProjects,
5+
getProjectFileForNxProject,
6+
} from '@nx-dotnet/utils';
7+
8+
import { addPrebuildMsbuildTask } from '../../../generators/utils/generate-project';
9+
10+
import { XmlDocument } from 'xmldoc';
11+
12+
export default async function update(host: Tree) {
13+
const projects = getNxDotnetProjects(host);
14+
for (const [name, project] of projects.entries()) {
15+
const projectFilePath = await getProjectFileForNxProject(project);
16+
const buffer = host.read(projectFilePath);
17+
if (!buffer) {
18+
throw new Error(`Error reading file ${projectFilePath}`);
19+
}
20+
const xml = new XmlDocument(buffer.toString());
21+
if (
22+
!xml
23+
.childrenNamed('Target')
24+
.some((x) => x.attr['Name'] === 'CheckNxModuleBoundaries')
25+
) {
26+
addPrebuildMsbuildTask(host, { name, projectRoot: project.root }, xml);
27+
host.write(projectFilePath, xml.toString());
28+
}
29+
}
30+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
2+
import {
3+
ProjectConfiguration,
4+
WorkspaceJsonConfiguration,
5+
Workspaces,
6+
} from '@nrwl/tao/src/shared/workspace';
7+
8+
import { ESLint } from 'eslint';
9+
10+
import { getDependantProjectsForNxProject } from '@nx-dotnet/utils';
11+
import {
12+
NxJsonConfiguration,
13+
NxJsonProjectConfiguration,
14+
readJsonFile,
15+
} from '@nrwl/devkit';
16+
17+
type ExtendedWorkspaceJson = WorkspaceJsonConfiguration & {
18+
projects: Record<string, ProjectConfiguration & NxJsonProjectConfiguration>;
19+
};
20+
21+
export async function checkModuleBoundariesForProject(
22+
project: string,
23+
workspace: ExtendedWorkspaceJson,
24+
): Promise<string[]> {
25+
const projectRoot = workspace.projects[project].root;
26+
const tags = workspace.projects[project].tags ?? [];
27+
if (!tags.length) {
28+
//
29+
return [];
30+
}
31+
32+
const { rules } = await new ESLint().calculateConfigForFile(
33+
`${projectRoot}/non-existant.ts`,
34+
);
35+
const [, moduleBoundaryConfig] = rules['@nrwl/nx/enforce-module-boundaries'];
36+
const configuredConstraints: {
37+
sourceTag: '*' | string;
38+
onlyDependOnLibsWithTags: string[];
39+
}[] = moduleBoundaryConfig?.depConstraints ?? [];
40+
const relevantConstraints = configuredConstraints.filter(
41+
(x) =>
42+
tags.includes(x.sourceTag) && !x.onlyDependOnLibsWithTags.includes('*'),
43+
);
44+
if (!relevantConstraints.length) {
45+
return [];
46+
}
47+
48+
const violations: string[] = [];
49+
getDependantProjectsForNxProject(
50+
project,
51+
workspace,
52+
(configuration, name) => {
53+
const tags = configuration?.tags ?? [];
54+
for (const constraint of relevantConstraints) {
55+
if (
56+
!tags.some((x) => constraint.onlyDependOnLibsWithTags.includes(x))
57+
) {
58+
violations.push(
59+
`${project} cannot depend on ${name}. Project tag ${constraint} is not satisfied.`,
60+
);
61+
}
62+
}
63+
},
64+
);
65+
return violations;
66+
}
67+
68+
async function main() {
69+
const parser = await import('yargs-parser');
70+
const { project } = parser(process.argv.slice(2), {
71+
alias: {
72+
project: 'p',
73+
},
74+
});
75+
const workspace = new Workspaces(appRootPath);
76+
const workspaceJson: ExtendedWorkspaceJson =
77+
workspace.readWorkspaceConfiguration();
78+
const nxJsonProjects = readJsonFile<NxJsonConfiguration>(
79+
`${appRootPath}/nx.json`,
80+
).projects;
81+
if (nxJsonProjects) {
82+
Object.entries(nxJsonProjects).forEach(([name, config]) => {
83+
const existingTags = workspaceJson.projects[name]?.tags ?? [];
84+
workspaceJson.projects[name].tags = [
85+
...existingTags,
86+
...(config.tags ?? []),
87+
];
88+
});
89+
}
90+
console.log(`Checking module boundaries for ${project}`);
91+
const violations = await checkModuleBoundariesForProject(
92+
project,
93+
workspaceJson,
94+
);
95+
if (violations.length) {
96+
violations.forEach((error) => {
97+
console.error(error);
98+
});
99+
process.exit(1);
100+
}
101+
process.exit(0);
102+
}
103+
104+
if (require.main === module) {
105+
process.chdir(appRootPath);
106+
main();
107+
}

packages/utils/src/lib/utility-functions/glob.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import * as _glob from 'glob';
2+
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
3+
4+
const globOptions = {
5+
cwd: appRootPath,
6+
};
27

38
/**
49
* Wraps the glob package in a promise api.
510
* @returns array of file paths
611
*/
712
export function glob(path: string): Promise<string[]> {
813
return new Promise((resolve, reject) =>
9-
_glob(path, (err, matches) => (err ? reject() : resolve(matches))),
14+
_glob(path, globOptions, (err, matches) =>
15+
err ? reject() : resolve(matches),
16+
),
1017
);
1118
}
1219

@@ -30,7 +37,7 @@ export function findProjectFileInPath(path: string): Promise<string> {
3037
}
3138

3239
export function findProjectFileInPathSync(path: string): string {
33-
const results = _glob.sync(`${path}/**/*.*proj`);
40+
const results = _glob.sync(`${path}/**/*.*proj`, globOptions);
3441
if (!results || results.length === 0) {
3542
throw new Error(
3643
"Unable to find a build-able project within project's source directory!",

packages/utils/src/lib/utility-functions/workspace.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export function getDependantProjectsForNxProject(
2929
targetProject: string,
3030
workspaceConfiguration: WorkspaceJsonConfiguration,
3131
forEachCallback?: (
32-
project: ProjectConfiguration & { projectFile: string },
32+
project: ProjectConfiguration &
33+
NxJsonProjectConfiguration & { projectFile: string },
3334
projectName: string,
3435
) => void,
3536
): {

0 commit comments

Comments
 (0)