Skip to content

Commit 8be8446

Browse files
committed
feat(core): ability to import projects into the nx configuration
1 parent 0c3a3aa commit 8be8446

File tree

12 files changed

+302
-9
lines changed

12 files changed

+302
-9
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @nx-dotnet/core:import-projects
2+
3+
## Import Projects
4+
5+
Import existing .NET projects in C#, VB, or F# that are in your workspace's apps or libs directories. Simply move the projects into these folders, and then run `nx g @nx-dotnet/core:import-projects` to move them into Nx. Projects inside the apps directory will include a serve target, while projects inside libs will only contain build + lint targets.

docs/core/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Restores NuGet packages and .NET tools used by the workspace
113113

114114
Generate a .NET test project for an existing application or library
115115

116+
### [import-projects](./generators/import-projects.md)
117+
118+
Import existing projects into your nx workspace
119+
116120
## Executors
117121

118122
### [build](./executors/build.md)

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ slug: /
88
## [@nx-dotnet/core](./core)
99

1010
- 5 Executors
11-
- 8 Generators
11+
- 9 Generators
1212

1313
## [@nx-dotnet/nx-ghpages](./nx-ghpages)
1414

e2e/core-e2e/tests/nx-dotnet.spec.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { joinPathFragments, names } from '@nrwl/devkit';
1+
import {
2+
joinPathFragments,
3+
names,
4+
WorkspaceJsonConfiguration,
5+
} from '@nrwl/devkit';
26
import {
37
checkFilesExist,
48
ensureNxProject,
59
readFile,
10+
readJson,
611
runNxCommandAsync,
712
uniq,
813
} from '@nrwl/nx-plugin/testing';
@@ -13,6 +18,8 @@ import { XmlDocument } from 'xmldoc';
1318

1419
import { findProjectFileInPathSync } from '@nx-dotnet/utils';
1520
import { readDependenciesFromNxCache } from '@nx-dotnet/utils/e2e';
21+
import { execSync } from 'child_process';
22+
import { ensureDirSync } from 'fs-extra';
1623

1724
const e2eDir = 'tmp/nx-e2e/proj';
1825

@@ -196,4 +203,41 @@ describe('nx-dotnet e2e', () => {
196203
expect(exists).toBeTruthy();
197204
});
198205
});
206+
207+
describe('nx g import-projects', () => {
208+
it('should import apps, libs, and test', async () => {
209+
const testApp = uniq('app');
210+
const testLib = uniq('lib');
211+
const testAppTest = `${testApp}-test`;
212+
ensureNxProject('@nx-dotnet/core', 'dist/packages/core');
213+
const appDir = `${e2eDir}/apps/${testApp}`;
214+
const testAppDir = `${e2eDir}/apps/${testAppTest}`;
215+
const libDir = `${e2eDir}/libs/${testLib}`;
216+
ensureDirSync(appDir);
217+
ensureDirSync(libDir);
218+
ensureDirSync(testAppDir);
219+
execSync('dotnet new webapi', { cwd: appDir });
220+
execSync('dotnet new classlib', { cwd: libDir });
221+
execSync('dotnet new nunit', { cwd: testAppDir });
222+
223+
await runNxCommandAsync(`generate @nx-dotnet/core:import-projects`);
224+
225+
const workspace = readJson<WorkspaceJsonConfiguration>('workspace.json');
226+
227+
console.log('workspace', workspace);
228+
229+
expect(workspace.projects[testApp].targets.serve).toBeDefined();
230+
expect(workspace.projects[testApp].targets.build).toBeDefined();
231+
expect(workspace.projects[testApp].targets.lint).toBeDefined();
232+
expect(workspace.projects[testLib].targets.serve).not.toBeDefined();
233+
expect(workspace.projects[testLib].targets.build).toBeDefined();
234+
expect(workspace.projects[testLib].targets.lint).toBeDefined();
235+
expect(workspace.projects[testAppTest].targets.build).toBeDefined();
236+
expect(workspace.projects[testAppTest].targets.lint).toBeDefined();
237+
expect(workspace.projects[testAppTest].targets.test).toBeDefined();
238+
239+
await runNxCommandAsync(`build ${testApp}`);
240+
checkFilesExist(`dist/apps/${testApp}`);
241+
});
242+
});
199243
});

packages/core/generators.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"schema": "./src/generators/test/schema.json",
5050
"description": "Generate a .NET test project for an existing application or library",
5151
"x-type": "library"
52+
},
53+
"import-projects": {
54+
"factory": "./src/generators/import-projects/generator",
55+
"schema": "./src/generators/import-projects/schema.json",
56+
"description": "Import existing projects into your nx workspace"
5257
}
5358
}
5459
}

packages/core/src/executors/publish/executor.spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ExecutorContext } from '@nrwl/devkit';
1+
import {
2+
ExecutorContext,
3+
joinPathFragments,
4+
normalizePath,
5+
} from '@nrwl/devkit';
26

37
import { promises as fs } from 'fs';
48

@@ -102,7 +106,7 @@ describe('Publish Executor', () => {
102106
});
103107

104108
it('should pass path relative to project root, not workspace root', async () => {
105-
const directoryPath = `${root}/apps/my-app`;
109+
const directoryPath = joinPathFragments(root, './apps/my-app');
106110
try {
107111
await fs.mkdir(directoryPath, { recursive: true });
108112
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
@@ -111,7 +115,7 @@ describe('Publish Executor', () => {
111115
}
112116
const res = await executor(options, context, dotnetClient);
113117
expect(dotnetClient.publish).toHaveBeenCalled();
114-
expect(dotnetClient.cwd).toEqual(directoryPath);
118+
expect(normalizePath(dotnetClient.cwd || '')).toEqual(directoryPath);
115119
expect(res.success).toBeTruthy();
116120
});
117121
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
2+
import { Tree, readProjectConfiguration, getProjects } from '@nrwl/devkit';
3+
4+
import generator from './generator';
5+
import * as utils from '@nx-dotnet/utils';
6+
import * as fs from 'fs';
7+
8+
jest.mock('@nx-dotnet/utils', () => ({
9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
...(jest.requireActual('@nx-dotnet/utils') as any),
11+
glob: jest.fn(),
12+
findProjectFileInPath: jest.fn(),
13+
resolve: (m: string) => m,
14+
}));
15+
16+
const MOCK_API_PROJECT = `
17+
<Project Sdk="Microsoft.NET.Sdk.Web">
18+
<PropertyGroup>
19+
<TargetFramework>net5.0</TargetFramework>
20+
<RootNamespace>MyTestApi</RootNamespace>
21+
</PropertyGroup>
22+
</Project>`;
23+
24+
const MOCK_TEST_PROJECT = `
25+
<Project Sdk="Microsoft.NET.Sdk.Web">
26+
<PropertyGroup>
27+
<TargetFramework>net5.0</TargetFramework>
28+
<RootNamespace>MyTestApi.Test</RootNamespace>
29+
</PropertyGroup>
30+
<ItemGroup>
31+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
32+
</ItemGroup>
33+
</Project>`;
34+
35+
describe('import-projects generator', () => {
36+
let appTree: Tree;
37+
38+
beforeEach(() => {
39+
appTree = createTreeWithEmptyWorkspace();
40+
});
41+
42+
afterEach(() => {
43+
jest.resetAllMocks();
44+
});
45+
46+
it('should run successfully if no new projects are found', async () => {
47+
jest.spyOn(utils, 'glob').mockResolvedValue([]);
48+
const promise = generator(appTree);
49+
const oldProjects = getProjects(appTree);
50+
await expect(promise).resolves.not.toThrow();
51+
const newProjects = getProjects(appTree);
52+
expect(oldProjects).toEqual(newProjects);
53+
});
54+
55+
it('should run successfully if new projects are found', async () => {
56+
jest
57+
.spyOn(utils, 'glob')
58+
.mockImplementation((x) =>
59+
Promise.resolve(
60+
x.startsWith('apps') ? ['apps/my-api/my-api.csproj'] : [],
61+
),
62+
);
63+
jest
64+
.spyOn(utils, 'findProjectFileInPath')
65+
.mockImplementation((x) =>
66+
x.startsWith('apps')
67+
? Promise.resolve('apps/my-api/my-api.csproj')
68+
: Promise.reject(),
69+
);
70+
jest.spyOn(fs, 'readFileSync').mockReturnValue(MOCK_TEST_PROJECT);
71+
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => null);
72+
appTree.write('apps/my-api/my-api.csproj', MOCK_API_PROJECT);
73+
const promise = generator(appTree);
74+
await expect(promise).resolves.not.toThrow();
75+
expect(readProjectConfiguration(appTree, 'my-test-api')).toBeDefined();
76+
});
77+
78+
it('should run add test target if test projects are found', async () => {
79+
jest
80+
.spyOn(utils, 'glob')
81+
.mockImplementation((x) =>
82+
Promise.resolve(
83+
x.startsWith('apps') ? ['apps/my-api-test/my-api-test.csproj'] : [],
84+
),
85+
);
86+
jest
87+
.spyOn(utils, 'findProjectFileInPath')
88+
.mockImplementation((x) =>
89+
x.startsWith('apps')
90+
? Promise.resolve('apps/my-api/my-api-test.csproj')
91+
: Promise.reject(),
92+
);
93+
jest.spyOn(fs, 'readFileSync').mockReturnValue(MOCK_TEST_PROJECT);
94+
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => null);
95+
appTree.write('apps/my-api-test/my-api-test.csproj', MOCK_TEST_PROJECT);
96+
const promise = generator(appTree);
97+
await expect(promise).resolves.not.toThrow();
98+
expect(readProjectConfiguration(appTree, 'my-test-api-test')).toBeDefined();
99+
expect(
100+
readProjectConfiguration(appTree, 'my-test-api-test').targets.test,
101+
).toBeDefined();
102+
expect(
103+
readProjectConfiguration(appTree, 'my-test-api-test').targets.serve,
104+
).not.toBeDefined();
105+
});
106+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
addProjectConfiguration,
3+
formatFiles,
4+
getProjects,
5+
getWorkspaceLayout,
6+
names,
7+
NxJsonProjectConfiguration,
8+
ProjectConfiguration,
9+
Tree,
10+
} from '@nrwl/devkit';
11+
12+
import { basename, dirname } from 'path';
13+
import { XmlDocument } from 'xmldoc';
14+
15+
import { glob, iterateChildrenByPath, NXDOTNET_TAG } from '@nx-dotnet/utils';
16+
17+
import {
18+
GetBuildExecutorConfiguration,
19+
GetLintExecutorConfiguration,
20+
GetServeExecutorConfig,
21+
GetTestExecutorConfig,
22+
} from '../../models';
23+
import { manipulateXmlProjectFile } from '../utils/generate-project';
24+
25+
export default async function (host: Tree) {
26+
const projectFiles = await getProjectFilesInWorkspace(host);
27+
const existingProjectRoots = Array.from(getProjects(host).values()).map(
28+
(x) => x.root,
29+
);
30+
for (const projectFile of projectFiles.newLibs) {
31+
if (!existingProjectRoots.some((x) => projectFile.startsWith(x))) {
32+
await addNewDotnetProject(host, projectFile, false);
33+
console.log('Found new library', projectFile);
34+
}
35+
}
36+
for (const projectFile of projectFiles.newApps) {
37+
if (!existingProjectRoots.some((x) => projectFile.startsWith(x))) {
38+
await addNewDotnetProject(host, projectFile, true);
39+
console.log('Found new application', projectFile);
40+
}
41+
}
42+
return formatFiles(host);
43+
}
44+
45+
async function addNewDotnetProject(
46+
host: Tree,
47+
projectFile: string,
48+
app: boolean,
49+
) {
50+
const rootNamespace = readRootNamespace(host, projectFile);
51+
const projectRoot = dirname(projectFile);
52+
const projectName = rootNamespace
53+
? names(rootNamespace).fileName.replace(/\./g, '-')
54+
: names(basename(projectRoot)).fileName;
55+
const configuration: ProjectConfiguration & NxJsonProjectConfiguration = {
56+
root: projectRoot,
57+
targets: {
58+
build: GetBuildExecutorConfiguration(projectRoot),
59+
lint: GetLintExecutorConfiguration(),
60+
},
61+
tags: [NXDOTNET_TAG],
62+
projectType: app ? 'application' : 'library',
63+
};
64+
const testProject = await checkIfTestProject(host, projectFile);
65+
if (app && !testProject) {
66+
configuration.targets.serve = GetServeExecutorConfig();
67+
}
68+
if (testProject) {
69+
configuration.targets.test = GetTestExecutorConfig();
70+
}
71+
addProjectConfiguration(host, projectName, configuration);
72+
await manipulateXmlProjectFile(host, {
73+
projectName,
74+
projectRoot,
75+
});
76+
}
77+
78+
async function getProjectFilesInWorkspace(host: Tree) {
79+
const { appsDir, libsDir } = getWorkspaceLayout(host);
80+
const newProjects = {
81+
newLibs: await glob(`${libsDir}/**/*.@(cs|fs|vb)proj`),
82+
newApps: [] as string[],
83+
};
84+
if (libsDir !== appsDir) {
85+
newProjects.newApps = await glob(`${appsDir}/**/*.@(cs|fs|vb)proj`);
86+
}
87+
return newProjects;
88+
}
89+
90+
function readRootNamespace(host: Tree, path: string): string | undefined {
91+
const xml = new XmlDocument(host.read(path)?.toString() as string);
92+
return xml.valueWithPath('PropertyGroup.RootNamespace');
93+
}
94+
95+
async function checkIfTestProject(host: Tree, path: string): Promise<boolean> {
96+
const xml = new XmlDocument(host.read(path)?.toString() as string);
97+
let isTestProject = false;
98+
await iterateChildrenByPath(xml, 'ItemGroup.PackageReference', (el) => {
99+
const pkg = el.attr['Include'];
100+
if (pkg === 'Microsoft.NET.Test.Sdk') {
101+
isTestProject = true;
102+
}
103+
});
104+
return isTestProject;
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"cli": "nx",
4+
"id": "@nx-dotnet/core:import-projects",
5+
"title": "Import Projects",
6+
"description": "Import existing .NET projects in C#, VB, or F# that are in your workspace's apps or libs directories. Simply move the projects into these folders, and then run `nx g @nx-dotnet/core:import-projects` to move them into Nx. Projects inside the apps directory will include a serve target, while projects inside libs will only contain build + lint targets.",
7+
"type": "object",
8+
"properties": {}
9+
}

0 commit comments

Comments
 (0)