Skip to content

Commit 6739a6b

Browse files
committed
feat(core): support for nx incremental builds
Signed-off-by: AgentEnder <craigorycoppola@gmail.com>
1 parent ed5aed0 commit 6739a6b

File tree

8 files changed

+256
-49
lines changed

8 files changed

+256
-49
lines changed

docs/core/guides/handling-solutions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ To add projects to a solution file by default, you can set the generator default
2020
},
2121
// ... other default configurations
2222
'@nx-dotnet/core:library': {
23-
solution: 'my-sln.sln',
23+
solutionFile: 'my-sln.sln',
2424
},
2525
},
2626
}
2727
```
2828

29-
> 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.
29+
> 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.
3030
3131
## Subgraph Solutions
3232

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

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,7 @@ describe('nx-dotnet e2e', () => {
8080
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --dry-run`,
8181
);
8282

83-
let exists = true;
84-
try {
85-
checkFilesExist(`apps/${app}`);
86-
} catch (ex) {
87-
exists = false;
88-
}
89-
90-
expect(exists).toBeFalsy();
83+
expect(() => checkFilesExist(`apps/${app}`)).toThrow();
9184
});
9285

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

99-
let exists = true;
100-
try {
101-
checkFilesExist(`apps/${app}`);
102-
} catch (ex) {
103-
exists = false;
104-
}
92+
expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
93+
});
94+
95+
it('should build and test an app', async () => {
96+
const app = uniq('app');
97+
const testProj = `${app}-test`;
98+
await runNxCommandAsync(
99+
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi"`,
100+
);
105101

106-
expect(exists).toBeTruthy();
102+
await runNxCommandAsync(`build ${app}`);
103+
await runNxCommandAsync(`test ${testProj}`);
104+
105+
expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
106+
expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
107+
});
108+
109+
it('should build an app which depends on a lib', async () => {
110+
const app = uniq('app');
111+
const lib = uniq('lib');
112+
await runNxCommandAsync(
113+
`generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi"`,
114+
);
115+
await runNxCommandAsync(
116+
`generate @nx-dotnet/core:lib ${lib} --language="C#" --template="classlib"`,
117+
);
118+
await runNxCommandAsync(
119+
`generate @nx-dotnet/core:project-reference --project ${app} --reference ${lib}`,
120+
);
121+
122+
await runNxCommandAsync(`build ${app}`);
123+
124+
expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
125+
expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
126+
expect(() => checkFilesExist(`dist/libs/${lib}`)).not.toThrow();
107127
});
108128

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

190-
let exists = true;
191-
try {
192-
checkFilesExist(`libs/${lib}`);
193-
} catch (ex) {
194-
exists = false;
195-
}
196-
197-
expect(exists).toBeFalsy();
210+
expect(() => checkFilesExist(`libs/${lib}`)).toThrow();
198211
});
199212

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

206-
let exists = true;
207-
try {
208-
checkFilesExist(`libs/${lib}`);
209-
} catch (ex) {
210-
exists = false;
211-
}
212-
213-
expect(exists).toBeTruthy();
219+
expect(() => checkFilesExist(`libs/${lib}`)).not.toThrow();
214220
});
215221
});
216222

packages/core/migrations.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
"description": "1.2.0-add-module-boundaries-check",
1212
"cli": "nx",
1313
"implementation": "./src/migrations/1.2.0/add-module-boundaries-check/migrate"
14+
},
15+
"update-1.8.0-beta.0": {
16+
"version": "1.8.0-beta.0",
17+
"description": "update-1.8.0-beta.0",
18+
"cli": "nx",
19+
"implementation": "./src/migrations/update-1.8.0-beta.0/update-1.8.0-beta.0"
1420
}
1521
}
1622
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { ExecutorContext } from '@nrwl/devkit';
22

3-
import * as fs from 'fs';
4-
53
import { DotNetClient, mockDotnetFactory } from '@nx-dotnet/dotnet';
6-
import { assertErrorMessage } from '@nx-dotnet/utils/testing';
4+
75
import * as utils from '@nx-dotnet/utils';
86

97
jest.mock('@nx-dotnet/utils', () => ({

packages/core/src/generators/import-projects/generator.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
getProjects,
55
getWorkspaceLayout,
66
names,
7-
NxJsonProjectConfiguration,
87
ProjectConfiguration,
98
TargetConfiguration,
109
Tree,
@@ -53,10 +52,9 @@ async function addNewDotnetProject(
5352
const projectName = rootNamespace
5453
? names(rootNamespace).fileName.replace(/\./g, '-')
5554
: names(basename(projectRoot)).fileName;
56-
const configuration: ProjectConfiguration &
57-
NxJsonProjectConfiguration & {
58-
targets: Record<string, TargetConfiguration>;
59-
} = {
55+
const configuration: ProjectConfiguration & {
56+
targets: Record<string, TargetConfiguration>;
57+
} = {
6058
root: projectRoot,
6159
targets: {
6260
build: GetBuildExecutorConfiguration(projectRoot),
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
addProjectConfiguration,
3+
readProjectConfiguration,
4+
stripIndents,
5+
Tree,
6+
} from '@nrwl/devkit';
7+
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
8+
import update from './remove-output-option';
9+
10+
import * as utils from '@nx-dotnet/utils';
11+
import { NXDOTNET_TAG } from '@nx-dotnet/utils';
12+
13+
jest.mock('@nx-dotnet/utils', () => ({
14+
...(jest.requireActual('@nx-dotnet/utils') as typeof utils),
15+
getProjectFileForNxProject: () =>
16+
Promise.resolve('apps/my-app/my-app.csproj'),
17+
}));
18+
19+
describe('remove-output-option', () => {
20+
let tree: Tree;
21+
22+
beforeEach(() => {
23+
tree = createTreeWithEmptyWorkspace(2);
24+
});
25+
26+
it('should not update projects where output != OutputPath', async () => {
27+
tree.write(
28+
'/apps/my-app/my-app.csproj',
29+
stripIndents`<Root>
30+
<PropertyGroup>
31+
<OutputPath>./dist/apps/my-app</OutputPath>
32+
</PropertyGroup>
33+
</Root>`,
34+
);
35+
36+
addProjectConfiguration(tree, 'my-app', {
37+
root: 'apps/my-app',
38+
targets: {
39+
build: {
40+
executor: '@nx-dotnet/core:build',
41+
options: {
42+
output: 'dist/apps/my-app',
43+
},
44+
},
45+
},
46+
tags: [NXDOTNET_TAG],
47+
});
48+
49+
await expect(update(tree)).resolves.not.toThrow();
50+
51+
const projectConfiguration = readProjectConfiguration(tree, 'my-app');
52+
expect(projectConfiguration.targets?.build?.options?.output).toEqual(
53+
'dist/apps/my-app',
54+
);
55+
});
56+
57+
it('should update projects where output == OutputPath', async () => {
58+
tree.write(
59+
'/apps/my-app/my-app.csproj',
60+
stripIndents`<Root>
61+
<PropertyGroup>
62+
<OutputPath>../../dist/apps/my-app</OutputPath>
63+
</PropertyGroup>
64+
</Root>`,
65+
);
66+
67+
addProjectConfiguration(tree, 'my-app', {
68+
root: 'apps/my-app',
69+
targets: {
70+
build: {
71+
executor: '@nx-dotnet/core:build',
72+
outputs: ['{options.output}'],
73+
options: {
74+
output: 'dist/apps/my-app',
75+
},
76+
},
77+
},
78+
tags: [NXDOTNET_TAG],
79+
});
80+
81+
await expect(update(tree)).resolves.not.toThrow();
82+
83+
const projectConfiguration = readProjectConfiguration(tree, 'my-app');
84+
expect(
85+
projectConfiguration.targets?.build?.options?.output,
86+
).toBeUndefined();
87+
expect(projectConfiguration.targets?.build?.outputs).toEqual([
88+
'dist/apps/my-app',
89+
]);
90+
});
91+
92+
it('should not update projects where OutputPath is not set', async () => {
93+
tree.write(
94+
'/apps/my-app/my-app.csproj',
95+
stripIndents`<Root>
96+
<PropertyGroup>
97+
</PropertyGroup>
98+
</Root>`,
99+
);
100+
101+
addProjectConfiguration(tree, 'my-app', {
102+
root: 'apps/my-app',
103+
targets: {
104+
build: {
105+
executor: '@nx-dotnet/core:build',
106+
options: {
107+
output: 'dist/apps/my-app',
108+
},
109+
},
110+
},
111+
tags: [NXDOTNET_TAG],
112+
});
113+
114+
await expect(update(tree)).resolves.not.toThrow();
115+
116+
const projectConfiguration = readProjectConfiguration(tree, 'my-app');
117+
expect(projectConfiguration.targets?.build?.options?.output).toEqual(
118+
'dist/apps/my-app',
119+
);
120+
});
121+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import {
3+
joinPathFragments,
4+
logger,
5+
normalizePath,
6+
TargetConfiguration,
7+
Tree,
8+
updateProjectConfiguration,
9+
} from '@nrwl/devkit';
10+
import {
11+
getNxDotnetProjects,
12+
getProjectFileForNxProject,
13+
} from '@nx-dotnet/utils';
14+
import { basename, relative, resolve } from 'path';
15+
import { XmlDocument } from 'xmldoc';
16+
import { BuildExecutorConfiguration } from '../../models';
17+
18+
export default async function update(host: Tree) {
19+
const projects = getNxDotnetProjects(host);
20+
for (const [name, projectConfiguration] of projects.entries()) {
21+
const projectFile = await getProjectFileForNxProject(projectConfiguration);
22+
if (projectFile) {
23+
const xml = new XmlDocument((host.read(projectFile) ?? '').toString());
24+
const outputPath = xml
25+
.childNamed('PropertyGroup')
26+
?.childNamed('OutputPath');
27+
28+
if (!outputPath) {
29+
logger.warn(
30+
`Skipping ${name} because it does not have OutputPath set in ${basename(
31+
projectFile,
32+
)}`,
33+
);
34+
continue;
35+
}
36+
37+
let xmlOutputPath = outputPath.val;
38+
xmlOutputPath = normalizePath(
39+
resolve(host.root, projectConfiguration.root, xmlOutputPath),
40+
);
41+
42+
const buildTarget = Object.entries(
43+
(projectConfiguration.targets ??= {}),
44+
).find(
45+
([, configuration]) =>
46+
configuration.executor === '@nx-dotnet/core:build',
47+
);
48+
49+
if (buildTarget) {
50+
const [target, { options }] = buildTarget as [
51+
string,
52+
BuildExecutorConfiguration,
53+
];
54+
const outputPath = normalizePath(resolve(host.root, options.output));
55+
if (outputPath !== xmlOutputPath) {
56+
logger.info(
57+
`Skipping ${name} since .csproj OutputPath is set differently from --output parameter`,
58+
);
59+
logger.info(`- .csproj OutputPath: ${xmlOutputPath}`);
60+
logger.info(`- project.json output: ${outputPath}`);
61+
continue;
62+
} else {
63+
const t: BuildExecutorConfiguration = projectConfiguration.targets[
64+
target
65+
] as BuildExecutorConfiguration;
66+
const output = t.options.output;
67+
const outputs = t.outputs || [];
68+
delete t.options.output;
69+
t.options['noIncremental'] = 'true';
70+
projectConfiguration.targets[target].outputs = outputs.filter(
71+
(x) => x !== '{options.output}',
72+
);
73+
(projectConfiguration.targets[target].outputs || []).push(output);
74+
}
75+
updateProjectConfiguration(host, name, projectConfiguration);
76+
}
77+
}
78+
}
79+
}

packages/core/src/models/build-executor-configuration.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TargetConfiguration } from '@nrwl/devkit';
2+
import { BuildExecutorSchema } from '../executors/build/schema';
23

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

1112
return {
1213
executor: '@nx-dotnet/core:build',
13-
outputs: ['{options.output}'],
14+
outputs: [outputDirectory],
1415
options: {
15-
output: outputDirectory,
1616
configuration: 'Debug',
17+
noDependencies: 'true',
1718
},
1819
configurations: {
1920
production: {
@@ -26,9 +27,7 @@ export function GetBuildExecutorConfiguration(
2627
/**
2728
* Configuration options relevant for the build executor
2829
*/
29-
export interface BuildExecutorConfiguration extends TargetConfiguration {
30-
options: {
31-
output: string;
32-
configuration: 'Debug' | 'Release';
33-
};
34-
}
30+
export type BuildExecutorConfiguration = TargetConfiguration & {
31+
executor: '@nx-dotnet/core:build';
32+
options: BuildExecutorSchema;
33+
};

0 commit comments

Comments
 (0)