Skip to content

Commit

Permalink
feat(core): add new executor for dotnet-format
Browse files Browse the repository at this point in the history
Add a new executor to lint and format projects using an external tool.

Fixes #13
  • Loading branch information
Ben Callaghan committed May 19, 2021
1 parent e2b1cfc commit 92afd05
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/core/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"implementation": "./src/executors/publish/executor",
"schema": "./src/executors/publish/schema.json",
"description": "publish executor"
},
"format": {
"implementation": "./src/executors/format/executor",
"schema": "./src/executors/format/schema.json",
"description": "Formats and lints a project using the dotnet-format tool"
}
},
"builders": {
Expand Down
104 changes: 104 additions & 0 deletions packages/core/src/executors/format/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ExecutorContext } from '@nrwl/devkit';

import { promises as fs } from 'fs';

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

import executor from './executor';
import { FormatExecutorSchema } from './schema';

const options: FormatExecutorSchema = {
check: true,
verbosity: 'minimal',
};

const root = process.cwd() + '/tmp';

jest.mock('../../../../dotnet/src/lib/core/dotnet.client');

describe('Format Executor', () => {
let context: ExecutorContext;
let dotnetClient: DotNetClient;

beforeEach(() => {
context = {
root: root,
cwd: root,
projectName: 'my-app',
targetName: 'lint',
workspace: {
version: 2,
projects: {
'my-app': {
root: `${root}/apps/my-app`,
sourceRoot: `${root}/apps/my-app`,
targets: {
lint: {
executor: '@nx-dotnet/core:format',
},
},
},
},
},
isVerbose: false,
};
dotnetClient = new DotNetClient(mockDotnetFactory());
});

afterEach(async () => {
await rimraf(root);
});

it('detects no dotnet project', async () => {
expect.assertions(1);
try {
await executor(options, context, dotnetClient);
} catch (e) {
console.log(e.message);
expect(e.message).toMatch(
"Unable to find a build-able project within project's source directory!",
);
}
});

it('detects multiple dotnet projects', async () => {
expect.assertions(1);

try {
const directoryPath = `${root}/apps/my-app`;
await fs.mkdir(directoryPath, { recursive: true });
await Promise.all([
fs.writeFile(`${directoryPath}/1.csproj`, ''),
fs.writeFile(`${directoryPath}/2.csproj`, ''),
]);
} catch (e) {
console.warn(e.message);
}

try {
await executor(options, context, dotnetClient);
} catch (e) {
console.log(e.message);
expect(e.message).toMatch(
"More than one build-able projects are contained within the project's source directory!",
);
}
});

it('calls build when 1 project file is found', async () => {
try {
const directoryPath = `${root}/apps/my-app`;
await fs.mkdir(directoryPath, { recursive: true });
await Promise.all([fs.writeFile(`${directoryPath}/1.csproj`, '')]);
} catch (e) {
console.warn(e.message);
}

const res = await executor(options, context, dotnetClient);
expect(
(dotnetClient as jest.Mocked<DotNetClient>).format,
).toHaveBeenCalled();
expect(res.success).toBeTruthy();
});
});
52 changes: 52 additions & 0 deletions packages/core/src/executors/format/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ExecutorContext } from '@nrwl/devkit';
import {
DotNetClient,
dotnetFactory,
dotnetFormatFlags,
} from '@nx-dotnet/dotnet';
import {
getExecutedProjectConfiguration,
getProjectFileForNxProject,
} from '@nx-dotnet/utils';
import { FormatExecutorSchema } from './schema';

function normalizeOptions(
options: FormatExecutorSchema,
): Record<string, string | boolean | undefined> {
const { diagnostics, include, exclude, check, fix, ...flags } = options;
return {
...flags,
diagnostics: Array.isArray(diagnostics)
? diagnostics.join(' ')
: diagnostics,
include: Array.isArray(include) ? include.join(' ') : include,
exclude: Array.isArray(exclude) ? exclude.join(' ') : exclude,
check: fix ? false : check,
};
}

export default async function runExecutor(
options: FormatExecutorSchema,
context: ExecutorContext,
dotnetClient: DotNetClient = new DotNetClient(dotnetFactory()),
) {
const nxProjectConfiguration = getExecutedProjectConfiguration(context);
const projectFilePath = await getProjectFileForNxProject(
nxProjectConfiguration,
);

const normalized = normalizeOptions(options);

dotnetClient.installTool('dotnet-format');
dotnetClient.format(
projectFilePath,
Object.keys(options).map((x) => ({
flag: x as dotnetFormatFlags,
value: normalized[x],
})),
);

return {
success: true,
};
}
23 changes: 23 additions & 0 deletions packages/core/src/executors/format/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface FormatExecutorSchema {
noRestore?: boolean;
fixWhitespace?: boolean;
fixStyle?: string;
diagnostics?: string | string[];
include?: string[];
exclude?: string[];
check: boolean;
report?: string;
binarylog?: string;
verbosity:
| 'q'
| 'quiet'
| 'm'
| 'minimal'
| 'n'
| 'normal'
| 'd'
| 'detailed'
| 'diag'
| 'diagnostic';
fix?: boolean;
}
92 changes: 92 additions & 0 deletions packages/core/src/executors/format/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "Format executor",
"description": "Formats and lints a project using the dotnet-format tool",
"type": "object",
"properties": {
"noRestore": {
"type": "boolean",
"description": "Doesn't execute an implicit restore before formatting"
},
"fixWhitespace": {
"type": "boolean",
"alias": ["w"],
"description": "Run whitespace formatting. Run by default when not applying fixes."
},
"fixStyle": {
"type": "string",
"alias": ["s"],
"description": "Run code style analyzers and apply fixes."
},
"fixAnalyzers": {
"type": "string",
"alias": ["a"],
"description": "Run 3rd party analyzers and apply fixes."
},
"diagnostics": {
"anyOf": [
{
"type": "string",
"description": "A space separated list of diagnostic ids to use as a filter when fixing code style or 3rd party analyzers."
},
{
"type": "array",
"description": "A list of diagnostic ids to use as a filter when fixing code style or 3rd party analyzers.",
"items": {
"type": "string"
}
}
]
},
"include": {
"type": "array",
"description": "A list of relative file or folder paths to include in formatting. All files are formatted if empty",
"items": {
"type": "string"
}
},
"exclude": {
"type": "array",
"description": "A list of relative file or folder paths to exclude from formatting.",
"items": {
"type": "string"
}
},
"check": {
"type": "boolean",
"description": "Formats files without saving changes to disk. Terminates with a non-zero exit code if any files were formatted.",
"default": true
},
"report": {
"type": "string",
"description": "Accepts a file path, which if provided, will produce a json report in the given directory."
},
"binarylog": {
"type": "string",
"description": "Log all project or solution load information to a binary log file."
},
"verbosity": {
"type": "string",
"description": "Set the verbosity level.",
"enum": [
"q",
"quiet",
"m",
"minimal",
"n",
"normal",
"d",
"detailed",
"diag",
"diagnostic"
],
"default": "minimal"
},
"fix": {
"type": "boolean",
"description": "Formats files and saves changes to disk. Equivalent to setting --check=false."
}
},
"required": []
}
16 changes: 16 additions & 0 deletions packages/dotnet/src/lib/core/dotnet.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
buildKeyMap,
dotnetAddPackageOptions,
dotnetBuildOptions,
dotnetFormatOptions,
dotnetNewOptions,
dotnetPublishOptions,
dotnetRunOptions,
dotnetTemplate,
dotnetTestOptions,
formatKeyMap,
newKeyMap,
publishKeyMap,
testKeyMap,
Expand Down Expand Up @@ -130,6 +132,20 @@ export class DotNetClient {
return this.logAndExecute(cmd);
}

format(project: string, parameters?: dotnetFormatOptions): Buffer {
let cmd = `${this.cliCommand.command} format ${project}`;
if (parameters) {
parameters = swapArrayFieldValueUsingMap(
parameters,
'flag',
formatKeyMap,
);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
}
return this.logAndExecute(cmd);
}

private logAndExecute(cmd: string): Buffer {
console.log(`Executing Command: ${cmd}`);
return execSync(cmd, { stdio: 'inherit' });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type dotnetFormatFlags =
| 'noRestore'
// Deliberately excluding the folder option, as the csproj file is always used as the workspace.
| 'fixWhitespace'
| 'fixStyle'
| 'fixAnalyzers'
| 'diagnostics'
| 'include'
| 'exclude'
| 'check'
| 'report'
| 'binarylog'
| 'verbosity';
// Deliberately excluding the version option, as it doesn't perform any actual formatting.

export const formatKeyMap: Partial<{ [key in dotnetFormatFlags]: string }> = {
noRestore: 'no-restore',
fixWhitespace: 'fix-whitespace',
fixStyle: 'fix-style',
fixAnalyzers: 'fix-analyzers',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { dotnetFormatFlags } from './dotnet-format-flags';

export type dotnetFormatOptions = {
flag: dotnetFormatFlags;
value?: string | boolean;
}[];
2 changes: 2 additions & 0 deletions packages/dotnet/src/lib/models/dotnet-format/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './dotnet-format-flags';
export * from './dotnet-format-options';
1 change: 1 addition & 0 deletions packages/dotnet/src/lib/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './dotnet-run';
export * from './dotnet-test';
export * from './dotnet-add-package';
export * from './dotnet-publish';
export * from './dotnet-format';

0 comments on commit 92afd05

Please sign in to comment.