generate-react-cli 9.1.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import cli from '../src/cli.js';
3
+ import { error } from '../src/utils/messagesUtils.js';
3
4
 
4
- const isNotValidNodeVersion = () => {
5
+ function isNotValidNodeVersion() {
5
6
  const currentNodeVersion = process.versions.node;
6
7
  const semver = currentNodeVersion.split('.');
7
8
  const major = semver[0];
8
9
 
9
- if (major < 18) {
10
- console.error(
11
- // eslint-disable-next-line
12
- 'You are running Node ' +
13
- currentNodeVersion +
14
- ' Generate React CLI requires Node 18 or higher. Please update your version of Node.'
15
- );
10
+ if (major < 22) {
11
+ error(`Node ${currentNodeVersion} is not supported`, {
12
+ details: 'Generate React CLI requires Node 22 or higher',
13
+ suggestions: [
14
+ 'Update your Node.js version to 22 or higher',
15
+ 'Use nvm to manage multiple Node versions: nvm install 22',
16
+ ],
17
+ });
16
18
 
17
19
  return true;
18
20
  }
19
21
 
20
22
  return false;
21
- };
23
+ }
22
24
 
23
25
  // --- Check if user is running Node 12 or higher.
24
26
 
package/package.json CHANGED
@@ -1,73 +1,68 @@
1
1
  {
2
2
  "name": "generate-react-cli",
3
- "version": "9.1.0",
3
+ "version": "10.1.0",
4
4
  "description": "A simple React CLI to generate components instantly and more.",
5
- "repository": "https://github.com/arminbro/generate-react-cli",
6
- "bugs": "https://github.com/arminbro/generate-react-cli/issues",
7
5
  "author": "Armin Broubakarian",
8
6
  "license": "MIT",
7
+ "repository": "https://github.com/arminbro/generate-react-cli",
8
+ "bugs": "https://github.com/arminbro/generate-react-cli/issues",
9
+ "keywords": [
10
+ "cli",
11
+ "react",
12
+ "build-tools",
13
+ "generate-react-cli"
14
+ ],
9
15
  "main": "bin/generate-react",
10
16
  "bin": {
11
17
  "generate-react": "bin/generate-react.js"
12
18
  },
13
19
  "type": "module",
14
20
  "files": [
15
- "bin/",
16
- "src/",
17
- "README.md",
18
21
  "CHANGELOG.md",
19
- "LICENSE"
22
+ "LICENSE",
23
+ "README.md",
24
+ "bin/",
25
+ "src/"
20
26
  ],
21
27
  "publishConfig": {
22
28
  "access": "public"
23
29
  },
24
- "keywords": [
25
- "cli",
26
- "react",
27
- "build-tools",
28
- "generate-react-cli"
29
- ],
30
30
  "engines": {
31
- "node": ">=10.x",
32
- "npm": ">= 6.x"
31
+ "node": ">=22",
32
+ "npm": ">=10"
33
33
  },
34
34
  "browserslist": [
35
35
  "maintained node versions"
36
36
  ],
37
37
  "scripts": {
38
- "prepare": "husky install"
38
+ "prepare": "husky install",
39
+ "lint": "eslint .",
40
+ "lint:fix": "eslint . --fix"
39
41
  },
40
42
  "dependencies": {
41
43
  "chalk": "5.6.2",
42
- "commander": "14.0.0",
43
- "deep-keys": "0.5.0",
44
- "dotenv": "16.6.1",
45
- "fs-extra": "11.2.0",
46
- "inquirer": "12.9.4",
44
+ "commander": "14.0.2",
45
+ "fs-extra": "11.3.3",
46
+ "inquirer": "13.2.0",
47
47
  "lodash": "4.17.21",
48
48
  "replace": "1.2.2"
49
49
  },
50
50
  "devDependencies": {
51
- "@commitlint/cli": "19.8.1",
52
- "@commitlint/config-conventional": "19.8.1",
51
+ "@antfu/eslint-config": "7.0.1",
52
+ "@commitlint/cli": "20.3.1",
53
+ "@commitlint/config-conventional": "20.3.1",
53
54
  "@semantic-release/commit-analyzer": "13.0.1",
54
55
  "@semantic-release/git": "10.0.1",
55
- "@semantic-release/github": "11.0.5",
56
- "@semantic-release/npm": "12.0.2",
57
- "@semantic-release/release-notes-generator": "14.0.3",
58
- "eslint": "8.57.1",
59
- "eslint-config-airbnb-base": "15.0.0",
60
- "eslint-config-prettier": "9.1.2",
61
- "eslint-plugin-prettier": "5.5.4",
56
+ "@semantic-release/github": "12.0.2",
57
+ "@semantic-release/npm": "13.1.3",
58
+ "@semantic-release/release-notes-generator": "14.1.0",
59
+ "eslint": "9.39.2",
62
60
  "husky": "9.1.7",
63
- "prettier": "3.6.2",
64
- "pretty-quick": "4.2.2",
65
- "semantic-release": "24.2.7"
61
+ "lint-staged": "16.2.7",
62
+ "semantic-release": "25.0.2"
66
63
  },
67
- "prettier": {
68
- "singleQuote": true,
69
- "trailingComma": "es5",
70
- "printWidth": 120
64
+ "lint-staged": {
65
+ "*.js": "eslint --fix"
71
66
  },
72
67
  "release": {
73
68
  "plugins": [
@@ -97,25 +92,5 @@
97
92
  200
98
93
  ]
99
94
  }
100
- },
101
- "eslintConfig": {
102
- "extends": [
103
- "airbnb-base",
104
- "plugin:prettier/recommended"
105
- ],
106
- "env": {
107
- "commonjs": false,
108
- "node": true
109
- },
110
- "parserOptions": {
111
- "ecmaVersion": "latest"
112
- },
113
- "rules": {
114
- "import/extensions": [
115
- {
116
- "js": "always"
117
- }
118
- ]
119
- }
120
95
  }
121
96
  }
package/src/cli.js CHANGED
@@ -1,7 +1,5 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { program } from 'commander';
2
- import { createRequire } from 'module';
3
- import { config as dotEnvConfig } from 'dotenv';
4
- import path from 'path';
5
3
 
6
4
  import initGenerateComponentCommand from './commands/generateComponent.js';
7
5
  import { getCLIConfigFile } from './utils/grcConfigUtils.js';
@@ -11,10 +9,6 @@ export default async function cli(args) {
11
9
  const localRequire = createRequire(import.meta.url);
12
10
  const pkg = localRequire('../package.json');
13
11
 
14
- // init dotenv
15
-
16
- dotEnvConfig({ path: path.resolve(process.cwd(), '.env.local') });
17
-
18
12
  // Initialize generate component command
19
13
 
20
14
  initGenerateComponentCommand(args, cliConfigFile, program);
@@ -11,29 +11,29 @@ export default function initGenerateComponentCommand(args, cliConfigFile, progra
11
11
  .command('component [names...]')
12
12
  .alias('c')
13
13
 
14
- // Static component command option defaults.
14
+ // Static component command option defaults.
15
15
 
16
16
  .option('-p, --path <path>', 'The path where the component will get generated in.', selectedComponentType.path)
17
17
  .option(
18
18
  '--type <type>',
19
19
  'You can pass a component type that you have configured in your GRC config file.',
20
- 'default'
20
+ 'default',
21
21
  )
22
22
  .option(
23
23
  '-f, --flat',
24
24
  'Generate the files in the mentioned path instead of creating new folder for it',
25
- selectedComponentType.flat || false
25
+ selectedComponentType.flat || false,
26
26
  )
27
27
  .option('--dry-run', 'Show what will be generated without writing to disk')
28
28
  .option(
29
29
  '--customDirectory <customDirectory>',
30
- 'You can pass a cased path template that will be used as the component path for the component being generated.\n' +
31
- 'E.g. this allows you to add a prefix or suffix to the component path, ' +
32
- 'or change the case of the name of the directory holding the components to kebab-case.\n' +
33
- 'Examples:\n' +
34
- '- TemplateName\n' +
35
- '- template-name\n' +
36
- '- TemplateNameSuffix'
30
+ 'You can pass a cased path template that will be used as the component path for the component being generated.\n'
31
+ + 'E.g. this allows you to add a prefix or suffix to the component path, '
32
+ + 'or change the case of the name of the directory holding the components to kebab-case.\n'
33
+ + 'Examples:\n'
34
+ + '- TemplateName\n'
35
+ + '- template-name\n'
36
+ + '- TemplateNameSuffix',
37
37
  );
38
38
 
39
39
  // Dynamic component command option defaults.
@@ -44,13 +44,13 @@ export default function initGenerateComponentCommand(args, cliConfigFile, progra
44
44
  componentCommand.option(
45
45
  `--${dynamicOption} <${dynamicOption}>`,
46
46
  `With corresponding ${dynamicOption.split('with')[1]} file.`,
47
- selectedComponentType[dynamicOption]
47
+ selectedComponentType[dynamicOption],
48
48
  );
49
49
  });
50
50
 
51
51
  // Component command action.
52
52
 
53
53
  componentCommand.action((componentNames, cmd) =>
54
- componentNames.forEach((componentName) => generateComponent(componentName, cmd, cliConfigFile))
54
+ componentNames.forEach(componentName => generateComponent(componentName, cmd, cliConfigFile)),
55
55
  );
56
56
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Recursively extracts all keys from a nested object as dot-notation paths.
3
+ *
4
+ * @param {object} obj - The object to extract keys from
5
+ * @param {string} [prefix] - Internal prefix for recursion
6
+ * @returns {string[]} Array of dot-notation key paths
7
+ *
8
+ * @example
9
+ * deepKeys({ a: 1, b: { c: 2 } })
10
+ * // Returns: ['a', 'b.c']
11
+ */
12
+ export default function deepKeys(obj, prefix = '') {
13
+ return Object.keys(obj).reduce((keys, key) => {
14
+ const path = prefix ? `${prefix}.${key}` : key;
15
+ const value = obj[key];
16
+
17
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
18
+ return [...keys, ...deepKeys(value, path)];
19
+ }
20
+
21
+ return [...keys, path];
22
+ }, []);
23
+ }
@@ -1,29 +1,29 @@
1
- import chalk from 'chalk';
2
- import path from 'path';
3
- import replace from 'replace';
1
+ import path from 'node:path';
2
+ import fsExtra from 'fs-extra';
4
3
  import camelCase from 'lodash/camelCase.js';
5
4
  import kebabCase from 'lodash/kebabCase.js';
6
5
  import snakeCase from 'lodash/snakeCase.js';
7
6
  import startCase from 'lodash/startCase.js';
8
- import fsExtra from 'fs-extra';
7
+ import replace from 'replace';
8
+ import componentCssTemplate from '../templates/component/componentCssTemplate.js';
9
9
 
10
10
  import componentJsTemplate from '../templates/component/componentJsTemplate.js';
11
- import componentTsTemplate from '../templates/component/componentTsTemplate.js';
12
- import componentCssTemplate from '../templates/component/componentCssTemplate.js';
13
- import componentStyledTemplate from '../templates/component/componentStyledTemplate.js';
14
11
  import componentLazyTemplate from '../templates/component/componentLazyTemplate.js';
15
- import componentTsLazyTemplate from '../templates/component/componentTsLazyTemplate.js';
16
12
  import componentStoryTemplate from '../templates/component/componentStoryTemplate.js';
17
- import componentTestEnzymeTemplate from '../templates/component/componentTestEnzymeTemplate.js';
13
+ import componentStyledTemplate from '../templates/component/componentStyledTemplate.js';
18
14
  import componentTestDefaultTemplate from '../templates/component/componentTestDefaultTemplate.js';
15
+ import componentTestEnzymeTemplate from '../templates/component/componentTestEnzymeTemplate.js';
19
16
  import componentTestTestingLibraryTemplate from '../templates/component/componentTestTestingLibraryTemplate.js';
17
+ import componentTsLazyTemplate from '../templates/component/componentTsLazyTemplate.js';
18
+ import componentTsTemplate from '../templates/component/componentTsTemplate.js';
19
+ import { error, fileSummary } from './messagesUtils.js';
20
20
 
21
21
  const templateNameRE = /.*(template[|_-]?name).*/i;
22
22
 
23
23
  const { existsSync, outputFileSync, readFileSync } = fsExtra;
24
24
 
25
25
  export function getComponentByType(args, cliConfigFile) {
26
- const hasComponentTypeOption = args.find((arg) => arg.includes('--type'));
26
+ const hasComponentTypeOption = args.find(arg => arg.includes('--type'));
27
27
 
28
28
  // Check for component type option.
29
29
 
@@ -34,14 +34,14 @@ export function getComponentByType(args, cliConfigFile) {
34
34
  // If the selected component type does not exists in the cliConfigFile under `component` throw an error
35
35
 
36
36
  if (!selectedComponentType) {
37
- console.error(
38
- chalk.red(
39
- `
40
- ERROR: Please make sure the component type you're trying to use exists in the
41
- ${chalk.bold('generate-react-cli.json')} config file under the ${chalk.bold('component')} object.
42
- `
43
- )
44
- );
37
+ const availableTypes = Object.keys(cliConfigFile.component).join(', ');
38
+ error(`Unknown component type "${componentType}"`, {
39
+ details: `Available types: ${availableTypes}`,
40
+ suggestions: [
41
+ `Use one of the available types: ${availableTypes}`,
42
+ 'Add this component type to your generate-react-cli.json config',
43
+ ],
44
+ });
45
45
 
46
46
  process.exit(1);
47
47
  }
@@ -57,7 +57,7 @@ export function getComponentByType(args, cliConfigFile) {
57
57
  }
58
58
 
59
59
  export function getCorrespondingComponentFileTypes(component) {
60
- return Object.keys(component).filter((key) => key.split('with').length > 1);
60
+ return Object.keys(component).filter(key => key.split('with').length > 1);
61
61
  }
62
62
 
63
63
  function getCustomTemplate(componentName, templatePath) {
@@ -68,15 +68,14 @@ function getCustomTemplate(componentName, templatePath) {
68
68
  const filename = path.basename(templatePath).replace(/template[_-]?name/i, componentName);
69
69
 
70
70
  return { template, filename };
71
- } catch (e) {
72
- console.error(
73
- chalk.red(
74
- `
75
- ERROR: The custom template path of "${templatePath}" does not exist.
76
- Please make sure you're pointing to the right custom template path in your generate-react-cli.json config file.
77
- `
78
- )
79
- );
71
+ } catch {
72
+ error(`Custom template not found: "${templatePath}"`, {
73
+ suggestions: [
74
+ 'Verify the template path in your generate-react-cli.json config',
75
+ 'Check that the file exists and is readable',
76
+ 'Use an absolute path or a path relative to project root',
77
+ ],
78
+ });
80
79
 
81
80
  return process.exit(1);
82
81
  }
@@ -93,18 +92,20 @@ function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, fi
93
92
  cliConfigFile.component.default.customDirectory,
94
93
  cliConfigFile.component[cmd.type].customDirectory,
95
94
  cmd.customDirectory,
96
- ].filter((e) => Boolean(e) && typeof e === 'string');
95
+ ].filter(e => Boolean(e) && typeof e === 'string');
97
96
 
98
97
  if (customDirectoryConfigs.length > 0) {
99
98
  const customDirectory = customDirectoryConfigs.slice(-1).toString();
100
99
 
101
100
  // Double check if the customDirectory is templatable
102
101
  if (templateNameRE.exec(customDirectory) == null) {
103
- console.error(
104
- chalk.red(
105
- `customDirectory [${customDirectory}] for ${componentName} does not contain a templatable value.\nPlease check your configuration!`
106
- )
107
- );
102
+ error(`Invalid customDirectory: "${customDirectory}"`, {
103
+ details: 'customDirectory must contain a template placeholder',
104
+ suggestions: [
105
+ 'Use templatename, TemplateName, template-name, or template_name',
106
+ 'Example: "{{templatename}}" or "TemplateName"',
107
+ ],
108
+ });
108
109
 
109
110
  process.exit(-2);
110
111
  }
@@ -128,7 +129,7 @@ function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, fi
128
129
 
129
130
  function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convertors }) {
130
131
  // @ts-ignore
131
- const { usesStyledComponents, cssPreprocessor, testLibrary, usesCssModule, usesTypeScript } = cliConfigFile;
132
+ const { cssPreprocessor, testLibrary, usesCssModule, usesTypeScript } = cliConfigFile;
132
133
  const { customTemplates } = cliConfigFile.component[cmd.type];
133
134
  let template = null;
134
135
  let filename = null;
@@ -140,7 +141,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
140
141
 
141
142
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
142
143
  componentName,
143
- customTemplates.component
144
+ customTemplates.component,
144
145
  );
145
146
 
146
147
  template = customTemplate;
@@ -164,7 +165,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
164
165
  const cssPath = `${componentName}.styled`;
165
166
  template = template.replace(
166
167
  `import styles from './templatename.module.css'`,
167
- `import { templatenameWrapper } from './${cssPath}'`
168
+ `import { templatenameWrapper } from './${cssPath}'`,
168
169
  );
169
170
  template = template.replace(` className={styles.templatename}`, '');
170
171
  template = template.replace(` <div`, '<templatenameWrapper');
@@ -209,7 +210,7 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, co
209
210
 
210
211
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
211
212
  componentName,
212
- customTemplates.style
213
+ customTemplates.style,
213
214
  );
214
215
 
215
216
  template = customTemplate;
@@ -250,7 +251,7 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, con
250
251
 
251
252
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
252
253
  componentName,
253
- customTemplates.test
254
+ customTemplates.test,
254
255
  );
255
256
 
256
257
  template = customTemplate;
@@ -289,7 +290,7 @@ function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName, co
289
290
 
290
291
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
291
292
  componentName,
292
- customTemplates.story
293
+ customTemplates.story,
293
294
  );
294
295
 
295
296
  template = customTemplate;
@@ -321,7 +322,7 @@ function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile, con
321
322
 
322
323
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
323
324
  componentName,
324
- customTemplates.lazy
325
+ customTemplates.lazy,
325
326
  );
326
327
 
327
328
  template = customTemplate;
@@ -349,14 +350,13 @@ function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, compon
349
350
  // Check for a valid custom template for the corresponding custom component file.
350
351
 
351
352
  if (!customTemplates || !customTemplates[fileType]) {
352
- console.error(
353
- chalk.red(
354
- `
355
- ERROR: Custom component files require a valid custom template.
356
- Please make sure you're pointing to the right custom template path in your generate-react-cli.json config file.
357
- `
358
- )
359
- );
353
+ error(`Missing custom template for "${fileType}"`, {
354
+ details: 'Custom component files require a matching custom template',
355
+ suggestions: [
356
+ `Add a "${fileType}" template path to customTemplates in your config`,
357
+ 'Check that the template file exists at the specified path',
358
+ ],
359
+ });
360
360
 
361
361
  return process.exit(1);
362
362
  }
@@ -365,7 +365,7 @@ Please make sure you're pointing to the right custom template path in your gener
365
365
 
366
366
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
367
367
  componentName,
368
- customTemplates[fileType]
368
+ customTemplates[fileType],
369
369
  );
370
370
 
371
371
  template = customTemplate;
@@ -400,25 +400,27 @@ const componentTemplateGeneratorMap = {
400
400
 
401
401
  export function generateComponent(componentName, cmd, cliConfigFile) {
402
402
  const componentFileTypes = ['component', ...getCorrespondingComponentFileTypes(cmd)];
403
+ const generatedFiles = [];
404
+ let basePath = '';
403
405
 
404
406
  componentFileTypes.forEach((componentFileType) => {
405
407
  // --- Generate templates only if the component options (withStyle, withTest, etc..) are true,
406
408
  // or if the template type is "component"
407
409
 
408
410
  if (
409
- (cmd[componentFileType] && cmd[componentFileType].toString() === 'true') ||
410
- componentFileType === buildInComponentFileTypes.COMPONENT
411
+ (cmd[componentFileType] && cmd[componentFileType].toString() === 'true')
412
+ || componentFileType === buildInComponentFileTypes.COMPONENT
411
413
  ) {
412
414
  const generateTemplate = componentTemplateGeneratorMap[componentFileType] || customFileTemplateGenerator;
413
415
 
414
416
  const convertors = {
415
- templatename: componentName,
416
- TemplateName: startCase(camelCase(componentName)).replace(/ /g, ''),
417
- templateName: camelCase(componentName),
417
+ 'templatename': componentName,
418
+ 'TemplateName': startCase(camelCase(componentName)).replace(/ /g, ''),
419
+ 'templateName': camelCase(componentName),
418
420
  'template-name': kebabCase(componentName),
419
- template_name: snakeCase(componentName),
420
- TEMPLATE_NAME: snakeCase(componentName).toUpperCase(),
421
- TEMPLATENAME: componentName.toUpperCase(),
421
+ 'template_name': snakeCase(componentName),
422
+ 'TEMPLATE_NAME': snakeCase(componentName).toUpperCase(),
423
+ 'TEMPLATENAME': componentName.toUpperCase(),
422
424
  };
423
425
 
424
426
  const { componentPath, filename, template } = generateTemplate({
@@ -429,10 +431,15 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
429
431
  convertors,
430
432
  });
431
433
 
434
+ // Extract base path for summary
435
+ if (!basePath) {
436
+ basePath = path.dirname(componentPath);
437
+ }
438
+
432
439
  // --- Make sure the component does not already exist in the path directory.
433
440
 
434
441
  if (existsSync(componentPath)) {
435
- console.error(chalk.red(`${filename} already exists in this path "${componentPath}".`));
442
+ generatedFiles.push({ filename, status: 'skipped', path: componentPath });
436
443
  } else {
437
444
  try {
438
445
  if (!cmd.dryRun) {
@@ -441,7 +448,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
441
448
  // Will replace the templatename in whichever format the user typed the component name in the command.
442
449
  replace({
443
450
  regex: 'templatename',
444
- replacement: convertors['templatename'],
451
+ replacement: convertors.templatename,
445
452
  paths: [componentPath],
446
453
  recursive: false,
447
454
  silent: true,
@@ -450,7 +457,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
450
457
  // Will replace the TemplateName in PascalCase
451
458
  replace({
452
459
  regex: 'TemplateName',
453
- replacement: convertors['TemplateName'],
460
+ replacement: convertors.TemplateName,
454
461
  paths: [componentPath],
455
462
  recursive: false,
456
463
  silent: true,
@@ -459,7 +466,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
459
466
  // Will replace the templateName in camelCase
460
467
  replace({
461
468
  regex: 'templateName',
462
- replacement: convertors['templateName'],
469
+ replacement: convertors.templateName,
463
470
  paths: [componentPath],
464
471
  recursive: false,
465
472
  silent: true,
@@ -477,7 +484,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
477
484
  // Will replace the template_name in snake_case
478
485
  replace({
479
486
  regex: 'template_name',
480
- replacement: convertors['template_name'],
487
+ replacement: convertors.template_name,
481
488
  paths: [componentPath],
482
489
  recursive: false,
483
490
  silent: true,
@@ -486,7 +493,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
486
493
  // Will replace the TEMPLATE_NAME in uppercase SNAKE_CASE
487
494
  replace({
488
495
  regex: 'TEMPLATE_NAME',
489
- replacement: convertors['TEMPLATE_NAME'],
496
+ replacement: convertors.TEMPLATE_NAME,
490
497
  paths: [componentPath],
491
498
  recursive: false,
492
499
  silent: true,
@@ -495,24 +502,28 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
495
502
  // Will replace the TEMPLATENAME in uppercase SNAKE_CASE
496
503
  replace({
497
504
  regex: 'TEMPLATENAME',
498
- replacement: convertors['TEMPLATENAME'],
505
+ replacement: convertors.TEMPLATENAME,
499
506
  paths: [componentPath],
500
507
  recursive: false,
501
508
  silent: true,
502
509
  });
503
510
  }
504
511
 
505
- console.log(chalk.green(`${filename} was successfully created at ${componentPath}`));
506
- } catch (error) {
507
- console.error(chalk.red(`${filename} failed and was not created.`));
508
- console.error(error);
512
+ generatedFiles.push({ filename, status: 'created', path: componentPath });
513
+ } catch (err) {
514
+ generatedFiles.push({ filename, status: 'failed', path: componentPath });
515
+ error(`Failed to create ${filename}`, {
516
+ details: err.message,
517
+ suggestions: [
518
+ 'Check that you have write permissions to the target directory',
519
+ 'Verify the path is valid',
520
+ ],
521
+ });
509
522
  }
510
523
  }
511
524
  }
512
525
  });
513
526
 
514
- if (cmd.dryRun) {
515
- console.log();
516
- console.log(chalk.yellow(`NOTE: The "dry-run" flag means no changes were made.`));
517
- }
527
+ // Show summary
528
+ fileSummary(generatedFiles, basePath, { dryRun: cmd.dryRun });
518
529
  }
@@ -1,9 +1,9 @@
1
- import chalk from 'chalk';
2
-
3
- import deepKeys from 'deep-keys';
1
+ import fsExtra from 'fs-extra';
4
2
  import inquirer from 'inquirer';
3
+
5
4
  import merge from 'lodash/merge.js';
6
- import fsExtra from 'fs-extra';
5
+ import deepKeys from './deepKeysUtils.js';
6
+ import { blank, error, header, outro, success } from './messagesUtils.js';
7
7
 
8
8
  const { accessSync, constants, outputFileSync, readFileSync } = fsExtra;
9
9
  const { prompt } = inquirer;
@@ -25,19 +25,19 @@ const projectLevelQuestions = [
25
25
  },
26
26
  {
27
27
  type: 'confirm',
28
- when: (answers) => !answers['usesStyledComponents'],
28
+ when: answers => !answers.usesStyledComponents,
29
29
  name: 'usesCssModule',
30
30
  message: 'Does this project use CSS modules?',
31
31
  },
32
32
  {
33
- type: 'list',
33
+ type: 'select',
34
34
  name: 'cssPreprocessor',
35
- when: (answers) => !answers['usesStyledComponents'],
35
+ when: answers => !answers.usesStyledComponents,
36
36
  message: 'Does this project use a CSS Preprocessor?',
37
37
  choices: ['css', 'scss', 'less', 'styl'],
38
38
  },
39
39
  {
40
- type: 'list',
40
+ type: 'select',
41
41
  name: 'testLibrary',
42
42
  message: 'What testing library does your project use?',
43
43
  choices: ['Testing Library', 'Enzyme', 'None'],
@@ -50,23 +50,27 @@ export const componentLevelQuestions = [
50
50
  {
51
51
  type: 'input',
52
52
  name: 'component.default.path',
53
- message: 'Set the default path directory to where your components will be generated in?',
53
+ message:
54
+ 'Set the default path directory to where your components will be generated in?',
54
55
  default: () => 'src/components',
55
56
  },
56
57
  {
57
58
  type: 'confirm',
58
59
  name: 'component.default.withStyle',
59
- message: 'Would you like to create a corresponding stylesheet file with each component you generate?',
60
+ message:
61
+ 'Would you like to create a corresponding stylesheet file with each component you generate?',
60
62
  },
61
63
  {
62
64
  type: 'confirm',
63
65
  name: 'component.default.withTest',
64
- message: 'Would you like to create a corresponding test file with each component you generate?',
66
+ message:
67
+ 'Would you like to create a corresponding test file with each component you generate?',
65
68
  },
66
69
  {
67
70
  type: 'confirm',
68
71
  name: 'component.default.withStory',
69
- message: 'Would you like to create a corresponding story with each component you generate?',
72
+ message:
73
+ 'Would you like to create a corresponding story with each component you generate?',
70
74
  },
71
75
  {
72
76
  type: 'confirm',
@@ -78,97 +82,69 @@ export const componentLevelQuestions = [
78
82
 
79
83
  // --- merge all questions together.
80
84
 
81
- const grcConfigQuestions = [...projectLevelQuestions, ...componentLevelQuestions];
85
+ const grcConfigQuestions = [
86
+ ...projectLevelQuestions,
87
+ ...componentLevelQuestions,
88
+ ];
82
89
 
83
90
  async function createCLIConfigFile() {
84
91
  try {
85
- console.log();
86
- console.log(
87
- chalk.cyan(
88
- '--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------'
89
- )
90
- );
91
- console.log(
92
- chalk.cyan("It looks like this is the first time that you're running generate-react-cli within this project.")
92
+ header(
93
+ 'Welcome to Generate React CLI!',
94
+ 'Answer a few questions to customize the CLI for your project.',
93
95
  );
94
- console.log();
95
- console.log(
96
- chalk.cyan(
97
- 'Answer a few questions to customize generate-react-cli for your project needs (this will create a "generate-react-cli.json" config file on the root level of this project).'
98
- )
99
- );
100
- console.log(
101
- chalk.cyan(
102
- '--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------'
103
- )
104
- );
105
- console.log();
106
96
 
107
97
  const answers = await prompt(grcConfigQuestions);
108
98
 
109
99
  outputFileSync('generate-react-cli.json', JSON.stringify(answers, null, 2));
110
100
 
111
- console.log();
112
- console.log(
113
- chalk.cyan(
114
- 'The "generate-react-cli.json" config file has been successfully created on the root level of your project.'
115
- )
116
- );
117
-
118
- console.log('');
119
- console.log(chalk.cyan('You can always go back and update it as needed.'));
120
- console.log('');
121
- console.log(chalk.cyan('Happy Hacking!'));
122
- console.log('');
123
- console.log('');
101
+ blank();
102
+ success('Config file created successfully');
103
+ blank();
104
+ outro('You can always update it manually. Happy Hacking!');
124
105
 
125
106
  return answers;
126
107
  } catch (e) {
127
- console.error(chalk.red.bold('ERROR: Could not create a "generate-react-cli.json" config file.'));
108
+ error('Could not create config file', {
109
+ details: 'Failed to write generate-react-cli.json',
110
+ suggestions: [
111
+ 'Check that you have write permissions in this directory',
112
+ 'Make sure the directory is not read-only',
113
+ ],
114
+ });
128
115
  return e;
129
116
  }
130
117
  }
131
118
 
132
119
  async function updateCLIConfigFile(missingConfigQuestions, currentConfigFile) {
133
120
  try {
134
- console.log('');
135
- console.log(
136
- chalk.cyan(
137
- '------------------------------------------------------------------------------------------------------------------------------'
138
- )
139
- );
140
- console.log(
141
- chalk.cyan(
142
- 'Generate React CLI has been updated and has a few new features from the last time you ran it within this project.'
143
- )
121
+ header(
122
+ 'Generate React CLI has new features!',
123
+ 'Answer a few questions to update your config file.',
144
124
  );
145
- console.log('');
146
- console.log(chalk.cyan('Please answer a few questions to update the "generate-react-cli.json" config file.'));
147
- console.log(
148
- chalk.cyan(
149
- '------------------------------------------------------------------------------------------------------------------------------'
150
- )
151
- );
152
- console.log('');
153
125
 
154
126
  const answers = await prompt(missingConfigQuestions);
155
127
  const updatedAnswers = merge({}, currentConfigFile, answers);
156
128
 
157
- outputFileSync('generate-react-cli.json', JSON.stringify(updatedAnswers, null, 2));
158
-
159
- console.log();
160
- console.log(chalk.cyan('The ("generate-react-cli.json") has successfully updated for this project.'));
129
+ outputFileSync(
130
+ 'generate-react-cli.json',
131
+ JSON.stringify(updatedAnswers, null, 2),
132
+ );
161
133
 
162
- console.log();
163
- console.log(chalk.cyan('You can always go back and manually update it as needed.'));
164
- console.log();
165
- console.log(chalk.cyan('Happy Hacking!'));
166
- console.log();
167
- console.log();
134
+ blank();
135
+ success('Config file updated successfully');
136
+ blank();
137
+ outro('You can always update it manually. Happy Hacking!');
168
138
 
169
139
  return updatedAnswers;
170
140
  } catch (e) {
171
- console.error(chalk.red.bold('ERROR: Could not update the "generate-react-cli.json" config file.'));
141
+ error('Could not update config file', {
142
+ details: 'Failed to write generate-react-cli.json',
143
+ suggestions: [
144
+ 'Check that the file is not locked or read-only',
145
+ 'Verify you have write permissions',
146
+ ],
147
+ });
172
148
  return e;
173
149
  }
174
150
  }
@@ -183,7 +159,9 @@ export async function getCLIConfigFile() {
183
159
 
184
160
  try {
185
161
  accessSync('./generate-react-cli.json', constants.R_OK);
186
- const currentConfigFile = JSON.parse(readFileSync('./generate-react-cli.json'));
162
+ const currentConfigFile = JSON.parse(
163
+ readFileSync('./generate-react-cli.json'),
164
+ );
187
165
 
188
166
  /**
189
167
  * Check to see if there's a difference between grcConfigQuestions and the currentConfigFile.
@@ -191,25 +169,30 @@ export async function getCLIConfigFile() {
191
169
  */
192
170
 
193
171
  const missingConfigQuestions = grcConfigQuestions.filter(
194
- (question) =>
195
- !deepKeys(currentConfigFile).includes(question.name) &&
196
- (question.when ? question.when(currentConfigFile) : true)
172
+ question =>
173
+ !deepKeys(currentConfigFile).includes(question.name)
174
+ && (question.when ? question.when(currentConfigFile) : true),
197
175
  );
198
176
 
199
177
  if (missingConfigQuestions.length) {
200
- return await updateCLIConfigFile(missingConfigQuestions, currentConfigFile);
178
+ return await updateCLIConfigFile(
179
+ missingConfigQuestions,
180
+ currentConfigFile,
181
+ );
201
182
  }
202
183
 
203
184
  return currentConfigFile;
204
- } catch (e) {
185
+ } catch {
205
186
  return await createCLIConfigFile();
206
187
  }
207
- } catch (error) {
208
- console.error(
209
- chalk.red.bold(
210
- "ERROR: Please make sure that you're running the generate-react-cli commands from the root level of your React project"
211
- )
212
- );
188
+ } catch {
189
+ error('Not in project root', {
190
+ details: 'Could not find package.json in current directory',
191
+ suggestions: [
192
+ 'Run this command from your project root directory',
193
+ 'Make sure package.json exists in the current directory',
194
+ ],
195
+ });
213
196
  return process.exit(1);
214
197
  }
215
198
  }
@@ -0,0 +1,137 @@
1
+ import chalk from 'chalk';
2
+
3
+ // Symbols for consistent visual feedback
4
+ const symbols = {
5
+ success: chalk.green('✓'),
6
+ error: chalk.red('✖'),
7
+ warning: chalk.yellow('⚠'),
8
+ info: chalk.blue('ℹ'),
9
+ arrow: chalk.cyan('→'),
10
+ bullet: chalk.dim('•'),
11
+ };
12
+
13
+ // Create a responsive divider that adapts to terminal width
14
+ function divider(color = 'cyan') {
15
+ const width = Math.min(process.stdout.columns || 80, 80);
16
+ return chalk[color]('─'.repeat(width));
17
+ }
18
+
19
+ // Success message with optional file path
20
+ export function success(message, filePath) {
21
+ if (filePath) {
22
+ console.log(` ${symbols.success} ${chalk.green(message)}`);
23
+ console.log(` ${symbols.arrow} ${chalk.dim(filePath)}`);
24
+ } else {
25
+ console.log(`${symbols.success} ${chalk.green(message)}`);
26
+ }
27
+ }
28
+
29
+ // Error message with optional details and suggestions
30
+ export function error(message, { details, suggestions } = {}) {
31
+ console.log();
32
+ console.log(`${symbols.error} ${chalk.red.bold('ERROR:')} ${chalk.red(message)}`);
33
+
34
+ if (details) {
35
+ console.log(` ${chalk.dim(details)}`);
36
+ }
37
+
38
+ if (suggestions && suggestions.length > 0) {
39
+ console.log();
40
+ console.log(chalk.dim(' Try one of:'));
41
+ suggestions.forEach((suggestion) => {
42
+ console.log(` ${symbols.bullet} ${chalk.dim(suggestion)}`);
43
+ });
44
+ }
45
+ console.log();
46
+ }
47
+
48
+ // Warning message
49
+ export function warning(message) {
50
+ console.log(`${symbols.warning} ${chalk.yellow(message)}`);
51
+ }
52
+
53
+ // Info message
54
+ export function info(message) {
55
+ console.log(`${symbols.info} ${chalk.cyan(message)}`);
56
+ }
57
+
58
+ // Header for sections (like config setup intro)
59
+ export function header(title, subtitle) {
60
+ console.log();
61
+ console.log(divider());
62
+ console.log(chalk.cyan.bold(title));
63
+ if (subtitle) {
64
+ console.log(chalk.dim(subtitle));
65
+ }
66
+ console.log(divider());
67
+ console.log();
68
+ }
69
+
70
+ // Summary after file generation
71
+ export function fileSummary(files, basePath, { dryRun = false } = {}) {
72
+ console.log();
73
+
74
+ const createdFiles = files.filter(f => f.status === 'created');
75
+ const skippedFiles = files.filter(f => f.status === 'skipped');
76
+
77
+ if (dryRun) {
78
+ // Dry-run mode: show what would happen
79
+ console.log(`${symbols.info} ${chalk.cyan('Dry-run mode')} ${chalk.dim('- no files were created')}`);
80
+ console.log();
81
+
82
+ if (createdFiles.length > 0) {
83
+ console.log(chalk.dim(`Would create in ${basePath}:`));
84
+ createdFiles.forEach((file, index) => {
85
+ const isLast = index === createdFiles.length - 1 && skippedFiles.length === 0;
86
+ const prefix = isLast ? '└──' : '├──';
87
+ console.log(` ${chalk.dim(prefix)} ${file.filename}`);
88
+ });
89
+ }
90
+
91
+ if (skippedFiles.length > 0) {
92
+ if (createdFiles.length > 0) {
93
+ console.log();
94
+ }
95
+ console.log(chalk.dim('Already exist (would be skipped):'));
96
+ skippedFiles.forEach((file, index) => {
97
+ const isLast = index === skippedFiles.length - 1;
98
+ const prefix = isLast ? '└──' : '├──';
99
+ console.log(` ${chalk.dim(prefix)} ${symbols.warning} ${chalk.dim(file.filename)}`);
100
+ });
101
+ }
102
+ } else {
103
+ // Actual run: show what happened
104
+ if (createdFiles.length > 0) {
105
+ console.log(`${symbols.success} ${chalk.green(`Created ${createdFiles.length} file${createdFiles.length === 1 ? '' : 's'} in ${basePath}`)}`);
106
+ }
107
+ if (skippedFiles.length > 0) {
108
+ console.log(`${symbols.warning} ${chalk.yellow(`Skipped ${skippedFiles.length} file${skippedFiles.length === 1 ? '' : 's'} (already exist)`)}`);
109
+ }
110
+ console.log();
111
+
112
+ // Show file tree with status icons
113
+ files.forEach((file, index) => {
114
+ const isLast = index === files.length - 1;
115
+ const prefix = isLast ? '└──' : '├──';
116
+ const statusIcon = file.status === 'created'
117
+ ? symbols.success
118
+ : file.status === 'skipped'
119
+ ? symbols.warning
120
+ : symbols.error;
121
+ console.log(` ${chalk.dim(prefix)} ${statusIcon} ${file.filename}`);
122
+ });
123
+ }
124
+
125
+ console.log();
126
+ }
127
+
128
+ // Closing message (Happy Hacking)
129
+ export function outro(message = 'Happy Hacking!') {
130
+ console.log(chalk.cyan(message));
131
+ console.log();
132
+ }
133
+
134
+ // Blank line helper
135
+ export function blank() {
136
+ console.log();
137
+ }