Skip to content

Commit

Permalink
feat(core): ability to import projects into the nx configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Sep 10, 2021
1 parent 0c3a3aa commit 8be8446
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 9 deletions.
5 changes: 5 additions & 0 deletions docs/core/generators/import-projects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @nx-dotnet/core:import-projects

## Import Projects

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.
4 changes: 4 additions & 0 deletions docs/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ Restores NuGet packages and .NET tools used by the workspace

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

### [import-projects](./generators/import-projects.md)

Import existing projects into your nx workspace

## Executors

### [build](./executors/build.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ slug: /
## [@nx-dotnet/core](./core)

- 5 Executors
- 8 Generators
- 9 Generators

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

Expand Down
46 changes: 45 additions & 1 deletion e2e/core-e2e/tests/nx-dotnet.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { joinPathFragments, names } from '@nrwl/devkit';
import {
joinPathFragments,
names,
WorkspaceJsonConfiguration,
} from '@nrwl/devkit';
import {
checkFilesExist,
ensureNxProject,
readFile,
readJson,
runNxCommandAsync,
uniq,
} from '@nrwl/nx-plugin/testing';
Expand All @@ -13,6 +18,8 @@ import { XmlDocument } from 'xmldoc';

import { findProjectFileInPathSync } from '@nx-dotnet/utils';
import { readDependenciesFromNxCache } from '@nx-dotnet/utils/e2e';
import { execSync } from 'child_process';
import { ensureDirSync } from 'fs-extra';

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

Expand Down Expand Up @@ -196,4 +203,41 @@ describe('nx-dotnet e2e', () => {
expect(exists).toBeTruthy();
});
});

describe('nx g import-projects', () => {
it('should import apps, libs, and test', async () => {
const testApp = uniq('app');
const testLib = uniq('lib');
const testAppTest = `${testApp}-test`;
ensureNxProject('@nx-dotnet/core', 'dist/packages/core');
const appDir = `${e2eDir}/apps/${testApp}`;
const testAppDir = `${e2eDir}/apps/${testAppTest}`;
const libDir = `${e2eDir}/libs/${testLib}`;
ensureDirSync(appDir);
ensureDirSync(libDir);
ensureDirSync(testAppDir);
execSync('dotnet new webapi', { cwd: appDir });
execSync('dotnet new classlib', { cwd: libDir });
execSync('dotnet new nunit', { cwd: testAppDir });

await runNxCommandAsync(`generate @nx-dotnet/core:import-projects`);

const workspace = readJson<WorkspaceJsonConfiguration>('workspace.json');

console.log('workspace', workspace);

expect(workspace.projects[testApp].targets.serve).toBeDefined();
expect(workspace.projects[testApp].targets.build).toBeDefined();
expect(workspace.projects[testApp].targets.lint).toBeDefined();
expect(workspace.projects[testLib].targets.serve).not.toBeDefined();
expect(workspace.projects[testLib].targets.build).toBeDefined();
expect(workspace.projects[testLib].targets.lint).toBeDefined();
expect(workspace.projects[testAppTest].targets.build).toBeDefined();
expect(workspace.projects[testAppTest].targets.lint).toBeDefined();
expect(workspace.projects[testAppTest].targets.test).toBeDefined();

await runNxCommandAsync(`build ${testApp}`);
checkFilesExist(`dist/apps/${testApp}`);
});
});
});
5 changes: 5 additions & 0 deletions packages/core/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
"schema": "./src/generators/test/schema.json",
"description": "Generate a .NET test project for an existing application or library",
"x-type": "library"
},
"import-projects": {
"factory": "./src/generators/import-projects/generator",
"schema": "./src/generators/import-projects/schema.json",
"description": "Import existing projects into your nx workspace"
}
}
}
10 changes: 7 additions & 3 deletions packages/core/src/executors/publish/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ExecutorContext } from '@nrwl/devkit';
import {
ExecutorContext,
joinPathFragments,
normalizePath,
} from '@nrwl/devkit';

import { promises as fs } from 'fs';

Expand Down Expand Up @@ -102,7 +106,7 @@ describe('Publish Executor', () => {
});

it('should pass path relative to project root, not workspace root', async () => {
const directoryPath = `${root}/apps/my-app`;
const directoryPath = joinPathFragments(root, './apps/my-app');
try {
await fs.mkdir(directoryPath, { recursive: true });
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
Expand All @@ -111,7 +115,7 @@ describe('Publish Executor', () => {
}
const res = await executor(options, context, dotnetClient);
expect(dotnetClient.publish).toHaveBeenCalled();
expect(dotnetClient.cwd).toEqual(directoryPath);
expect(normalizePath(dotnetClient.cwd || '')).toEqual(directoryPath);
expect(res.success).toBeTruthy();
});
});
106 changes: 106 additions & 0 deletions packages/core/src/generators/import-projects/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Tree, readProjectConfiguration, getProjects } from '@nrwl/devkit';

import generator from './generator';
import * as utils from '@nx-dotnet/utils';
import * as fs from 'fs';

jest.mock('@nx-dotnet/utils', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(jest.requireActual('@nx-dotnet/utils') as any),
glob: jest.fn(),
findProjectFileInPath: jest.fn(),
resolve: (m: string) => m,
}));

const MOCK_API_PROJECT = `
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>MyTestApi</RootNamespace>
</PropertyGroup>
</Project>`;

const MOCK_TEST_PROJECT = `
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>MyTestApi.Test</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
</ItemGroup>
</Project>`;

describe('import-projects generator', () => {
let appTree: Tree;

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

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

it('should run successfully if no new projects are found', async () => {
jest.spyOn(utils, 'glob').mockResolvedValue([]);
const promise = generator(appTree);
const oldProjects = getProjects(appTree);
await expect(promise).resolves.not.toThrow();
const newProjects = getProjects(appTree);
expect(oldProjects).toEqual(newProjects);
});

it('should run successfully if new projects are found', async () => {
jest
.spyOn(utils, 'glob')
.mockImplementation((x) =>
Promise.resolve(
x.startsWith('apps') ? ['apps/my-api/my-api.csproj'] : [],
),
);
jest
.spyOn(utils, 'findProjectFileInPath')
.mockImplementation((x) =>
x.startsWith('apps')
? Promise.resolve('apps/my-api/my-api.csproj')
: Promise.reject(),
);
jest.spyOn(fs, 'readFileSync').mockReturnValue(MOCK_TEST_PROJECT);
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => null);
appTree.write('apps/my-api/my-api.csproj', MOCK_API_PROJECT);
const promise = generator(appTree);
await expect(promise).resolves.not.toThrow();
expect(readProjectConfiguration(appTree, 'my-test-api')).toBeDefined();
});

it('should run add test target if test projects are found', async () => {
jest
.spyOn(utils, 'glob')
.mockImplementation((x) =>
Promise.resolve(
x.startsWith('apps') ? ['apps/my-api-test/my-api-test.csproj'] : [],
),
);
jest
.spyOn(utils, 'findProjectFileInPath')
.mockImplementation((x) =>
x.startsWith('apps')
? Promise.resolve('apps/my-api/my-api-test.csproj')
: Promise.reject(),
);
jest.spyOn(fs, 'readFileSync').mockReturnValue(MOCK_TEST_PROJECT);
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => null);
appTree.write('apps/my-api-test/my-api-test.csproj', MOCK_TEST_PROJECT);
const promise = generator(appTree);
await expect(promise).resolves.not.toThrow();
expect(readProjectConfiguration(appTree, 'my-test-api-test')).toBeDefined();
expect(
readProjectConfiguration(appTree, 'my-test-api-test').targets.test,
).toBeDefined();
expect(
readProjectConfiguration(appTree, 'my-test-api-test').targets.serve,
).not.toBeDefined();
});
});
105 changes: 105 additions & 0 deletions packages/core/src/generators/import-projects/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
addProjectConfiguration,
formatFiles,
getProjects,
getWorkspaceLayout,
names,
NxJsonProjectConfiguration,
ProjectConfiguration,
Tree,
} from '@nrwl/devkit';

import { basename, dirname } from 'path';
import { XmlDocument } from 'xmldoc';

import { glob, iterateChildrenByPath, NXDOTNET_TAG } from '@nx-dotnet/utils';

import {
GetBuildExecutorConfiguration,
GetLintExecutorConfiguration,
GetServeExecutorConfig,
GetTestExecutorConfig,
} from '../../models';
import { manipulateXmlProjectFile } from '../utils/generate-project';

export default async function (host: Tree) {
const projectFiles = await getProjectFilesInWorkspace(host);
const existingProjectRoots = Array.from(getProjects(host).values()).map(
(x) => x.root,
);
for (const projectFile of projectFiles.newLibs) {
if (!existingProjectRoots.some((x) => projectFile.startsWith(x))) {
await addNewDotnetProject(host, projectFile, false);
console.log('Found new library', projectFile);
}
}
for (const projectFile of projectFiles.newApps) {
if (!existingProjectRoots.some((x) => projectFile.startsWith(x))) {
await addNewDotnetProject(host, projectFile, true);
console.log('Found new application', projectFile);
}
}
return formatFiles(host);
}

async function addNewDotnetProject(
host: Tree,
projectFile: string,
app: boolean,
) {
const rootNamespace = readRootNamespace(host, projectFile);
const projectRoot = dirname(projectFile);
const projectName = rootNamespace
? names(rootNamespace).fileName.replace(/\./g, '-')
: names(basename(projectRoot)).fileName;
const configuration: ProjectConfiguration & NxJsonProjectConfiguration = {
root: projectRoot,
targets: {
build: GetBuildExecutorConfiguration(projectRoot),
lint: GetLintExecutorConfiguration(),
},
tags: [NXDOTNET_TAG],
projectType: app ? 'application' : 'library',
};
const testProject = await checkIfTestProject(host, projectFile);
if (app && !testProject) {
configuration.targets.serve = GetServeExecutorConfig();
}
if (testProject) {
configuration.targets.test = GetTestExecutorConfig();
}
addProjectConfiguration(host, projectName, configuration);
await manipulateXmlProjectFile(host, {
projectName,
projectRoot,
});
}

async function getProjectFilesInWorkspace(host: Tree) {
const { appsDir, libsDir } = getWorkspaceLayout(host);
const newProjects = {
newLibs: await glob(`${libsDir}/**/*.@(cs|fs|vb)proj`),
newApps: [] as string[],
};
if (libsDir !== appsDir) {
newProjects.newApps = await glob(`${appsDir}/**/*.@(cs|fs|vb)proj`);
}
return newProjects;
}

function readRootNamespace(host: Tree, path: string): string | undefined {
const xml = new XmlDocument(host.read(path)?.toString() as string);
return xml.valueWithPath('PropertyGroup.RootNamespace');
}

async function checkIfTestProject(host: Tree, path: string): Promise<boolean> {
const xml = new XmlDocument(host.read(path)?.toString() as string);
let isTestProject = false;
await iterateChildrenByPath(xml, 'ItemGroup.PackageReference', (el) => {
const pkg = el.attr['Include'];
if (pkg === 'Microsoft.NET.Test.Sdk') {
isTestProject = true;
}
});
return isTestProject;
}
9 changes: 9 additions & 0 deletions packages/core/src/generators/import-projects/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"id": "@nx-dotnet/core:import-projects",
"title": "Import Projects",
"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.",
"type": "object",
"properties": {}
}
Loading

0 comments on commit 8be8446

Please sign in to comment.