Skip to content

Commit

Permalink
feat(core): #81 support for nx-enforce-module-boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Aug 20, 2021
1 parent f652fc4 commit 3fc92fd
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 77 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@
"@types/xmldoc": "^1.1.5",
"chokidar": "^3.5.1",
"clsx": "^1.1.1",
"eslint": "^7.22.0",
"glob": "^7.1.6",
"inquirer": "^8.0.0",
"prism-react-renderer": "^1.2.1",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"rimraf": "^3.0.2",
"rxjs": "^7.0.1",
"xmldoc": "^1.1.2"
"xmldoc": "^1.1.2",
"yargs-parser": "^20.2.9"
},
"devDependencies": {
"@commitlint/cli": "^12.1.1",
Expand Down Expand Up @@ -83,10 +85,10 @@
"@types/react": "17",
"@types/rimraf": "^3.0.0",
"@types/tmp": "^0.2.0",
"@types/yargs-parser": "^20.2.1",
"@typescript-eslint/eslint-plugin": "4.19.0",
"@typescript-eslint/parser": "4.19.0",
"dotenv": "8.2.0",
"eslint": "7.22.0",
"eslint-config-prettier": "8.1.0",
"fs-extra": "^10.0.0",
"husky": "^6.0.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
"description": "Adds lint target to all existing dotnet projects",
"cli": "nx",
"implementation": "./src/migrations/add-lint-target/add-lint-target"
},
"1.2.0-add-module-boundaries-check": {
"version": "1.2.0",
"description": "1.2.0-add-module-boundaries-check",
"cli": "nx",
"implementation": "./src/migrations/1.2.0/add-module-boundaries-check/migrate"
}
}
}
85 changes: 48 additions & 37 deletions packages/core/src/generators/utils/generate-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
Tree,
} from '@nrwl/devkit';

// Files generated via `dotnet` are not available in the virtual fs
import { readFileSync, writeFileSync } from 'fs';

import { dirname, relative } from 'path';
import { XmlDocument, XmlNode, XmlTextNode } from 'xmldoc';
import { XmlDocument } from 'xmldoc';

import { DotNetClient, dotnetNewOptions } from '@nx-dotnet/dotnet';
import {
Expand Down Expand Up @@ -84,41 +86,18 @@ export function normalizeOptions(
};
}

export function SetOutputPath(
export async function manipulateXmlProjectFile(
host: Tree,
projectRootPath: string,
projectFilePath: string,
): void {
options: NormalizedSchema,
): Promise<void> {
const projectFilePath = await findProjectFileInPath(options.projectRoot);

const xml: XmlDocument = new XmlDocument(
readFileSync(projectFilePath).toString(),
);

let outputPath = `${relative(
dirname(projectFilePath),
process.cwd(),
)}/dist/${projectRootPath}`;
outputPath = outputPath.replace('\\', '/'); // Forward slash works on windows, backslash does not work on mac/linux

const textNode: Partial<XmlTextNode> = {
text: outputPath,
type: 'text',
};
textNode.toString = () => textNode.text ?? '';
textNode.toStringWithIndent = () => textNode.text ?? '';

const el: Partial<XmlNode> = {
name: 'OutputPath',
attr: {},
type: 'element',
children: [textNode as XmlTextNode],
firstChild: null,
lastChild: null,
};

el.toStringWithIndent = xml.toStringWithIndent.bind(el);
el.toString = xml.toString.bind(el);

xml.childNamed('PropertyGroup')?.children.push(el as XmlNode);
setOutputPath(xml, options.projectRoot, projectFilePath);
addPrebuildMsbuildTask(host, options, xml);

writeFileSync(projectFilePath, xml.toString());
}
Expand Down Expand Up @@ -180,12 +159,10 @@ export async function GenerateProject(

if (options['testTemplate'] !== 'none') {
await GenerateTestProject(host, normalizedOptions, dotnetClient);
} else if (!options.skipOutputPathManipulation) {
SetOutputPath(
host,
normalizedOptions.projectRoot,
await findProjectFileInPath(normalizedOptions.projectRoot),
);
}

if (!options.skipOutputPathManipulation && !isDryRun()) {
await manipulateXmlProjectFile(host, normalizedOptions);
}

await formatFiles(host);
Expand All @@ -197,3 +174,37 @@ export function addDryRunParameter(parameters: dotnetNewOptions): void {
value: true,
});
}

export function setOutputPath(
xml: XmlDocument,
projectRootPath: string,
projectFilePath: string,
) {
let outputPath = `${relative(
dirname(projectFilePath),
process.cwd(),
)}/dist/${projectRootPath}`;
outputPath = outputPath.replace('\\', '/'); // Forward slash works on windows, backslash does not work on mac/linux

const fragment = new XmlDocument(`<OutputPath>${outputPath}</OutputPath>`);
xml.childNamed('PropertyGroup')?.children.push(fragment);
}

export function addPrebuildMsbuildTask(
host: Tree,
options: { projectRoot: string; name: string },
xml: XmlDocument,
) {
const scriptPath = relative(
options.projectRoot,
require.resolve('@nx-dotnet/core/src/tasks/check-module-boundaries'),
);

const fragment = new XmlDocument(`
<Target Name="CheckNxModuleBoundaries" BeforeTargets="Build">
<Exec Command="node ${scriptPath} -p ${options.name}"/>
</Target>
`);

xml.children.push(fragment);
}
5 changes: 2 additions & 3 deletions packages/core/src/generators/utils/generate-test-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
addDryRunParameter,
NormalizedSchema,
normalizeOptions,
SetOutputPath,
manipulateXmlProjectFile,
} from './generate-project';

export async function GenerateTestProject(
Expand Down Expand Up @@ -68,10 +68,9 @@ export async function GenerateTestProject(
dotnetClient.new(schema.testTemplate, newParams);

if (!isDryRun() && !schema.skipOutputPathManipulation) {
await manipulateXmlProjectFile(host, { ...schema, projectRoot: testRoot });
const testCsProj = await findProjectFileInPath(testRoot);
SetOutputPath(host, testRoot, testCsProj);
const baseCsProj = await findProjectFileInPath(schema.projectRoot);
SetOutputPath(host, schema.projectRoot, baseCsProj);
dotnetClient.addProjectReference(testCsProj, baseCsProj);
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './graph/process-project-graph';
export * from './tasks/check-module-boundaries';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Tree } from '@nrwl/devkit';
import {
getNxDotnetProjects,
getProjectFileForNxProject,
} from '@nx-dotnet/utils';

import { addPrebuildMsbuildTask } from '../../../generators/utils/generate-project';

import { XmlDocument } from 'xmldoc';

export default async function update(host: Tree) {
const projects = getNxDotnetProjects(host);
for (const [name, project] of projects.entries()) {
const projectFilePath = await getProjectFileForNxProject(project);
const buffer = host.read(projectFilePath);
if (!buffer) {
throw new Error(`Error reading file ${projectFilePath}`);
}
const xml = new XmlDocument(buffer.toString());
if (
!xml
.childrenNamed('Target')
.some((x) => x.attr['Name'] === 'CheckNxModuleBoundaries')
) {
addPrebuildMsbuildTask(host, { name, projectRoot: project.root }, xml);
host.write(projectFilePath, xml.toString());
}
}
}
107 changes: 107 additions & 0 deletions packages/core/src/tasks/check-module-boundaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import {
ProjectConfiguration,
WorkspaceJsonConfiguration,
Workspaces,
} from '@nrwl/tao/src/shared/workspace';

import { ESLint } from 'eslint';

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

type ExtendedWorkspaceJson = WorkspaceJsonConfiguration & {
projects: Record<string, ProjectConfiguration & NxJsonProjectConfiguration>;
};

export async function checkModuleBoundariesForProject(
project: string,
workspace: ExtendedWorkspaceJson,
): Promise<string[]> {
const projectRoot = workspace.projects[project].root;
const tags = workspace.projects[project].tags ?? [];
if (!tags.length) {
//
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 relevantConstraints = configuredConstraints.filter(
(x) =>
tags.includes(x.sourceTag) && !x.onlyDependOnLibsWithTags.includes('*'),
);
if (!relevantConstraints.length) {
return [];
}

const violations: string[] = [];
getDependantProjectsForNxProject(
project,
workspace,
(configuration, name) => {
const tags = configuration?.tags ?? [];
for (const constraint of relevantConstraints) {
if (
!tags.some((x) => constraint.onlyDependOnLibsWithTags.includes(x))
) {
violations.push(
`${project} cannot depend on ${name}. Project tag ${constraint} is not satisfied.`,
);
}
}
},
);
return violations;
}

async function main() {
const parser = await import('yargs-parser');
const { project } = parser(process.argv.slice(2), {
alias: {
project: 'p',
},
});
const workspace = new Workspaces(appRootPath);
const workspaceJson: ExtendedWorkspaceJson =
workspace.readWorkspaceConfiguration();
const nxJsonProjects = readJsonFile<NxJsonConfiguration>(
`${appRootPath}/nx.json`,
).projects;
if (nxJsonProjects) {
Object.entries(nxJsonProjects).forEach(([name, config]) => {
const existingTags = workspaceJson.projects[name]?.tags ?? [];
workspaceJson.projects[name].tags = [
...existingTags,
...(config.tags ?? []),
];
});
}
console.log(`Checking module boundaries for ${project}`);
const violations = await checkModuleBoundariesForProject(
project,
workspaceJson,
);
if (violations.length) {
violations.forEach((error) => {
console.error(error);
});
process.exit(1);
}
process.exit(0);
}

if (require.main === module) {
process.chdir(appRootPath);
main();
}
11 changes: 9 additions & 2 deletions packages/utils/src/lib/utility-functions/glob.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import * as _glob from 'glob';
import { appRootPath } from '@nrwl/tao/src/utils/app-root';

const globOptions = {
cwd: appRootPath,
};

/**
* Wraps the glob package in a promise api.
* @returns array of file paths
*/
export function glob(path: string): Promise<string[]> {
return new Promise((resolve, reject) =>
_glob(path, (err, matches) => (err ? reject() : resolve(matches))),
_glob(path, globOptions, (err, matches) =>
err ? reject() : resolve(matches),
),
);
}

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

export function findProjectFileInPathSync(path: string): string {
const results = _glob.sync(`${path}/**/*.*proj`);
const results = _glob.sync(`${path}/**/*.*proj`, globOptions);
if (!results || results.length === 0) {
throw new Error(
"Unable to find a build-able project within project's source directory!",
Expand Down
3 changes: 2 additions & 1 deletion packages/utils/src/lib/utility-functions/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export function getDependantProjectsForNxProject(
targetProject: string,
workspaceConfiguration: WorkspaceJsonConfiguration,
forEachCallback?: (
project: ProjectConfiguration & { projectFile: string },
project: ProjectConfiguration &
NxJsonProjectConfiguration & { projectFile: string },
projectName: string,
) => void,
): {
Expand Down
Loading

0 comments on commit 3fc92fd

Please sign in to comment.