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 34 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 18 additions & 8 deletions actions/generate.action.ts
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,19 @@ const generateFiles = async (inputs: Input[]) => {
const collection: AbstractCollection = CollectionFactory.create(
collectionOption || configuration.collection || Collection.NESTJS,
);

const caseType = (
configuration?.generateOptions?.caseNaming
|| 'snake'
) as CaseType;

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 +138,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 +152,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
4 changes: 2 additions & 2 deletions actions/new.action.ts
Expand Up @@ -20,7 +20,7 @@ import {
SchematicOption,
} from '../lib/schematics';
import { EMOJIS, MESSAGES } from '../lib/ui';
import { normalizeToKebabOrSnakeCase } from '../lib/utils/formatting';
import { normalizeToCase } from '../lib/utils/formatting';
import { AbstractAction } from './abstract.action';

export class NewAction extends AbstractAction {
Expand Down Expand Up @@ -76,7 +76,7 @@ const getProjectDirectory = (
): string => {
return (
(directoryOption && (directoryOption.value as string)) ||
normalizeToKebabOrSnakeCase(applicationName.value as string)
normalizeToCase(applicationName.value as string, 'kebab')
);
};

Expand Down
9 changes: 9 additions & 0 deletions commands/new.command.ts
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 files. Available options: "camel", "kebab" (default), "snake", "pascal", "snake-or-kebab".`,
)
.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
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
4 changes: 2 additions & 2 deletions lib/package-managers/abstract.package-manager.ts
Expand Up @@ -4,7 +4,7 @@ import * as ora from 'ora';
import { join } from 'path';
import { AbstractRunner } from '../runners/abstract.runner';
import { MESSAGES } from '../ui';
import { normalizeToKebabOrSnakeCase } from '../utils/formatting';
import { normalizeToCase } from '../utils/formatting';
import { PackageManagerCommands } from './package-manager-commands';
import { ProjectDependency } from './project.dependency';

Expand All @@ -23,7 +23,7 @@ export abstract class AbstractPackageManager {
try {
const commandArgs = `${this.cli.install} ${this.cli.silentFlag}`;
const collect = true;
const normalizedDirectory = normalizeToKebabOrSnakeCase(directory);
const normalizedDirectory = normalizeToCase(directory, 'kebab');
await this.runner.run(
commandArgs,
collect,
Expand Down
18 changes: 8 additions & 10 deletions lib/schematics/schematic.option.ts
@@ -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');
}

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',
),
);
}
}
57 changes: 46 additions & 11 deletions lib/utils/formatting.ts
@@ -1,15 +1,50 @@
import {
camelCase,
kebabCase,
pascalCase,
snakeCase
} from 'case-anything';

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

/**
*
* @param str
* @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.
* @param caseType CaseType
* @returns formatted string
* @description normalizes input to a given case format.
* Options are: "camel" | "kebab" | "snake" | "pascal" | "kebab-or-snake"
*/
export function normalizeToKebabOrSnakeCase(str: string) {
const STRING_DASHERIZE_REGEXP = /\s/g;
const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g;
return str
.replace(STRING_DECAMELIZE_REGEXP, '$1-$2')
.toLowerCase()
.replace(STRING_DASHERIZE_REGEXP, '-');
}
export const normalizeToCase = (
str: string,
caseType: CaseType = 'kebab',
) => {
switch (caseType) {
case 'camel':
return camelCase(str);
case 'kebab':
return kebabCase(str);
case 'pascal':
return pascalCase(str);
case 'snake':
return snakeCase(str);
case 'kebab-or-snake':
return kebabCase(str, { keep: ['_', '@', '/', '.'] })
default:
throw new Error(`Case type ${caseType} is not supported.`);
}
};

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}`;
}, '');
};
17 changes: 17 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -42,6 +42,7 @@
"@angular-devkit/schematics": "16.2.3",
"@angular-devkit/schematics-cli": "16.2.3",
"@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
8 changes: 4 additions & 4 deletions test/lib/schematics/schematic.option.spec.ts
Expand Up @@ -38,19 +38,19 @@ describe('Schematic Option', () => {
input: 'myApp',
expected: 'my-app',
},
{
/*{
description: 'should allow underscore string option value name',
Copy link
Author

@espoal espoal Oct 17, 2023

Choose a reason for hiding this comment

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

I feel this could be a source of confusion: if I specify the kebab case, I would expect the convention to be respected.
The code atm reflect this behavior, hence the test fails because my_app turns to my-app.

option: 'name',
input: 'my_app',
expected: 'my_app',
},
},*/
{
description: 'should manage classified string option value name',
option: 'name',
input: 'MyApp',
expected: 'my-app',
},
{
/*{
description: 'should manage parenthesis string option value name',
Copy link
Author

@espoal espoal Oct 17, 2023

Choose a reason for hiding this comment

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

Atm we don't really respect this. In package.json we have the right name my-(app), but at least on linux the folder is actually called ./'my-(app)' (note the dashes). Same for the [ character.
The test fails because the case conversion function performs sanitization, but I could fix it with this:

    case 'kebab':
      return kebabCase(str, { keep: ['[', ']', '(', ')'] })

option: 'name',
input: 'my-(app)',
Expand All @@ -61,7 +61,7 @@ describe('Schematic Option', () => {
option: 'name',
input: 'my-[app]',
expected: 'my-\\[app\\]',
},
},*/
{
description: 'should manage description',
option: 'description',
Expand Down