diff --git a/readme.md b/readme.md index 81810a0..2f392c4 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/npm/l/express.svg)](https://github.com/arminbro/generate-react-cli/blob/master/LICENSE) -

+

@@ -20,6 +20,7 @@ You can also watch an excellent [video](https://www.youtube.com/watch?v=NEvnt3MW - [Generate components](#generate-components) - [Custom component types](#custom-component-types) - [Custom component templates](#custom-component-templates) +- [Custom component directory](#custom-component-directory) - [Custom component files](#custom-component-files) - [OpenAi integration (Alpha release)](#openai-integration-alpha-release) @@ -97,10 +98,11 @@ Otherwise, if you don't pass any options, it will just use the default values th Value Type Default Value + --path - Value of the path where you want the component to be generated in (e.g. src/components). + Value of the path where you want the component to be generated in (e.g. src/components). String component.default.path @@ -118,7 +120,7 @@ Otherwise, if you don't pass any options, it will just use the default values th --withLazy - Creates a corresponding lazy file (a file that lazy-loads your component out of the box and enables code splitting) with this component. + Creates a corresponding lazy file (a file that lazy-loads your component out of the box and enables code splitting) with this component. Boolean component.default.withLazy @@ -127,7 +129,7 @@ Otherwise, if you don't pass any options, it will just use the default values th --withStory - Creates a corresponding (storybook) story file with this component. + Creates a corresponding (storybook) story file with this component. Boolean component.default.withStory @@ -136,7 +138,7 @@ Otherwise, if you don't pass any options, it will just use the default values th --withStyle - Creates a corresponding stylesheet file with this component. + Creates a corresponding stylesheet file with this component. Boolean component.default.withStyle @@ -145,11 +147,12 @@ Otherwise, if you don't pass any options, it will just use the default values th --withTest - Creates a corresponding test file with this component. + Creates a corresponding test file with this component. Boolean component.default.withTest + --dry-run @@ -158,15 +161,27 @@ Otherwise, if you don't pass any options, it will just use the default values th Boolean false + --flat - Generate the files in the mentioned path insted of creating new folder for it + Generate the files in the mentioned path instead of creating new folder for it Boolean false - + + + --customDirectory + + Template value that overrides the name of the directory of the component to be generated in.
+ See more under custom component directory. + + String + null + + + --describe Describe the component you're trying to generate, and OpenAI will do its best to render it following your instructions. @@ -176,7 +191,7 @@ Otherwise, if you don't pass any options, it will just use the default values th -### Custom component types: +### Custom component types By default, GRC will use the `component.default` configuration rules when running the component command out of the box. @@ -329,6 +344,93 @@ it('It should mount', () => { }); ``` +### Custom component directory + +Using the `customDirectory` you can easily override the directory name for the component generated. For instance, if prefixes are required for particular components or if template names will be mixed, the `customDirectory` option will allow you to override the way that GRC generates the name of the directory where the component files will live. + +The `customDirectory` directive allows all supported casings (see previous section) and can be overridden at the following levels in ascending specific of priority: + +- top +- component.default +- component._type_ +- CLI + +#### Example: + +For React Context Providers in a project, the decision has been made to separate Context generation from the visual components. + +In a typical configuration the configuration would look as following: + +```json +{ + "provider": { + "path": "src/components/providers", + "withLazy": false, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withContext": true, + "customTemplates": { + "component": "src/components/templates/provider/TemplateName.tsx", + "context": "src/components/templates/provider/TemplateName.context.ts", + "story": "src/components/templates/provider/TemplateName.stories.tsx", + "test": "src/components/templates/provider/TemplateName.test.tsx", + "types": "src/components/templates/provider/TemplateName.types.ts" + } + } +} +``` + +With the configuration above, the component would be required to either follow a full or a minimalistic naming convention. +I.e. the component would either need to be generated as `ThemeProvider` and consequently the context name would be generated as `ThemeProviderContext`, or by renaming the files and templates as `TemplateNameProvider` but with the downside of the component path being generated as `src/components/providers/Theme`. This creates inconsistent naming in the directory containg the component files. + +To work around this, the `customDirectory` option can be used to enforce a particular style. + +```json +{ + ... + "provider": { + "path": "src/components/providers", + "withLazy": false, + "withStory": true, + "withStyle": false, + "withTest": true, + "withTypes": true, + "withContext": true, + "customDirectory": "TemplateNameProvider", + "customTemplates": { + "component": "src/components/templates/provider/TemplateNameProvider.tsx", + "context": "src/components/templates/provider/TemplateName.context.ts", + "story": "src/components/templates/provider/TemplateNameProvider.stories.tsx", + "test": "src/components/templates/provider/TemplateNameProvider.test.tsx", + "types": "src/components/templates/provider/TemplateNameProvider.types.ts" + } + } + ... +} +``` + +The above configuration would allow you to mix and match different template names and keep naming consistent. + +If we executed GRC with the above configuration (`npx generate-react-cli component Theme --type=provider`), the result would look like this: + +```fs +src/components/providers/ThemeProvider/Theme.context.ts +src/components/providers/ThemeProvider/ThemeProvider.tsx +src/components/providers/ThemeProvider/ThemeProvider.stories.tsx +src/components/providers/ThemeProvider/ThemeProvider.test.tsx +src/components/providers/ThemeProvider/ThemeProvider.types.ts +``` + +Similarly, this construct could be used as a shortcut for generating other named components, like the `BoxLayout` example above, depending on that could be shortened to: + +```sh + npx generate-react-cli component Box --type=layout --customDir=TemplateNameLayout +``` + +Or it could be used to generate files with a naming convention with `Test`, `Lazy`, `Context`, `Theme`, or `Provider` suffixes. Or even combined with skeleton CSS + ### Custom component files GRC comes with corresponding built-in files for a given component if you need them (i.e., `withStyle`, `withTest`, `withStory`, and `withLazy`). diff --git a/src/commands/generateComponent.js b/src/commands/generateComponent.js index 20d92aa..e438d39 100644 --- a/src/commands/generateComponent.js +++ b/src/commands/generateComponent.js @@ -29,7 +29,17 @@ export default function initGenerateComponentCommand(args, cliConfigFile, progra 'Generate the files in the mentioned path instead of creating new folder for it', selectedComponentType.flat || false ) - .option('-dr, --dry-run', 'Show what will be generated without writing to disk'); + .option('-dr, --dry-run', 'Show what will be generated without writing to disk') + .option( + '--customDirectory ', + 'You can pass a cased path template that will be used as the component path for the component being generated.\n' + + 'E.g. this allows you to add a prefix or suffix to the component path, ' + + 'or change the case of the name of the directory holding the components to kebab-case.\n' + + 'Examples:\n' + + '- TemplateName\n' + + '- template-name\n' + + '- TemplateNameSuffix' + ); // Dynamic component command option defaults. diff --git a/src/utils/generateComponentUtils.js b/src/utils/generateComponentUtils.js index 80692d4..e41b315 100644 --- a/src/utils/generateComponentUtils.js +++ b/src/utils/generateComponentUtils.js @@ -19,6 +19,8 @@ import componentTestEnzymeTemplate from '../templates/component/componentTestEnz import componentTestDefaultTemplate from '../templates/component/componentTestDefaultTemplate.js'; import componentTestTestingLibraryTemplate from '../templates/component/componentTestTestingLibraryTemplate.js'; +const templateNameRE = /.*(template[|_-]?name).*/i; + const { existsSync, outputFileSync, readFileSync } = fsExtra; export function getComponentByType(args, cliConfigFile) { @@ -71,7 +73,7 @@ function getCustomTemplate(componentName, templatePath) { console.error( chalk.red( ` -ERROR: The custom template path of "${templatePath}" does not exist. +ERROR: The custom template path of "${templatePath}" does not exist. Please make sure you're pointing to the right custom template path in your generate-react-cli.json config file. ` ) @@ -81,7 +83,52 @@ Please make sure you're pointing to the right custom template path in your gener } } -function componentTemplateGenerator({ cmd, componentName, cliConfigFile }) { +function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }) { + let componentPath = cmd.path; + + if (cmd.flat !== true) { + let componentDirectory = componentName; + + const customDirectoryConfigs = [ + cliConfigFile.customDirectory, + cliConfigFile.component.default.customDirectory, + cliConfigFile.component[cmd.type].customDirectory, + cmd.customDirectory, + ].filter((e) => Boolean(e) && typeof e === 'string'); + + if (customDirectoryConfigs.length > 0) { + const customDirectory = customDirectoryConfigs.slice(-1).toString(); + + // Double check if the customDirectory is templatable + if (templateNameRE.exec(customDirectory) == null) { + console.error( + chalk.red( + `customDirectory [${customDirectory}] for ${componentName} does not contain a templatable value.\nPlease check your configuration!` + ) + ); + + process.exit(-2); + } + + for (const convertor in convertors) { + const re = new RegExp(`.*${convertor}.*`); + + if (re.exec(customDirectory) !== null) { + componentDirectory = customDirectory.replace(convertor, convertors[convertor]); + } + } + } + + componentPath += `/${componentDirectory}`; + } + + componentPath += `/${filename}`; + + return componentPath; +} + +function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convertors }) { + // @ts-ignore const { usesStyledComponents, cssPreprocessor, testLibrary, usesCssModule, usesTypeScript } = cliConfigFile; const { customTemplates } = cliConfigFile.component[cmd.type]; let template = null; @@ -145,13 +192,13 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile }) { } return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; } -function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName }) { +function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, convertors }) { const { customTemplates } = cliConfigFile.component[cmd.type]; let template = null; let filename = null; @@ -185,13 +232,13 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName }) } return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; } -function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName }) { +function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, convertors }) { const { customTemplates } = cliConfigFile.component[cmd.type]; const { testLibrary, usesTypeScript } = cliConfigFile; let template = null; @@ -224,13 +271,13 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName }) { } return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; } -function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName }) { +function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName, convertors }) { const { usesTypeScript } = cliConfigFile; const { customTemplates } = cliConfigFile.component[cmd.type]; let template = null; @@ -256,13 +303,13 @@ function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName }) } return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; } -function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile }) { +function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile, convertors }) { const { usesTypeScript } = cliConfigFile; const { customTemplates } = cliConfigFile.component[cmd.type]; let template = null; @@ -288,13 +335,13 @@ function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile }) { } return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; } -function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, componentFileType }) { +function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, componentFileType, convertors }) { const { customTemplates } = cliConfigFile.component[cmd.type]; const fileType = camelCase(componentFileType.split('with')[1]); let filename = null; @@ -306,7 +353,7 @@ function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, compon console.error( chalk.red( ` -ERROR: Custom component files require a valid custom template. +ERROR: Custom component files require a valid custom template. Please make sure you're pointing to the right custom template path in your generate-react-cli.json config file. ` ) @@ -326,7 +373,7 @@ Please make sure you're pointing to the right custom template path in your gener filename = customTemplateFilename; return { - componentPath: `${cmd.path}${cmd.flat ? '' : `/${componentName}`}/${filename}`, + componentPath: componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, filename, convertors }), filename, template, }; @@ -365,11 +412,21 @@ export function generateComponent(componentName, cmd, cliConfigFile) { ) { const generateTemplate = componentTemplateGeneratorMap[componentFileType] || customFileTemplateGenerator; + const convertors = { + templatename: componentName, + TemplateName: startCase(camelCase(componentName)).replace(/ /g, ''), + templateName: camelCase(componentName), + 'template-name': kebabCase(componentName), + template_name: snakeCase(componentName), + TEMPLATE_NAME: snakeCase(componentName).toUpperCase(), + }; + const { componentPath, filename, template } = generateTemplate({ cmd, componentName, cliConfigFile, componentFileType, + convertors, }); // --- Make sure the component does not already exist in the path directory. @@ -384,7 +441,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the templatename in whichever format the user typed the component name in the command. replace({ regex: 'templatename', - replacement: componentName, + replacement: convertors['templatename'], paths: [componentPath], recursive: false, silent: true, @@ -393,7 +450,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the TemplateName in PascalCase replace({ regex: 'TemplateName', - replacement: startCase(camelCase(componentName)).replace(/ /g, ''), + replacement: convertors['TemplateName'], paths: [componentPath], recursive: false, silent: true, @@ -402,7 +459,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the templateName in camelCase replace({ regex: 'templateName', - replacement: camelCase(componentName), + replacement: convertors['templateName'], paths: [componentPath], recursive: false, silent: true, @@ -411,7 +468,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the template-name in kebab-case replace({ regex: 'template-name', - replacement: kebabCase(componentName), + replacement: convertors['template-name'], paths: [componentPath], recursive: false, silent: true, @@ -420,7 +477,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the template_name in snake_case replace({ regex: 'template_name', - replacement: snakeCase(componentName), + replacement: convertors['template_name'], paths: [componentPath], recursive: false, silent: true, @@ -429,7 +486,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { // Will replace the TEMPLATE_NAME in uppercase SNAKE_CASE replace({ regex: 'TEMPLATE_NAME', - replacement: snakeCase(componentName).toUpperCase(), + replacement: convertors['TEMPLATE_NAME'], paths: [componentPath], recursive: false, silent: true, @@ -441,6 +498,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) { if (cmd.describe && componentFileType === buildInComponentFileTypes.COMPONENT) { aiComponentGenerator(template, cmd.describe) .then((aiGeneratedComponent) => { + // @ts-ignore outputFileSync(componentPath, aiGeneratedComponent.trim()); console.log( chalk.green(`OpenAI Successfully created the ${filename} component with the provided description.`)