Skip to content

Commit 3ad6291

Browse files
bcallaghan-etAgentEnderBen Callaghan
authored
fix(core): Check SDK and tool installation before running format command (#204)
Closes #179 Closes #202 Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com> Co-authored-by: Ben Callaghan <bcallaghan@selectbankcard.com>
1 parent 76c9f2b commit 3ad6291

File tree

3 files changed

+147
-6
lines changed

3 files changed

+147
-6
lines changed

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ describe('Format Executor', () => {
4545
isVerbose: false,
4646
};
4747
dotnetClient = new DotNetClient(mockDotnetFactory());
48+
(dotnetClient as jest.Mocked<DotNetClient>).printSdkVersion.mockReturnValue(
49+
Buffer.from('5.0.402'),
50+
);
4851
});
4952

5053
afterEach(async () => {
@@ -91,4 +94,111 @@ describe('Format Executor', () => {
9194
).toHaveBeenCalled();
9295
expect(res.success).toBeTruthy();
9396
});
97+
98+
it('installs dotnet-format if not already installed', async () => {
99+
try {
100+
const directoryPath = `${root}/apps/my-app`;
101+
await fs.mkdir(directoryPath, { recursive: true });
102+
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
103+
104+
const manifestPath = `${root}/.config`;
105+
await fs.mkdir(manifestPath, { recursive: true });
106+
await fs.writeFile(`${manifestPath}/dotnet-tools.json`, '{"tools": {}}');
107+
} catch (e) {
108+
if (assertErrorMessage(e)) console.warn(e.message);
109+
}
110+
111+
const res = await executor(options, context, dotnetClient);
112+
expect(
113+
(dotnetClient as jest.Mocked<DotNetClient>).installTool,
114+
).toHaveBeenCalled();
115+
expect(res.success).toBeTruthy();
116+
});
117+
118+
it('does not install dotnet-format if already installed', async () => {
119+
try {
120+
const directoryPath = `${root}/apps/my-app`;
121+
await fs.mkdir(directoryPath, { recursive: true });
122+
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
123+
124+
const manifestPath = `${root}/.config`;
125+
await fs.mkdir(manifestPath, { recursive: true });
126+
await fs.writeFile(
127+
`${manifestPath}/dotnet-tools.json`,
128+
'{"tools": {"dotnet-format": {"version": "5.1.250801"}}}',
129+
);
130+
} catch (e) {
131+
if (assertErrorMessage(e)) console.warn(e.message);
132+
}
133+
134+
const res = await executor(options, context, dotnetClient);
135+
expect(
136+
(dotnetClient as jest.Mocked<DotNetClient>).installTool,
137+
).not.toHaveBeenCalled();
138+
expect(res.success).toBeTruthy();
139+
});
140+
141+
it('does not install dotnet-format if SDK is 6+', async () => {
142+
(dotnetClient as jest.Mocked<DotNetClient>).printSdkVersion.mockReturnValue(
143+
Buffer.from('6.0.101'),
144+
);
145+
146+
try {
147+
const directoryPath = `${root}/apps/my-app`;
148+
await fs.mkdir(directoryPath, { recursive: true });
149+
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
150+
151+
const manifestPath = `${root}/.config`;
152+
await fs.mkdir(manifestPath, { recursive: true });
153+
await fs.writeFile(`${manifestPath}/dotnet-tools.json`, '{"tools": {}}');
154+
} catch (e) {
155+
if (assertErrorMessage(e)) console.warn(e.message);
156+
}
157+
158+
const res = await executor(options, context, dotnetClient);
159+
expect(
160+
(dotnetClient as jest.Mocked<DotNetClient>).installTool,
161+
).not.toHaveBeenCalled();
162+
expect(res.success).toBeTruthy();
163+
});
164+
165+
it('passes the --check option on .NET 5 and earlier', async () => {
166+
try {
167+
const directoryPath = `${root}/apps/my-app`;
168+
await fs.mkdir(directoryPath, { recursive: true });
169+
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
170+
} catch (e) {
171+
if (assertErrorMessage(e)) console.warn(e.message);
172+
}
173+
174+
const res = await executor(options, context, dotnetClient);
175+
expect(res.success).toBeTruthy();
176+
177+
const formatOptions = (dotnetClient as jest.Mocked<DotNetClient>).format
178+
.mock.calls[0][1];
179+
const checkFlag = formatOptions?.find((o) => o.flag == 'check');
180+
expect(checkFlag?.value).toBeTruthy();
181+
});
182+
183+
it('passes the --verify-no-changes option on .NET 6 and later', async () => {
184+
(dotnetClient as jest.Mocked<DotNetClient>).printSdkVersion.mockReturnValue(
185+
Buffer.from('6.0.101'),
186+
);
187+
188+
try {
189+
const directoryPath = `${root}/apps/my-app`;
190+
await fs.mkdir(directoryPath, { recursive: true });
191+
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
192+
} catch (e) {
193+
if (assertErrorMessage(e)) console.warn(e.message);
194+
}
195+
196+
const res = await executor(options, context, dotnetClient);
197+
expect(res.success).toBeTruthy();
198+
199+
const formatOptions = (dotnetClient as jest.Mocked<DotNetClient>).format
200+
.mock.calls[0][1];
201+
const checkFlag = formatOptions?.find((o) => o.flag == 'verifyNoChanges');
202+
expect(checkFlag?.value).toBeTruthy();
203+
});
94204
});

packages/core/src/executors/format/executor.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ExecutorContext } from '@nrwl/devkit';
1+
import { ExecutorContext, readJsonFile } from '@nrwl/devkit';
2+
import { existsSync } from 'fs';
23

34
import {
45
DotNetClient,
@@ -14,6 +15,7 @@ import { FormatExecutorSchema } from './schema';
1415

1516
function normalizeOptions(
1617
options: FormatExecutorSchema,
18+
isNet6OrHigher: boolean,
1719
): Record<string, string | boolean | undefined> {
1820
const { diagnostics, include, exclude, check, fix, ...flags } = options;
1921
return {
@@ -23,7 +25,8 @@ function normalizeOptions(
2325
: diagnostics,
2426
include: Array.isArray(include) ? include.join(' ') : include,
2527
exclude: Array.isArray(exclude) ? exclude.join(' ') : exclude,
26-
check: fix ? false : check,
28+
check: fix ? false : check && !isNet6OrHigher, // The --check flag is for .NET 5 and older
29+
verifyNoChanges: fix ? false : check && isNet6OrHigher, // The --verify-no-changes flag is for .NET 6 and newer
2730
};
2831
}
2932

@@ -32,17 +35,21 @@ export default async function runExecutor(
3235
context: ExecutorContext,
3336
dotnetClient: DotNetClient = new DotNetClient(dotnetFactory()),
3437
) {
38+
const sdkVersion = dotnetClient.printSdkVersion().toString();
39+
const majorVersion = parseInt(sdkVersion.split('.')[0]);
40+
const isNet6OrHigher = majorVersion >= 6;
41+
3542
const nxProjectConfiguration = getExecutedProjectConfiguration(context);
3643
const projectFilePath = await getProjectFileForNxProject(
3744
nxProjectConfiguration,
3845
);
3946

40-
const normalized = normalizeOptions(options);
47+
const normalized = normalizeOptions(options, isNet6OrHigher);
4148

42-
dotnetClient.installTool('dotnet-format');
49+
ensureFormatToolInstalled(context, dotnetClient, isNet6OrHigher);
4350
dotnetClient.format(
4451
projectFilePath,
45-
Object.keys(options).map((x) => ({
52+
Object.keys(normalized).map((x) => ({
4653
flag: x as dotnetFormatFlags,
4754
value: normalized[x],
4855
})),
@@ -52,3 +59,25 @@ export default async function runExecutor(
5259
success: true,
5360
};
5461
}
62+
63+
function ensureFormatToolInstalled(
64+
context: ExecutorContext,
65+
dotnetClient: DotNetClient,
66+
isNet6OrHigher: boolean,
67+
) {
68+
if (isNet6OrHigher) {
69+
// dotnet-format is already included as part of .NET SDK 6+
70+
return;
71+
}
72+
73+
const manifestPath = `${context.cwd}/.config/dotnet-tools.json`;
74+
const manifest = existsSync(manifestPath)
75+
? readJsonFile(manifestPath)
76+
: undefined;
77+
if (manifest?.tools['dotnet-format']) {
78+
// dotnet-format is already installed.
79+
return;
80+
}
81+
82+
dotnetClient.installTool('dotnet-format');
83+
}

packages/dotnet/src/lib/models/dotnet-format/dotnet-format-flags.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ export type dotnetFormatFlags =
1010
| 'check'
1111
| 'report'
1212
| 'binarylog'
13-
| 'verbosity';
13+
| 'verbosity'
14+
| 'verifyNoChanges';
1415
// Deliberately excluding the version option, as it doesn't perform any actual formatting.
1516

1617
export const formatKeyMap: Partial<{ [key in dotnetFormatFlags]: string }> = {
1718
noRestore: 'no-restore',
1819
fixWhitespace: 'fix-whitespace',
1920
fixStyle: 'fix-style',
2021
fixAnalyzers: 'fix-analyzers',
22+
verifyNoChanges: 'verify-no-changes',
2123
};

0 commit comments

Comments
 (0)