diff --git a/actions/add.action.ts b/actions/add.action.ts index 5c5d1d09e..693421729 100644 --- a/actions/add.action.ts +++ b/actions/add.action.ts @@ -19,6 +19,7 @@ import { shouldAskForProject, } from '../lib/utils/project-utils'; import { AbstractAction } from './abstract.action'; +import { CaseType } from '../lib/utils/formatting'; const schematicName = 'nest-add'; @@ -122,10 +123,13 @@ export class AddAction extends AbstractAction { ) { console.info(MESSAGES.LIBRARY_INSTALLATION_STARTS); const schematicOptions: SchematicOption[] = []; + const caseType = options + .find((option) => option.name === 'case')?.value as CaseType schematicOptions.push( new SchematicOption( 'sourceRoot', options.find((option) => option.name === 'sourceRoot')!.value as string, + caseType ), ); const extraFlagsString = extraFlags ? extraFlags.join(' ') : undefined; diff --git a/actions/generate.action.ts b/actions/generate.action.ts index 22c3ff012..486814d4f 100644 --- a/actions/generate.action.ts +++ b/actions/generate.action.ts @@ -19,6 +19,7 @@ import { shouldGenerateSpec, } from '../lib/utils/project-utils'; import { AbstractAction } from './abstract.action'; +import { CaseType } from '../lib/utils/formatting'; export class GenerateAction extends AbstractAction { public async handle(inputs: Input[], options: Input[]) { @@ -41,12 +42,14 @@ const generateFiles = async (inputs: Input[]) => { (option) => option.name === 'specFileSuffix', ); + const caseType = inputs.find((option) => option.name === 'case')!.value as CaseType; + const collection: AbstractCollection = CollectionFactory.create( collectionOption || configuration.collection || Collection.NESTJS, ); - const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs); + const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs, caseType); schematicOptions.push( - new SchematicOption('language', configuration.language), + new SchematicOption('language', configuration.language, caseType), ); const configurationProjects = configuration.projects; @@ -125,11 +128,11 @@ const generateFiles = async (inputs: Input[]) => { } } - schematicOptions.push(new SchematicOption('sourceRoot', sourceRoot)); - schematicOptions.push(new SchematicOption('spec', generateSpec)); - schematicOptions.push(new SchematicOption('flat', generateFlat)); + schematicOptions.push(new SchematicOption('sourceRoot', sourceRoot, caseType)); + schematicOptions.push(new SchematicOption('spec', generateSpec, caseType)); + schematicOptions.push(new SchematicOption('flat', generateFlat, caseType)); schematicOptions.push( - new SchematicOption('specFileSuffix', generateSpecFileSuffix), + new SchematicOption('specFileSuffix', generateSpecFileSuffix, caseType), ); try { const schematicInput = inputs.find((input) => input.name === 'schematic'); @@ -144,12 +147,12 @@ const generateFiles = async (inputs: Input[]) => { } }; -const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => { +const mapSchematicOptions = (inputs: Input[], caseType: CaseType): SchematicOption[] => { const excludedInputNames = ['schematic', 'spec', 'flat', 'specFileSuffix']; const options: SchematicOption[] = []; inputs.forEach((input) => { if (!excludedInputNames.includes(input.name) && input.value !== undefined) { - options.push(new SchematicOption(input.name, input.value)); + options.push(new SchematicOption(input.name, input.value, caseType)); } }); return options; diff --git a/actions/new.action.ts b/actions/new.action.ts index 99c3fef50..eab5ebe0f 100644 --- a/actions/new.action.ts +++ b/actions/new.action.ts @@ -130,7 +130,7 @@ const mapSchematicOptions = (options: Input[]): SchematicOption[] => { return options.reduce( (schematicOptions: SchematicOption[], option: Input) => { if (option.name !== 'skip-install') { - schematicOptions.push(new SchematicOption(option.name, option.value)); + schematicOptions.push(new SchematicOption(option.name, option.value, 'kebab')); } return schematicOptions; }, diff --git a/commands/generate.command.ts b/commands/generate.command.ts index d6613afe5..0dfabb50d 100644 --- a/commands/generate.command.ts +++ b/commands/generate.command.ts @@ -48,6 +48,10 @@ export class GenerateCommand extends AbstractCommand { '-c, --collection [collectionName]', 'Schematics collection to use.', ) + .option( + '--case [case]', + 'Case for variable naming. Options are \'kebab\' | \'snake\' | \'camel\' | \'pascal\' | \'upper\'.', + ) .action( async ( schematic: string, @@ -93,6 +97,11 @@ export class GenerateCommand extends AbstractCommand { value: command.skipImport, }); + options.push({ + name: 'case', + value: command.case, + }); + const inputs: Input[] = []; inputs.push({ name: 'schematic', value: schematic }); inputs.push({ name: 'name', value: name }); diff --git a/lib/schematics/schematic.option.ts b/lib/schematics/schematic.option.ts index b0bb4bf85..01d14a3f7 100644 --- a/lib/schematics/schematic.option.ts +++ b/lib/schematics/schematic.option.ts @@ -1,10 +1,10 @@ -import { normalizeToKebabOrSnakeCase } from '../utils/formatting'; +import { normalizeToCase, formatString, CaseType } from '../utils/formatting'; export class SchematicOption { - constructor(private name: string, private value: boolean | string) {} + constructor(private name: string, private value: boolean | string, private caseType: CaseType) {} get normalizedName() { - return normalizeToKebabOrSnakeCase(this.name); + return normalizeToCase(this.name, 'kebab'); } public toCommandString(): string { @@ -25,13 +25,14 @@ export class SchematicOption { } private format() { - return normalizeToKebabOrSnakeCase(this.value as string) - .split('') - .reduce((content, char) => { - if (char === '(' || char === ')' || char === '[' || char === ']') { - return `${content}\\${char}`; - } - return `${content}${char}`; - }, ''); + + return formatString( + normalizeToCase( + this.value as string, + this.caseType + ) + ); } } + + diff --git a/lib/utils/formatting.ts b/lib/utils/formatting.ts index 3f9b39bdf..102b30dc0 100644 --- a/lib/utils/formatting.ts +++ b/lib/utils/formatting.ts @@ -1,3 +1,48 @@ +import { CaseToCase } from 'case-to-case' + +const formatter = new CaseToCase() + +export type CaseType = 'kebab' | 'snake' | 'camel' | 'pascal' | 'upper'; + +/** + * + * @param str + * @param caseType + * @returns formated string + * @description normalizes input to supported path and file name format. + * Changes camelCase strings to kebab-case, replaces spaces with dash and keeps underscores. + */ +export const normalizeToCase = (str: string, caseType: CaseType = 'kebab') => { + switch (caseType) { + case 'kebab': + return normalizeToKebabOrSnakeCase(str); + case 'snake': + return normalizeToKebabOrSnakeCase(str); + case 'camel': + return formatter.toCamelCase(str); + case 'pascal': + return formatter.toPascalCase(str); + case 'upper': + return formatter.toUpperCase(str); + default: + console.log(`Error! case type ${caseType} is not supported.`) + return str; + + } +} + +export const formatString = (str: string) => { + return str.split('') + .reduce((content, char) => { + if (char === '(' || char === ')' || char === '[' || char === ']') { + return `${content}\\${char}`; + } + return `${content}${char}`; + }, '') +} + + + /** * * @param str @@ -13,3 +58,5 @@ export function normalizeToKebabOrSnakeCase(str: string) { .toLowerCase() .replace(STRING_DASHERIZE_REGEXP, '-'); } + + diff --git a/package-lock.json b/package-lock.json index 5f3924876..105c0d108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "@nestjs/cli", - "version": "10.1.8", + "version": "10.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@nestjs/cli", - "version": "10.1.8", + "version": "10.2.0", "license": "MIT", "dependencies": { "@angular-devkit/core": "16.1.3", "@angular-devkit/schematics": "16.1.3", "@angular-devkit/schematics-cli": "16.1.3", "@nestjs/schematics": "^10.0.1", + "case-to-case": "1.2.0", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", @@ -5612,6 +5613,11 @@ } ] }, + "node_modules/case-to-case": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/case-to-case/-/case-to-case-1.2.0.tgz", + "integrity": "sha512-ziCZ0BceuKBDYHi7EgRyhpCg3oqtffrzxPCrB7KWpHVfxJbeGxvWBcrq0Z2ly+ncBrA09U+yRhjpA9U0Q6Usmg==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -23827,6 +23833,11 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz", "integrity": "sha512-F6x5IEuigtUfU5ZMQK2jsy5JqUUlEFRVZq8bO2a+ysq5K7jD6PPc9YXZj78xDNS3uNchesp1Jw47YXEqr+Viyg==" }, + "case-to-case": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/case-to-case/-/case-to-case-1.2.0.tgz", + "integrity": "sha512-ziCZ0BceuKBDYHi7EgRyhpCg3oqtffrzxPCrB7KWpHVfxJbeGxvWBcrq0Z2ly+ncBrA09U+yRhjpA9U0Q6Usmg==" + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index 2729a04eb..82539e694 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nestjs/cli", - "version": "10.1.8", + "version": "10.2.0", "description": "Nest - modern, fast, powerful node.js web framework (@cli)", "publishConfig": { "access": "public" @@ -42,6 +42,7 @@ "@angular-devkit/schematics": "16.1.3", "@angular-devkit/schematics-cli": "16.1.3", "@nestjs/schematics": "^10.0.1", + "case-to-case": "1.2.0", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", diff --git a/test/lib/schematics/schematic.option.spec.ts b/test/lib/schematics/schematic.option.spec.ts index de4f13451..56f40db6b 100644 --- a/test/lib/schematics/schematic.option.spec.ts +++ b/test/lib/schematics/schematic.option.spec.ts @@ -100,7 +100,7 @@ describe('Schematic Option', () => { tests.forEach((test) => { it(test.description, () => { - const option = new SchematicOption(test.option, test.input); + const option = new SchematicOption(test.option, test.input, 'kebab'); if (isFlagTest(test)) { if (test.input) { @@ -117,7 +117,7 @@ describe('Schematic Option', () => { }); it('should manage boolean option', () => { - const option = new SchematicOption('dry-run', false); + const option = new SchematicOption('dry-run', false, 'kebab'); expect(option.toCommandString()).toEqual('--no-dry-run'); }); }); diff --git a/test/lib/utils/formatting.spec.ts b/test/lib/utils/formatting.spec.ts new file mode 100644 index 000000000..1d49ce201 --- /dev/null +++ b/test/lib/utils/formatting.spec.ts @@ -0,0 +1,33 @@ +import { normalizeToCase, formatString, CaseType } from '../../../lib/utils/formatting'; +import {SchematicOption} from "../../../lib/schematics"; + +type TestSuite = { + description: string; + input: string; + caseType: CaseType; + expected: string; +}; + +describe('Format strings', () => { + + const tests: TestSuite[] = [ + { + description: 'From kebap to camel', + input: 'my-app', + caseType: 'camel', + expected: 'myApp', + }, + ]; + + tests.forEach((test) => { + it(test.description, () => { + + expect( + normalizeToCase(test.input, test.caseType) + ).toEqual( + test.expected + ); + }); + }); + +});