import { getSpawnParameterArray, swapKeysUsingMap } from '@nx-dotnet/utils'; import { ChildProcess, spawn, spawnSync } from 'child_process'; import * as semver from 'semver'; import { addPackageKeyMap, buildKeyMap, dotnetAddPackageOptions, dotnetBuildOptions, dotnetFormatOptions, dotnetNewOptions, dotnetPublishOptions, dotnetRunOptions, KnownDotnetTemplates, dotnetTestOptions, formatKeyMap, newKeyMap, publishKeyMap, runKeyMap, testKeyMap, DotnetTemplate, } from '../models'; import { LoadedCLI } from './dotnet.factory'; import { parseDotnetNewListOutput } from '../utils/parse-dotnet-new-list-output'; export class DotNetClient { constructor(private cliCommand: LoadedCLI, public cwd?: string) {} new(template: KnownDotnetTemplates, parameters?: dotnetNewOptions): void { const params = [`new`, template]; if (parameters) { parameters = swapKeysUsingMap(parameters, newKeyMap); params.push(...getSpawnParameterArray(parameters)); } return this.logAndExecute(params); } listInstalledTemplates(opts?: { search?: string; language?: string; }): DotnetTemplate[] { const version = this.getSdkVersion(); const params: string[] = ['new']; if (semver.lt(version, '6.0.100') && opts?.search) { params.push(opts.search); } if (semver.gte(version, '7.0.100')) { params.push('list'); } else { params.push('--list'); } if (semver.gte(version, '6.0.100') && opts?.search) { params.push(opts.search); } if (opts?.language) { params.push('--language', opts.language); } const output = this.spawnAndGetOutput(params); return parseDotnetNewListOutput(output); } build(project: string, parameters?: dotnetBuildOptions): void { const params = [`build`, project]; if (parameters) { parameters = swapKeysUsingMap(parameters, buildKeyMap); params.push(...getSpawnParameterArray(parameters)); } return this.logAndExecute(params); } run( project: string, watch = false, parameters?: dotnetRunOptions, ): ChildProcess { const params = watch ? [`watch`, `--project`, project, `run`] : [`run`, `--project`, project]; if (parameters) { parameters = swapKeysUsingMap(parameters, runKeyMap); params.push(...getSpawnParameterArray(parameters)); } return this.logAndSpawn(params); } test( project: string, watch?: boolean, parameters?: dotnetTestOptions, ): void | ChildProcess { const params = watch ? [`watch`, `--project`, project, `test`] : [`test`, project]; if (parameters) { parameters = swapKeysUsingMap(parameters, testKeyMap); params.push(...getSpawnParameterArray(parameters)); } if (!watch) { return this.logAndExecute(params); } else { return this.logAndSpawn(params); } } addPackageReference( project: string, pkg: string, parameters?: dotnetAddPackageOptions, ): void { const params = [`add`, project, `package`, pkg]; if (parameters) { parameters = swapKeysUsingMap(parameters, addPackageKeyMap); params.push(...getSpawnParameterArray(parameters)); } return this.logAndExecute(params); } addProjectReference(hostCsProj: string, targetCsProj: string): void { return this.logAndExecute([`add`, hostCsProj, `reference`, targetCsProj]); } publish( project: string, parameters?: dotnetPublishOptions, publishProfile?: string, extraParameters?: string, ): void { const params = [`publish`, `"${project}"`]; if (parameters) { parameters = swapKeysUsingMap(parameters, publishKeyMap); params.push(...getSpawnParameterArray(parameters)); } if (publishProfile) { params.push(`-p:PublishProfile=${publishProfile}`); } if (extraParameters) { const matches = extraParameters.match(EXTRA_PARAMS_REGEX); params.push(...(matches as RegExpMatchArray)); } return this.logAndExecute(params); } installTool(tool: string, version?: string, source?: string): void { const cmd = [`tool`, `install`, tool]; if (version) { cmd.push('--version', version); } if (source) { cmd.push('--add-source', source); } return this.logAndExecute(cmd); } restorePackages(project: string): void { const cmd = [`restore`, project]; return this.logAndExecute(cmd); } restoreTools(): void { const cmd = [`tool`, `restore`]; return this.logAndExecute(cmd); } format( project: string, parameters?: dotnetFormatOptions, forceToolUsage?: boolean, ): void { const params = forceToolUsage ? ['tool', 'run', 'dotnet-format', '--', project] : [`format`, project]; if (parameters) { parameters = swapKeysUsingMap(parameters, formatKeyMap); params.push(...getSpawnParameterArray(parameters)); } return this.logAndExecute(params); } runTool<T extends Record<string, string | boolean>>( tool: string, positionalParameters?: string[], parameters?: T, ) { const params = ['tool', 'run', tool]; if (positionalParameters) { params.push(...positionalParameters); } if (parameters) { params.push(...getSpawnParameterArray(parameters)); } return this.logAndExecute(params); } addProjectToSolution(solutionFile: string, project: string) { const params = [`sln`, solutionFile, `add`, project]; this.logAndExecute(params); } getSdkVersion(): string { return this.cliCommand.info.version.toString(); } printSdkVersion(): void { this.logAndExecute(['--version']); } private logAndExecute(params: string[]): void { params = params.map((param) => param.replace(/\$(\w+)/, (_, varName) => process.env[varName] ?? ''), ); const cmd = `${this.cliCommand.command} "${params.join('" "')}"`; console.log(`Executing Command: ${cmd}`); const res = spawnSync(this.cliCommand.command, params, { cwd: this.cwd || process.cwd(), stdio: 'inherit', }); if (res.status !== 0) { throw new Error(`dotnet execution returned status code ${res.status}`); } } private spawnAndGetOutput(params: string[]): string { params = params.map((param) => param.replace(/\$(\w+)/, (_, varName) => process.env[varName] ?? ''), ); const res = spawnSync(this.cliCommand.command, params, { cwd: this.cwd || process.cwd(), stdio: 'pipe', }); if (res.status !== 0) { throw new Error( `dotnet execution returned status code ${res.status} \n ${res.stderr}`, ); } return res.stdout.toString(); } private logAndSpawn(params: string[]): ChildProcess { console.log( `Executing Command: ${this.cliCommand.command} "${params.join('" "')}"`, ); return spawn(this.cliCommand.command, params, { stdio: 'inherit', cwd: this.cwd || process.cwd(), }); } } /** * Regular Expression for Parsing Extra Params before sending to spawn / exec * First part of expression matches parameters such as --flag="my answer" * Second part of expression matches parameters such as --flag=my_answer */ const EXTRA_PARAMS_REGEX = /\S*".+?"|\S+/g;