Skip to content

Commit

Permalink
feat(core): update prompt for installed template to present available…
Browse files Browse the repository at this point in the history
… options (#436)
  • Loading branch information
AgentEnder authored May 17, 2022
1 parent 51c74ff commit 5a941ae
Show file tree
Hide file tree
Showing 20 changed files with 308 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/core/generators/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Generate a dotnet project under the application directory.

- (string): A directory where the project is placed

### <span className="required">template</span>
### template

- (string): The template to instantiate when the command is invoked. Each template might have specific options you can pass.

Expand Down
2 changes: 1 addition & 1 deletion docs/core/generators/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Generate a dotnet project under the library directory.

- (string): A directory where the project is placed

### <span className="required">template</span>
### template

- (string): The template to instantiate when the command is invoked. Each template might have specific options you can pass.

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@types/node": "17.0.18",
"@types/react": "17",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"@types/tmp": "^0.2.3",
"@types/yargs-parser": "^20.2.1",
"@typescript-eslint/eslint-plugin": "5.12.1",
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/generators/app/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
},
"template": {
"type": "string",
"description": "The template to instantiate when the command is invoked. Each template might have specific options you can pass.",
"x-prompt": "What template should the project be initialized with? (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new#template-options)"
"description": "The template to instantiate when the command is invoked. Each template might have specific options you can pass."
},
"language": {
"type": "string",
Expand Down Expand Up @@ -80,5 +79,5 @@
"aliases": ["solution", "s"]
}
},
"required": ["name", "template", "language"]
"required": ["name", "language"]
}
4 changes: 2 additions & 2 deletions packages/core/src/generators/init/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@nrwl/devkit';

import { DotNetClient, dotnetFactory } from '@nx-dotnet/dotnet';
import { CONFIG_FILE_PATH, NxDotnetConfig } from '@nx-dotnet/utils';
import { CONFIG_FILE_PATH, isDryRun, NxDotnetConfig } from '@nx-dotnet/utils';

export async function initGenerator(
host: Tree,
Expand Down Expand Up @@ -73,7 +73,7 @@ function updateNxJson(host: Tree) {

function initToolManifest(host: Tree, dotnetClient: DotNetClient) {
const initialized = host.exists('.config/dotnet-tools.json');
if (!initialized) {
if (!initialized && !isDryRun()) {
console.log('Tool Manifest created for managing local .NET tools');
dotnetClient.new('tool-manifest');
}
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/generators/lib/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
},
"template": {
"type": "string",
"description": "The template to instantiate when the command is invoked. Each template might have specific options you can pass.",
"x-prompt": "What template should the project be initialized with? (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new#template-options)"
"description": "The template to instantiate when the command is invoked. Each template might have specific options you can pass."
},
"language": {
"type": "string",
Expand Down Expand Up @@ -74,5 +73,5 @@
"aliases": ["solution", "s"]
}
},
"required": ["name", "template", "language", "testTemplate"]
"required": ["name", "language", "testTemplate"]
}
7 changes: 5 additions & 2 deletions packages/core/src/generators/test/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { normalizeOptions } from '../utils/generate-project';
import { GenerateTestProject } from '../utils/generate-test-project';
import { NxDotnetGeneratorSchema } from './schema';

export default function (
export default async function (
host: Tree,
options: NxDotnetGeneratorSchema,
dotnetClient = new DotNetClient(dotnetFactory()),
Expand All @@ -34,7 +34,10 @@ export default function (
projectType: project.projectType ?? 'library',
};

const normalizedOptions = normalizeOptions(host, projectGeneratorOptions);
const normalizedOptions = await normalizeOptions(
host,
projectGeneratorOptions,
);

return GenerateTestProject(host, normalizedOptions, dotnetClient);
}
9 changes: 9 additions & 0 deletions packages/core/src/generators/utils/generate-project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ describe('nx-dotnet project generator', () => {
standalone: false,
projectType: 'application',
};

jest.spyOn(dotnetClient, 'listInstalledTemplates').mockReturnValue([
{
shortNames: ['classlib'],
templateName: 'Class Library',
languages: ['C#'],
tags: [],
},
]);
});

afterEach(async () => {
Expand Down
32 changes: 24 additions & 8 deletions packages/core/src/generators/utils/generate-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import { readFileSync, writeFileSync } from 'fs';
import { dirname, relative } from 'path';
import { XmlDocument } from 'xmldoc';

import { DotNetClient, dotnetNewOptions } from '@nx-dotnet/dotnet';
import {
DotNetClient,
dotnetNewOptions,
KnownDotnetTemplates,
} from '@nx-dotnet/dotnet';
import { findProjectFileInPath, isDryRun, resolve } from '@nx-dotnet/utils';

import {
Expand All @@ -29,24 +33,27 @@ import {
import initSchematic from '../init/generator';
import { GenerateTestProject } from './generate-test-project';
import { addToSolutionFile } from './add-to-sln';
import { promptForTemplate } from './prompt-for-template';

export interface NormalizedSchema extends NxDotnetProjectGeneratorSchema {
export interface NormalizedSchema
extends Omit<NxDotnetProjectGeneratorSchema, 'template'> {
projectName: string;
projectRoot: string;
projectDirectory: string;
projectLanguage: string;
projectTemplate: string;
projectTemplate: KnownDotnetTemplates;
parsedTags: string[];
className: string;
namespaceName: string;
projectType?: ProjectType;
}

export function normalizeOptions(
export async function normalizeOptions(
host: Tree,
options: NxDotnetProjectGeneratorSchema,
client?: DotNetClient,
projectType?: ProjectType,
): NormalizedSchema {
): Promise<NormalizedSchema> {
const name = names(options.name).fileName;
const className = names(options.name).className;
const projectDirectory = options.directory
Expand All @@ -62,6 +69,10 @@ export function normalizeOptions(
? options.tags.split(',').map((s) => s.trim())
: [];

const template = client
? await promptForTemplate(client, options.template, options.language)
: options.template;

const npmScope = names(
readWorkspaceConfiguration(host).npmScope || '',
).className;
Expand All @@ -81,7 +92,7 @@ export function normalizeOptions(
projectDirectory,
parsedTags,
projectLanguage: options.language,
projectTemplate: options.template,
projectTemplate: template as KnownDotnetTemplates,
namespaceName,
projectType: projectType ?? options.projectType ?? 'library',
};
Expand Down Expand Up @@ -113,7 +124,12 @@ export async function GenerateProject(

options.testTemplate = options.testTemplate ?? 'none';

const normalizedOptions = normalizeOptions(host, options, projectType);
const normalizedOptions = await normalizeOptions(
host,
options,
dotnetClient,
projectType,
);

const projectConfiguration: ProjectConfiguration = {
root: normalizedOptions.projectRoot,
Expand Down Expand Up @@ -146,7 +162,7 @@ export async function GenerateProject(
newParams['dryRun'] = true;
}

dotnetClient.new(normalizedOptions.template, newParams);
dotnetClient.new(normalizedOptions.projectTemplate, newParams);
if (!isDryRun()) {
addToSolutionFile(
host,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe('nx-dotnet test project generator', () => {

options = {
name: 'domain-existing-app',
template: 'xunit',
testTemplate: 'xunit',
language: 'C#',
skipOutputPathManipulation: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function GenerateTestProject(
dotnetClient: DotNetClient,
) {
if (!('projectRoot' in schema)) {
schema = normalizeOptions(host, schema);
schema = await normalizeOptions(host, schema);
}

const suffix = schema.testProjectNameSuffix || 'test';
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/generators/utils/prompt-for-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DotNetClient } from '@nx-dotnet/dotnet';

import { prompt } from 'inquirer';

export function promptForTemplate(
client: DotNetClient,
search?: string,
language?: string,
): string | Promise<string> {
const available = client.listInstalledTemplates({ search, language });

if (search) {
const exactMatch = available.find((x) => x.shortNames.includes(search));
if (exactMatch) {
return exactMatch.shortNames[0];
}
}

return prompt<{ template: string }>([
{
name: 'template',
type: 'list',
message:
'What template should the project be initialized with? (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new#template-options) ',
choices: available.map((a) => ({
value: a.shortNames[0],
message: a.templateName,
})),
},
]).then((a) => a.template);
}
2 changes: 1 addition & 1 deletion packages/core/src/models/project-generator-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface NxDotnetProjectGeneratorSchema {
name: string;
tags?: string;
directory?: string;
template: string;
template?: string;
language: string;
testTemplate: 'nunit' | 'mstest' | 'xunit' | 'none';
testProjectNameSuffix?: string;
Expand Down
82 changes: 81 additions & 1 deletion packages/dotnet/src/lib/core/dotnet.client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DotNetClient } from './dotnet.client';
import { mockDotnetFactory } from './dotnet.factory';
import { dotnetFactory, mockDotnetFactory } from './dotnet.factory';
import * as cp from 'child_process';

describe('dotnet client', () => {
Expand Down Expand Up @@ -78,4 +78,84 @@ describe('dotnet client', () => {
});
});
});

describe('listInstalledTemplates', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('should list templates', () => {
jest.spyOn(cp, 'spawnSync').mockReturnValue({
status: 0,
stdout:
Buffer.from(`Templates Short Name Language Tags
-------------------------------------------- ---------- -------- -------------------
ASP.NET Core Empty web [C#], F# Web/Empty
ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC
ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
ASP.NET Core Web API webapi [C#], F# Web/WebAPI
ASP.NET Core gRPC Service grpc [C#] Web/gRPC `),
} as Partial<cp.SpawnSyncReturns<Buffer>> as cp.SpawnSyncReturns<Buffer>);

const client = new DotNetClient(dotnetFactory());
const results = client
.listInstalledTemplates()
.flatMap((x) => x.shortNames);
expect(results).toContain('webapi');
expect(results).toContain('mvc');
expect(results).toContain('webapp');
});

it('should filter by search', () => {
jest.spyOn(cp, 'spawnSync').mockReturnValue({
status: 0,
stdout: Buffer.from(`These templates matched your input: 'asp'.
Templates Short Name Language Tags
-------------------------------------------- ---------- -------- -------------------
ASP.NET Core Empty web [C#], F# Web/Empty
ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC
ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
ASP.NET Core Web API webapi [C#], F# Web/WebAPI
ASP.NET Core gRPC Service grpc [C#] Web/gRPC `),
} as Partial<cp.SpawnSyncReturns<Buffer>> as cp.SpawnSyncReturns<Buffer>);

const client = new DotNetClient(dotnetFactory());
const results = client.listInstalledTemplates({ search: 'asp' });
console.log(results);
for (const result of results) {
expect(result.templateName).toMatch(/^asp.*/i);
}
});

it('should filter by language', () => {
jest.spyOn(cp, 'spawnSync').mockReturnValue({
status: 0,
stdout: Buffer.from(`These templates matched your input: language='C#'
Templates Short Name Language Tags
-------------------------------------------- ---------- -------- -------------------
ASP.NET Core Empty web [C#], F# Web/Empty
ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC
ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
ASP.NET Core Web API webapi [C#], F# Web/WebAPI
ASP.NET Core gRPC Service grpc [C#] Web/gRPC `),
} as Partial<cp.SpawnSyncReturns<Buffer>> as cp.SpawnSyncReturns<Buffer>);

const client = new DotNetClient(dotnetFactory());
const results = client.listInstalledTemplates({ language: 'C#' });
for (const result of results) {
expect(result.languages).toContain('C#');
}
});
});
});
Loading

0 comments on commit 5a941ae

Please sign in to comment.