Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial support for case type selection #2208

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4e4668e
feat: case naming
espoal Jul 31, 2023
f0065ed
chore: linting and formatting
espoal Jul 31, 2023
bfb061a
feat: make schematic option config optional
espoal Jul 31, 2023
d2cc288
feat: update jsdoc
espoal Jul 31, 2023
3b015b2
chore: fixing typos
espoal Aug 3, 2023
40c5032
fix: consistent option naming
espoal Aug 3, 2023
9235d0d
feat: improved switch case loop
espoal Aug 3, 2023
0eef7e9
chore: package.json versioning
espoal Sep 27, 2023
33301f4
feat: nest cli
espoal Sep 27, 2023
b59ae2e
feat: read case naming from cli
espoal Sep 29, 2023
7592150
chore: cleanup
espoal Sep 29, 2023
5cba7cf
chore: more cleanup
espoal Sep 29, 2023
14190bd
chore: more cleanup
espoal Sep 29, 2023
3d80b1b
chore: more cleanup
espoal Sep 29, 2023
fa607e3
chore: more cleanup
espoal Sep 29, 2023
cf1ddf7
chore: more cleanup
espoal Sep 29, 2023
50fe8b2
chore: more cleanup
espoal Sep 29, 2023
35c4167
chore: package.json version
espoal Sep 29, 2023
2fce7ec
Update commands/new.command.ts
espoal Sep 29, 2023
26f5f06
chore: fix tests
espoal Oct 4, 2023
ee1f782
Merge branch 'master' into feat/caseNaming
espoal Oct 4, 2023
707dbb1
chore: removed cli option
espoal Oct 5, 2023
5b0afbb
chore: fixed npm version for case-anything
espoal Oct 5, 2023
7c15944
feat: added tests for strings
espoal Oct 13, 2023
8cd3da7
feat: snake case
espoal Oct 16, 2023
2eaf507
Merge branch 'master' into feat/caseNaming
espoal Oct 16, 2023
9abfe52
chore: align package-lock.json
espoal Oct 16, 2023
1ac1093
chore: aligning package-lock.json
espoal Oct 16, 2023
a482f79
feat: remove kebab-or-snake case
espoal Oct 17, 2023
b4048db
chore: silenced some tests
espoal Oct 17, 2023
b730e41
feat: sanitze kebab-or-snake input
espoal Oct 17, 2023
e5d57c2
Merge branch 'master' into feat/caseNaming
espoal Oct 18, 2023
301cf0c
feat: fix semantic version of case-anything
espoal Oct 18, 2023
3531cd2
feat: explicit mention of kebab-or-snake case
espoal Oct 18, 2023
46d1f48
feat: better docs
espoal Nov 15, 2023
5a5618b
Merge branch 'master' into feat/caseNaming
espoal Nov 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions actions/generate.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
shouldGenerateSpec,
} from '../lib/utils/project-utils';
import { AbstractAction } from './abstract.action';
import { CaseType, normalizeToCase } from '../lib/utils/formatting';

export class GenerateAction extends AbstractAction {
public async handle(inputs: Input[], options: Input[]) {
Expand All @@ -44,10 +45,22 @@ const generateFiles = async (inputs: Input[]) => {
const collection: AbstractCollection = CollectionFactory.create(
collectionOption || configuration.collection || Collection.NESTJS,
);

const optionsCaseType = (
inputs.find((option) => option.name === 'caseNaming')?.value
) as CaseType | undefined;
const configCaseType = (
configuration?.generateOptions?.caseNaming
) as CaseType | undefined;
const caseType = optionsCaseType || configCaseType || 'kebab-or-snake';

const inputName = inputs.find((option) => option.name === 'name');
const name = normalizeToCase(inputName?.value as string, caseType);

const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs);
schematicOptions.push(
new SchematicOption('language', configuration.language),
);
schematicOptions.push(new SchematicOption('name', name));
schematicOptions.push(new SchematicOption('caseNaming', caseType));
schematicOptions.push(new SchematicOption('language', configuration.language));
const configurationProjects = configuration.projects;

let sourceRoot = appName
Expand Down Expand Up @@ -128,9 +141,7 @@ 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('specFileSuffix', generateSpecFileSuffix),
);
schematicOptions.push(new SchematicOption('specFileSuffix', generateSpecFileSuffix));
try {
const schematicInput = inputs.find((input) => input.name === 'schematic');
if (!schematicInput) {
Expand All @@ -144,8 +155,10 @@ const generateFiles = async (inputs: Input[]) => {
}
};

const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => {
const excludedInputNames = ['schematic', 'spec', 'flat', 'specFileSuffix'];
const mapSchematicOptions = (
inputs: Input[],
): SchematicOption[] => {
const excludedInputNames = ['name','schematic', 'spec', 'flat', 'specFileSuffix'];
const options: SchematicOption[] = [];
inputs.forEach((input) => {
if (!excludedInputNames.includes(input.name) && input.value !== undefined) {
Expand Down
9 changes: 9 additions & 0 deletions commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export class GenerateCommand extends AbstractCommand {
'-c, --collection [collectionName]',
'Schematics collection to use.',
)
.option(
'--caseNaming [caseType]',
`Casing type for generated elements. Available options: "pascal", "camel", "kebab-or-snake" (default).`,
)
.action(
async (
schematic: string,
Expand Down Expand Up @@ -93,6 +97,11 @@ export class GenerateCommand extends AbstractCommand {
value: command.skipImport,
});

options.push({
name: 'caseNaming',
value: command.caseNaming,
});

const inputs: Input[] = [];
inputs.push({ name: 'schematic', value: schematic });
inputs.push({ name: 'name', value: name });
Expand Down
9 changes: 9 additions & 0 deletions commands/new.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export class NewCommand extends AbstractCommand {
Collection.NESTJS,
)
.option('--strict', 'Enables strict mode in TypeScript.', false)
.option(
'--caseNaming [caseType]',
`Casing type for generated elements. Available options: "pascal", "camel", "kebab-or-snake" (default).`,
Copy link
Member

@micalevisk micalevisk Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't follow what is the kebab-or-snake type 🤔

Also,

Suggested change
`Casing type for generated elements. Available options: "pascal", "camel", "kebab-or-snake" (default).`,
`Casing type for generated files. Available options: "pascal", "camel", "kebab" (default).`,

since this will cover file names only, as discussed in the issue

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the kebab-or-snake case:
In the original code there is a case normalization function that is a mix of kebap and snake case. I kept it for historical purposes, but I also introduced separated kebap and snake normalization functions. I would more than willing to remove it.

)
.action(async (name: string, command: Command) => {
const options: Input[] = [];
const availableLanguages = ['js', 'ts', 'javascript', 'typescript'];
Expand All @@ -46,6 +50,11 @@ export class NewCommand extends AbstractCommand {
});
options.push({ name: 'collection', value: command.collection });

options.push({
name: 'caseNaming',
value: command.caseNaming,
});

if (!!command.language) {
const lowercasedLanguage = command.language.toLowerCase();
const langMatch = availableLanguages.includes(lowercasedLanguage);
Expand Down
1 change: 1 addition & 0 deletions lib/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface GenerateOptions {
spec?: boolean | Record<string, boolean>;
flat?: boolean;
specFileSuffix?: string;
caseNaming?: string;
}

export interface ProjectConfiguration {
Expand Down
18 changes: 8 additions & 10 deletions lib/schematics/schematic.option.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { normalizeToKebabOrSnakeCase } from '../utils/formatting';
import { normalizeToCase, formatString } from '../utils/formatting';

export class SchematicOption {
constructor(
Expand All @@ -7,7 +7,7 @@ export class SchematicOption {
) {}

get normalizedName() {
return normalizeToKebabOrSnakeCase(this.name);
return normalizeToCase(this.name, 'kebab-or-snake');
}

public toCommandString(): string {
Expand All @@ -28,13 +28,11 @@ 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,
'kebab-or-snake',
),
);
}
}
56 changes: 55 additions & 1 deletion lib/utils/formatting.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,61 @@
import {
camelCase,
kebabCase,
pascalCase,
snakeCase,
capitalCase,
} from 'case-anything';

export type CaseType =
| 'kebab'
| 'snake'
| 'camel'
| 'pascal'
| 'capital'
| 'kebab-or-snake';

/**
*
* @param str
* @returns formated string
* @param caseType CaseType
* @returns formatted string
* @description normalizes input to a given case format.
*/
export const normalizeToCase = (
str: string,
caseType: CaseType,
) => {
switch (caseType) {
case 'kebab':
return kebabCase(str);
case 'snake':
return snakeCase(str);
case 'camel':
return camelCase(str);
case 'pascal':
return pascalCase(str);
case 'capital':
return capitalCase(str);
// For legacy purposes
case 'kebab-or-snake':
default:
return normalizeToKebabOrSnakeCase(str);
}
};

export const formatString = (str: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate here in the code on what this function is supposed to do?

github copilot can help :p

Copy link
Author

@espoal espoal Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang, I'm surprised copilot understood immediately the code. I'm starting to question reality :D

Added some JsDoc. Basically this function escapes parenthesis which will then later be removed.

I see here an opportunity to create @nestjs/utils or @nestjs/stringUtils because this code is shared across projects, and I feel it would be beneficial to centralize it so that it's easier to maintain and can be reused by plugin developers. I didn't do it to keep the scope limited, but it shouldn't be too hard to do and I would be up for it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I updated the same code also in @nestjs/schematics

return str.split('').reduce((content, char) => {
if (char === '(' || char === ')' || char === '[' || char === ']') {
return `${content}\\${char}`;
}
return `${content}${char}`;
}, '');
};

/**
*
* @param str
* @returns formatted string
* @description normalizes input to supported path and file name format.
* Changes camelCase strings to kebab-case, replaces spaces with dash and keeps underscores.
*/
Expand Down
21 changes: 19 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestjs/cli",
"version": "10.1.11",
"version": "10.1.18",
espoal marked this conversation as resolved.
Show resolved Hide resolved
"description": "Nest - modern, fast, powerful node.js web framework (@cli)",
"publishConfig": {
"access": "public"
Expand Down Expand Up @@ -42,6 +42,7 @@
"@angular-devkit/schematics": "16.1.4",
"@angular-devkit/schematics-cli": "16.1.4",
"@nestjs/schematics": "^10.0.1",
"case-anything": "2.1.13",
"chalk": "4.1.2",
"chokidar": "3.5.3",
"cli-table3": "0.6.3",
Expand Down
5 changes: 4 additions & 1 deletion test/lib/schematics/schematic.option.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { SchematicOption } from '../../../lib/schematics';
import {
SchematicOption,
SchematicOptionConfig,
} from '../../../lib/schematics';

interface TestOption {
input: string;
Expand Down
25 changes: 25 additions & 0 deletions test/lib/utils/formatting.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { normalizeToCase, CaseType } from '../../../lib/utils/formatting';

type TestSuite = {
description: string;
input: string;
caseType: CaseType;
expected: string;
};

describe('Format strings', () => {
const tests: TestSuite[] = [
espoal marked this conversation as resolved.
Show resolved Hide resolved
{
description: 'From kebab to camel',
input: 'my-app',
caseType: 'camel',
expected: 'myApp',
},
];

tests.forEach((test) => {
it(test.description, () => {
expect(normalizeToCase(test.input, test.caseType)).toEqual(test.expected);
});
});
});