Skip to content

Commit

Permalink
feat(core): support for nx incremental builds
Browse files Browse the repository at this point in the history
Signed-off-by: AgentEnder <craigorycoppola@gmail.com>
  • Loading branch information
AgentEnder committed Dec 14, 2021
1 parent ed5aed0 commit 6739a6b
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 49 deletions.
4 changes: 2 additions & 2 deletions docs/core/guides/handling-solutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ To add projects to a solution file by default, you can set the generator default
},
// ... other default configurations
'@nx-dotnet/core:library': {
solution: 'my-sln.sln',
solutionFile: 'my-sln.sln',
},
},
}
```

> Note that the generator names in `nx.json` must be the full name. Alias's like `app`, `lib` and so on will not be recognized.
> Note that the generator names in `nx.json` must be the full name. Alias's like `app`, `lib` and so on will not be recognized. Aliases that work on the command line for options, like --solution, are also not supported currently.
## Subgraph Solutions

Expand Down
68 changes: 37 additions & 31 deletions e2e/core-e2e/tests/nx-dotnet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,7 @@ describe('nx-dotnet e2e', () => {
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --dry-run`,
);

let exists = true;
try {
checkFilesExist(`apps/${app}`);
} catch (ex) {
exists = false;
}

expect(exists).toBeFalsy();
expect(() => checkFilesExist(`apps/${app}`)).toThrow();
});

it('should generate an app', async () => {
Expand All @@ -96,14 +89,41 @@ describe('nx-dotnet e2e', () => {
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi"`,
);

let exists = true;
try {
checkFilesExist(`apps/${app}`);
} catch (ex) {
exists = false;
}
expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
});

it('should build and test an app', async () => {
const app = uniq('app');
const testProj = `${app}-test`;
await runNxCommandAsync(
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi"`,
);

expect(exists).toBeTruthy();
await runNxCommandAsync(`build ${app}`);
await runNxCommandAsync(`test ${testProj}`);

expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
});

it('should build an app which depends on a lib', async () => {
const app = uniq('app');
const lib = uniq('lib');
await runNxCommandAsync(
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi"`,
);
await runNxCommandAsync(
`generate @nx-dotnet/core:lib ${lib} --language="C#" --template="classlib"`,
);
await runNxCommandAsync(
`generate @nx-dotnet/core:project-reference --project ${app} --reference ${lib}`,
);

await runNxCommandAsync(`build ${app}`);

expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
expect(() => checkFilesExist(`dist/libs/${lib}`)).not.toThrow();
});

it('should update output paths', async () => {
Expand Down Expand Up @@ -187,14 +207,7 @@ describe('nx-dotnet e2e', () => {
`generate @nx-dotnet/core:lib ${lib} --language="C#" --template="webapi" --dry-run`,
);

let exists = true;
try {
checkFilesExist(`libs/${lib}`);
} catch (ex) {
exists = false;
}

expect(exists).toBeFalsy();
expect(() => checkFilesExist(`libs/${lib}`)).toThrow();
});

it('should generate an lib', async () => {
Expand All @@ -203,14 +216,7 @@ describe('nx-dotnet e2e', () => {
`generate @nx-dotnet/core:lib ${lib} --language="C#" --template="webapi"`,
);

let exists = true;
try {
checkFilesExist(`libs/${lib}`);
} catch (ex) {
exists = false;
}

expect(exists).toBeTruthy();
expect(() => checkFilesExist(`libs/${lib}`)).not.toThrow();
});
});

Expand Down
6 changes: 6 additions & 0 deletions packages/core/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
"description": "1.2.0-add-module-boundaries-check",
"cli": "nx",
"implementation": "./src/migrations/1.2.0/add-module-boundaries-check/migrate"
},
"update-1.8.0-beta.0": {
"version": "1.8.0-beta.0",
"description": "update-1.8.0-beta.0",
"cli": "nx",
"implementation": "./src/migrations/update-1.8.0-beta.0/update-1.8.0-beta.0"
}
}
}
4 changes: 1 addition & 3 deletions packages/core/src/executors/build/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { ExecutorContext } from '@nrwl/devkit';

import * as fs from 'fs';

import { DotNetClient, mockDotnetFactory } from '@nx-dotnet/dotnet';
import { assertErrorMessage } from '@nx-dotnet/utils/testing';

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

jest.mock('@nx-dotnet/utils', () => ({
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/generators/import-projects/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
getProjects,
getWorkspaceLayout,
names,
NxJsonProjectConfiguration,
ProjectConfiguration,
TargetConfiguration,
Tree,
Expand Down Expand Up @@ -53,10 +52,9 @@ async function addNewDotnetProject(
const projectName = rootNamespace
? names(rootNamespace).fileName.replace(/\./g, '-')
: names(basename(projectRoot)).fileName;
const configuration: ProjectConfiguration &
NxJsonProjectConfiguration & {
targets: Record<string, TargetConfiguration>;
} = {
const configuration: ProjectConfiguration & {
targets: Record<string, TargetConfiguration>;
} = {
root: projectRoot,
targets: {
build: GetBuildExecutorConfiguration(projectRoot),
Expand Down
121 changes: 121 additions & 0 deletions packages/core/src/migrations/1.8.0/remove-output-option.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
addProjectConfiguration,
readProjectConfiguration,
stripIndents,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import update from './remove-output-option';

import * as utils from '@nx-dotnet/utils';
import { NXDOTNET_TAG } from '@nx-dotnet/utils';

jest.mock('@nx-dotnet/utils', () => ({
...(jest.requireActual('@nx-dotnet/utils') as typeof utils),
getProjectFileForNxProject: () =>
Promise.resolve('apps/my-app/my-app.csproj'),
}));

describe('remove-output-option', () => {
let tree: Tree;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace(2);
});

it('should not update projects where output != OutputPath', async () => {
tree.write(
'/apps/my-app/my-app.csproj',
stripIndents`<Root>
<PropertyGroup>
<OutputPath>./dist/apps/my-app</OutputPath>
</PropertyGroup>
</Root>`,
);

addProjectConfiguration(tree, 'my-app', {
root: 'apps/my-app',
targets: {
build: {
executor: '@nx-dotnet/core:build',
options: {
output: 'dist/apps/my-app',
},
},
},
tags: [NXDOTNET_TAG],
});

await expect(update(tree)).resolves.not.toThrow();

const projectConfiguration = readProjectConfiguration(tree, 'my-app');
expect(projectConfiguration.targets?.build?.options?.output).toEqual(
'dist/apps/my-app',
);
});

it('should update projects where output == OutputPath', async () => {
tree.write(
'/apps/my-app/my-app.csproj',
stripIndents`<Root>
<PropertyGroup>
<OutputPath>../../dist/apps/my-app</OutputPath>
</PropertyGroup>
</Root>`,
);

addProjectConfiguration(tree, 'my-app', {
root: 'apps/my-app',
targets: {
build: {
executor: '@nx-dotnet/core:build',
outputs: ['{options.output}'],
options: {
output: 'dist/apps/my-app',
},
},
},
tags: [NXDOTNET_TAG],
});

await expect(update(tree)).resolves.not.toThrow();

const projectConfiguration = readProjectConfiguration(tree, 'my-app');
expect(
projectConfiguration.targets?.build?.options?.output,
).toBeUndefined();
expect(projectConfiguration.targets?.build?.outputs).toEqual([
'dist/apps/my-app',
]);
});

it('should not update projects where OutputPath is not set', async () => {
tree.write(
'/apps/my-app/my-app.csproj',
stripIndents`<Root>
<PropertyGroup>
</PropertyGroup>
</Root>`,
);

addProjectConfiguration(tree, 'my-app', {
root: 'apps/my-app',
targets: {
build: {
executor: '@nx-dotnet/core:build',
options: {
output: 'dist/apps/my-app',
},
},
},
tags: [NXDOTNET_TAG],
});

await expect(update(tree)).resolves.not.toThrow();

const projectConfiguration = readProjectConfiguration(tree, 'my-app');
expect(projectConfiguration.targets?.build?.options?.output).toEqual(
'dist/apps/my-app',
);
});
});
79 changes: 79 additions & 0 deletions packages/core/src/migrations/1.8.0/remove-output-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
joinPathFragments,
logger,
normalizePath,
TargetConfiguration,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
import {
getNxDotnetProjects,
getProjectFileForNxProject,
} from '@nx-dotnet/utils';
import { basename, relative, resolve } from 'path';
import { XmlDocument } from 'xmldoc';
import { BuildExecutorConfiguration } from '../../models';

export default async function update(host: Tree) {
const projects = getNxDotnetProjects(host);
for (const [name, projectConfiguration] of projects.entries()) {
const projectFile = await getProjectFileForNxProject(projectConfiguration);
if (projectFile) {
const xml = new XmlDocument((host.read(projectFile) ?? '').toString());
const outputPath = xml
.childNamed('PropertyGroup')
?.childNamed('OutputPath');

if (!outputPath) {
logger.warn(
`Skipping ${name} because it does not have OutputPath set in ${basename(
projectFile,
)}`,
);
continue;
}

let xmlOutputPath = outputPath.val;
xmlOutputPath = normalizePath(
resolve(host.root, projectConfiguration.root, xmlOutputPath),
);

const buildTarget = Object.entries(
(projectConfiguration.targets ??= {}),
).find(
([, configuration]) =>
configuration.executor === '@nx-dotnet/core:build',
);

if (buildTarget) {
const [target, { options }] = buildTarget as [
string,
BuildExecutorConfiguration,
];
const outputPath = normalizePath(resolve(host.root, options.output));
if (outputPath !== xmlOutputPath) {
logger.info(
`Skipping ${name} since .csproj OutputPath is set differently from --output parameter`,
);
logger.info(`- .csproj OutputPath: ${xmlOutputPath}`);
logger.info(`- project.json output: ${outputPath}`);
continue;
} else {
const t: BuildExecutorConfiguration = projectConfiguration.targets[
target
] as BuildExecutorConfiguration;
const output = t.options.output;
const outputs = t.outputs || [];
delete t.options.output;
t.options['noIncremental'] = 'true';
projectConfiguration.targets[target].outputs = outputs.filter(
(x) => x !== '{options.output}',
);
(projectConfiguration.targets[target].outputs || []).push(output);
}
updateProjectConfiguration(host, name, projectConfiguration);
}
}
}
}
15 changes: 7 additions & 8 deletions packages/core/src/models/build-executor-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TargetConfiguration } from '@nrwl/devkit';
import { BuildExecutorSchema } from '../executors/build/schema';

/**
* Returns a TargetConfiguration for the nx-dotnet/core:build executor
Expand All @@ -10,10 +11,10 @@ export function GetBuildExecutorConfiguration(

return {
executor: '@nx-dotnet/core:build',
outputs: ['{options.output}'],
outputs: [outputDirectory],
options: {
output: outputDirectory,
configuration: 'Debug',
noDependencies: 'true',
},
configurations: {
production: {
Expand All @@ -26,9 +27,7 @@ export function GetBuildExecutorConfiguration(
/**
* Configuration options relevant for the build executor
*/
export interface BuildExecutorConfiguration extends TargetConfiguration {
options: {
output: string;
configuration: 'Debug' | 'Release';
};
}
export type BuildExecutorConfiguration = TargetConfiguration & {
executor: '@nx-dotnet/core:build';
options: BuildExecutorSchema;
};

0 comments on commit 6739a6b

Please sign in to comment.