generate-react-cli 10.1.0 → 11.0.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.
@@ -12,7 +12,6 @@ function isNotValidNodeVersion() {
12
12
  details: 'Generate React CLI requires Node 22 or higher',
13
13
  suggestions: [
14
14
  'Update your Node.js version to 22 or higher',
15
- 'Use nvm to manage multiple Node versions: nvm install 22',
16
15
  ],
17
16
  });
18
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generate-react-cli",
3
- "version": "10.1.0",
3
+ "version": "11.0.0",
4
4
  "description": "A simple React CLI to generate components instantly and more.",
5
5
  "author": "Armin Broubakarian",
6
6
  "license": "MIT",
package/readme.md CHANGED
@@ -55,6 +55,12 @@ When you run GRC within your project the first time, it will ask you a series of
55
55
  }
56
56
  ```
57
57
 
58
+ #### Test library options:
59
+
60
+ - `Testing Library` - Generates tests using [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
61
+ - `Vitest` - Generates tests using [Vitest](https://vitest.dev/) with React Testing Library
62
+ - `None` - Generates basic tests using React's createRoot API
63
+
58
64
  ## Generate Components
59
65
 
60
66
  ```sh
@@ -119,7 +125,7 @@ Otherwise, if you don't pass any options, it will just use the default values th
119
125
  <tr>
120
126
  <td width="20%"><b>--withLazy</b></td>
121
127
  <td width="60%">
122
- Creates a corresponding lazy file (a file that lazy-loads your component out of the box and enables <a href="https://reactjs.org/docs/code-splitting.html#code-splitting">code splitting</a>) with this component.
128
+ Creates a corresponding lazy file (a file that lazy-loads your component out of the box and enables <a href="https://react.dev/reference/react/lazy">code splitting</a>) with this component.
123
129
  </td>
124
130
  <td width="20%">Boolean</td>
125
131
  <td width="20%"><code>component.default.withLazy<code></td>
@@ -295,7 +301,6 @@ Notice in the `page.customTemplates` that we only specified the `test` custom te
295
301
  ```jsx
296
302
  // templates/component/TemplateName.js
297
303
 
298
- import React from 'react';
299
304
  import styles from './TemplateName.module.css';
300
305
 
301
306
  const TemplateName = () => (
@@ -324,14 +329,14 @@ export default TemplateName;
324
329
  ```jsx
325
330
  // templates/component/TemplateName.test.js
326
331
 
327
- import React from 'react';
328
- import ReactDOM from 'react-dom';
332
+ import { createRoot } from 'react-dom/client';
329
333
  import TemplateName from './TemplateName';
330
334
 
331
- it('It should mount', () => {
332
- const div = document.createElement('div');
333
- ReactDOM.render(<TemplateName />, div);
334
- ReactDOM.unmountComponentAtNode(div);
335
+ it('should mount', () => {
336
+ const container = document.createElement('div');
337
+ const root = createRoot(container);
338
+ root.render(<TemplateName />);
339
+ root.unmount();
335
340
  });
336
341
  ```
337
342
 
@@ -1,6 +1,4 @@
1
- export default `import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import styles from './templatename.module.css';
1
+ export default `import styles from './templatename.module.css';
4
2
 
5
3
  const templatename = () => (
6
4
  <div className={styles.templatename} data-testid="templatename">
@@ -8,9 +6,5 @@ const templatename = () => (
8
6
  </div>
9
7
  );
10
8
 
11
- templatename.propTypes = {};
12
-
13
- templatename.defaultProps = {};
14
-
15
9
  export default templatename;
16
10
  `;
@@ -1,8 +1,8 @@
1
- export default `import React, { lazy, Suspense } from 'react';
1
+ export default `import { lazy, Suspense } from 'react';
2
2
 
3
3
  const Lazytemplatename = lazy(() => import('./templatename'));
4
4
 
5
- const templatename = props => (
5
+ const templatename = (props) => (
6
6
  <Suspense fallback={null}>
7
7
  <Lazytemplatename {...props} />
8
8
  </Suspense>
@@ -1,13 +1,9 @@
1
- export default `/* eslint-disable */
2
- import templatename from './templatename';
1
+ export default `import templatename from './templatename';
3
2
 
4
3
  export default {
5
- title: "templatename",
4
+ title: 'templatename',
5
+ component: templatename,
6
6
  };
7
7
 
8
- export const Default = () => <templatename />;
9
-
10
- Default.story = {
11
- name: 'default',
12
- };
8
+ export const Default = {};
13
9
  `;
@@ -1,9 +1,9 @@
1
- export default `import React from 'react';
2
- import ReactDOM from 'react-dom';
1
+ export default `import { createRoot } from 'react-dom/client';
3
2
  import templatename from './templatename';
4
3
 
5
- it('It should mount', () => {
6
- const div = document.createElement('div');
7
- ReactDOM.render(<templatename />, div);
8
- ReactDOM.unmountComponentAtNode(div);
4
+ it('should mount', () => {
5
+ const container = document.createElement('div');
6
+ const root = createRoot(container);
7
+ root.render(<templatename />);
8
+ root.unmount();
9
9
  });`;
@@ -1,10 +1,9 @@
1
- export default `import React from 'react';
2
- import { render, screen } from '@testing-library/react';
1
+ export default `import { render, screen } from '@testing-library/react';
3
2
  import '@testing-library/jest-dom';
4
3
  import templatename from './templatename';
5
4
 
6
5
  describe('<templatename />', () => {
7
- test('it should mount', () => {
6
+ test('should mount', () => {
8
7
  render(<templatename />);
9
8
 
10
9
  const templateName = screen.getByTestId('templatename');
@@ -0,0 +1,15 @@
1
+ export default `import { describe, test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import templatename from './templatename';
5
+
6
+ describe('<templatename />', () => {
7
+ test('should mount', () => {
8
+ render(<templatename />);
9
+
10
+ const templateName = screen.getByTestId('templatename');
11
+
12
+ expect(templateName).toBeInTheDocument();
13
+ });
14
+ });
15
+ `;
@@ -1,8 +1,8 @@
1
- export default `import React, { lazy, Suspense } from 'react';
1
+ export default `import { lazy, Suspense, ComponentProps } from 'react';
2
2
 
3
3
  const Lazytemplatename = lazy(() => import('./templatename'));
4
4
 
5
- const templatename = (props: JSX.IntrinsicAttributes & { children?: React.ReactNode; }) => (
5
+ const templatename = (props: ComponentProps<typeof Lazytemplatename>) => (
6
6
  <Suspense fallback={null}>
7
7
  <Lazytemplatename {...props} />
8
8
  </Suspense>
@@ -1,4 +1,4 @@
1
- export default `import React, { FC } from 'react';
1
+ export default `import type { FC } from 'react';
2
2
  import styles from './templatename.module.css';
3
3
 
4
4
  interface templatenameProps {}
@@ -12,13 +12,13 @@ import componentLazyTemplate from '../templates/component/componentLazyTemplate.
12
12
  import componentStoryTemplate from '../templates/component/componentStoryTemplate.js';
13
13
  import componentStyledTemplate from '../templates/component/componentStyledTemplate.js';
14
14
  import componentTestDefaultTemplate from '../templates/component/componentTestDefaultTemplate.js';
15
- import componentTestEnzymeTemplate from '../templates/component/componentTestEnzymeTemplate.js';
16
15
  import componentTestTestingLibraryTemplate from '../templates/component/componentTestTestingLibraryTemplate.js';
16
+ import componentTestVitestTemplate from '../templates/component/componentTestVitestTemplate.js';
17
17
  import componentTsLazyTemplate from '../templates/component/componentTsLazyTemplate.js';
18
18
  import componentTsTemplate from '../templates/component/componentTsTemplate.js';
19
- import { error, fileSummary } from './messagesUtils.js';
19
+ import { error, exitWithError, fileSummary } from './messagesUtils.js';
20
20
 
21
- const templateNameRE = /.*(template[|_-]?name).*/i;
21
+ const TEMPLATE_NAME_REGEX = /template[-_]?name/i;
22
22
 
23
23
  const { existsSync, outputFileSync, readFileSync } = fsExtra;
24
24
 
@@ -35,15 +35,13 @@ export function getComponentByType(args, cliConfigFile) {
35
35
 
36
36
  if (!selectedComponentType) {
37
37
  const availableTypes = Object.keys(cliConfigFile.component).join(', ');
38
- error(`Unknown component type "${componentType}"`, {
38
+ exitWithError(`Unknown component type "${componentType}"`, {
39
39
  details: `Available types: ${availableTypes}`,
40
40
  suggestions: [
41
41
  `Use one of the available types: ${availableTypes}`,
42
42
  'Add this component type to your generate-react-cli.json config',
43
43
  ],
44
44
  });
45
-
46
- process.exit(1);
47
45
  }
48
46
 
49
47
  // Otherwise return it.
@@ -61,7 +59,7 @@ export function getCorrespondingComponentFileTypes(component) {
61
59
  }
62
60
 
63
61
  function getCustomTemplate(componentName, templatePath) {
64
- // --- Try loading custom template
62
+ // Try loading custom template
65
63
 
66
64
  try {
67
65
  const template = readFileSync(templatePath, 'utf8');
@@ -69,15 +67,13 @@ function getCustomTemplate(componentName, templatePath) {
69
67
 
70
68
  return { template, filename };
71
69
  } catch {
72
- error(`Custom template not found: "${templatePath}"`, {
70
+ exitWithError(`Custom template not found: "${templatePath}"`, {
73
71
  suggestions: [
74
72
  'Verify the template path in your generate-react-cli.json config',
75
73
  'Check that the file exists and is readable',
76
74
  'Use an absolute path or a path relative to project root',
77
75
  ],
78
76
  });
79
-
80
- return process.exit(1);
81
77
  }
82
78
  }
83
79
 
@@ -97,17 +93,15 @@ function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, fi
97
93
  if (customDirectoryConfigs.length > 0) {
98
94
  const customDirectory = customDirectoryConfigs.slice(-1).toString();
99
95
 
100
- // Double check if the customDirectory is templatable
101
- if (templateNameRE.exec(customDirectory) == null) {
102
- error(`Invalid customDirectory: "${customDirectory}"`, {
96
+ // Check if the customDirectory contains a template placeholder
97
+ if (!TEMPLATE_NAME_REGEX.test(customDirectory)) {
98
+ exitWithError(`Invalid customDirectory: "${customDirectory}"`, {
103
99
  details: 'customDirectory must contain a template placeholder',
104
100
  suggestions: [
105
101
  'Use templatename, TemplateName, template-name, or template_name',
106
102
  'Example: "{{templatename}}" or "TemplateName"',
107
103
  ],
108
104
  });
109
-
110
- process.exit(-2);
111
105
  }
112
106
 
113
107
  for (const convertor in convertors) {
@@ -137,7 +131,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
137
131
  // Check for a custom component template.
138
132
 
139
133
  if (customTemplates && customTemplates.component) {
140
- // --- Load and use the custom component template
134
+ // Load and use the custom component template
141
135
 
142
136
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
143
137
  componentName,
@@ -147,18 +141,19 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
147
141
  template = customTemplate;
148
142
  filename = customTemplateFilename;
149
143
  } else {
150
- // --- Else use GRC built-in component template
144
+ // Else use GRC built-in component template
151
145
 
152
146
  template = usesTypeScript ? componentTsTemplate : componentJsTemplate;
153
147
  filename = usesTypeScript ? `${componentName}.tsx` : `${componentName}.js`;
154
148
 
155
- // --- If test library is not Testing Library or if withTest is false. Remove data-testid from template
149
+ // If test library doesn't use data-testid or if withTest is false. Remove data-testid from template
156
150
 
157
- if (testLibrary !== 'Testing Library' || !cmd.withTest) {
151
+ const usesTestId = testLibrary === 'Testing Library' || testLibrary === 'Vitest';
152
+ if (!usesTestId || !cmd.withTest) {
158
153
  template = template.replace(` data-testid="templatename"`, '');
159
154
  }
160
155
 
161
- // --- If it has a corresponding stylesheet
156
+ // If it has a corresponding stylesheet
162
157
 
163
158
  if (cmd.withStyle) {
164
159
  if (cliConfigFile.usesStyledComponents) {
@@ -174,7 +169,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
174
169
  const module = usesCssModule ? '.module' : '';
175
170
  const cssPath = `${componentName}${module}.${cssPreprocessor}`;
176
171
 
177
- // --- If the css module is true make sure to update the template accordingly
172
+ // If the css module is true make sure to update the template accordingly
178
173
 
179
174
  if (module.length) {
180
175
  template = template.replace(`'./templatename.module.css'`, `'./${cssPath}'`);
@@ -184,7 +179,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
184
179
  }
185
180
  }
186
181
  } else {
187
- // --- If no stylesheet, remove className attribute and style import from jsTemplate
182
+ // If no stylesheet, remove className attribute and style import from jsTemplate
188
183
 
189
184
  template = template.replace(` className={styles.templatename}`, '');
190
185
  template = template.replace(`import styles from './templatename.module.css';`, '');
@@ -206,7 +201,7 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, co
206
201
  // Check for a custom style template.
207
202
 
208
203
  if (customTemplates && customTemplates.style) {
209
- // --- Load and use the custom style template
204
+ // Load and use the custom style template
210
205
 
211
206
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
212
207
  componentName,
@@ -224,7 +219,7 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, co
224
219
  const module = usesCssModule ? '.module' : '';
225
220
  const cssFilename = `${componentName}${module}.${cssPreprocessor}`;
226
221
 
227
- // --- Else use GRC built-in style template
222
+ // Else use GRC built-in style template
228
223
 
229
224
  template = componentCssTemplate;
230
225
  filename = cssFilename;
@@ -247,7 +242,7 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, con
247
242
  // Check for a custom test template.
248
243
 
249
244
  if (customTemplates && customTemplates.test) {
250
- // --- Load and use the custom test template
245
+ // Load and use the custom test template
251
246
 
252
247
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
253
248
  componentName,
@@ -259,12 +254,10 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, con
259
254
  } else {
260
255
  filename = usesTypeScript ? `${componentName}.test.tsx` : `${componentName}.test.js`;
261
256
 
262
- if (testLibrary === 'Enzyme') {
263
- // --- Else use GRC built-in test template based on test library type
264
-
265
- template = componentTestEnzymeTemplate;
266
- } else if (testLibrary === 'Testing Library') {
257
+ if (testLibrary === 'Testing Library') {
267
258
  template = componentTestTestingLibraryTemplate;
259
+ } else if (testLibrary === 'Vitest') {
260
+ template = componentTestVitestTemplate;
268
261
  } else {
269
262
  template = componentTestDefaultTemplate;
270
263
  }
@@ -286,7 +279,7 @@ function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName, co
286
279
  // Check for a custom story template.
287
280
 
288
281
  if (customTemplates && customTemplates.story) {
289
- // --- Load and use the custom story template
282
+ // Load and use the custom story template
290
283
 
291
284
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
292
285
  componentName,
@@ -296,7 +289,7 @@ function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName, co
296
289
  template = customTemplate;
297
290
  filename = customTemplateFilename;
298
291
  } else {
299
- // --- Else use GRC built-in story template
292
+ // Else use GRC built-in story template
300
293
 
301
294
  template = componentStoryTemplate;
302
295
  filename = usesTypeScript ? `${componentName}.stories.tsx` : `${componentName}.stories.js`;
@@ -318,7 +311,7 @@ function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile, con
318
311
  // Check for a custom lazy template.
319
312
 
320
313
  if (customTemplates && customTemplates.lazy) {
321
- // --- Load and use the custom lazy template
314
+ // Load and use the custom lazy template
322
315
 
323
316
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
324
317
  componentName,
@@ -328,7 +321,7 @@ function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile, con
328
321
  template = customTemplate;
329
322
  filename = customTemplateFilename;
330
323
  } else {
331
- // --- Else use GRC built-in lazy template
324
+ // Else use GRC built-in lazy template
332
325
 
333
326
  template = usesTypeScript ? componentTsLazyTemplate : componentLazyTemplate;
334
327
  filename = usesTypeScript ? `${componentName}.lazy.tsx` : `${componentName}.lazy.js`;
@@ -350,18 +343,16 @@ function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, compon
350
343
  // Check for a valid custom template for the corresponding custom component file.
351
344
 
352
345
  if (!customTemplates || !customTemplates[fileType]) {
353
- error(`Missing custom template for "${fileType}"`, {
346
+ exitWithError(`Missing custom template for "${fileType}"`, {
354
347
  details: 'Custom component files require a matching custom template',
355
348
  suggestions: [
356
349
  `Add a "${fileType}" template path to customTemplates in your config`,
357
350
  'Check that the template file exists at the specified path',
358
351
  ],
359
352
  });
360
-
361
- return process.exit(1);
362
353
  }
363
354
 
364
- // --- Load and use the custom component template.
355
+ // Load and use the custom component template.
365
356
 
366
357
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
367
358
  componentName,
@@ -378,7 +369,7 @@ function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, compon
378
369
  };
379
370
  }
380
371
 
381
- // --- Built in component file types
372
+ // Built in component file types
382
373
 
383
374
  const buildInComponentFileTypes = {
384
375
  COMPONENT: 'component',
@@ -388,7 +379,7 @@ const buildInComponentFileTypes = {
388
379
  LAZY: 'withLazy',
389
380
  };
390
381
 
391
- // --- Generate component template map
382
+ // Generate component template map
392
383
 
393
384
  const componentTemplateGeneratorMap = {
394
385
  [buildInComponentFileTypes.COMPONENT]: componentTemplateGenerator,
@@ -404,7 +395,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
404
395
  let basePath = '';
405
396
 
406
397
  componentFileTypes.forEach((componentFileType) => {
407
- // --- Generate templates only if the component options (withStyle, withTest, etc..) are true,
398
+ // Generate templates only if the component options (withStyle, withTest, etc..) are true,
408
399
  // or if the template type is "component"
409
400
 
410
401
  if (
@@ -436,7 +427,7 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
436
427
  basePath = path.dirname(componentPath);
437
428
  }
438
429
 
439
- // --- Make sure the component does not already exist in the path directory.
430
+ // Make sure the component does not already exist in the path directory.
440
431
 
441
432
  if (existsSync(componentPath)) {
442
433
  generatedFiles.push({ filename, status: 'skipped', path: componentPath });
@@ -445,67 +436,9 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
445
436
  if (!cmd.dryRun) {
446
437
  outputFileSync(componentPath, template);
447
438
 
448
- // Will replace the templatename in whichever format the user typed the component name in the command.
449
- replace({
450
- regex: 'templatename',
451
- replacement: convertors.templatename,
452
- paths: [componentPath],
453
- recursive: false,
454
- silent: true,
455
- });
456
-
457
- // Will replace the TemplateName in PascalCase
458
- replace({
459
- regex: 'TemplateName',
460
- replacement: convertors.TemplateName,
461
- paths: [componentPath],
462
- recursive: false,
463
- silent: true,
464
- });
465
-
466
- // Will replace the templateName in camelCase
467
- replace({
468
- regex: 'templateName',
469
- replacement: convertors.templateName,
470
- paths: [componentPath],
471
- recursive: false,
472
- silent: true,
473
- });
474
-
475
- // Will replace the template-name in kebab-case
476
- replace({
477
- regex: 'template-name',
478
- replacement: convertors['template-name'],
479
- paths: [componentPath],
480
- recursive: false,
481
- silent: true,
482
- });
483
-
484
- // Will replace the template_name in snake_case
485
- replace({
486
- regex: 'template_name',
487
- replacement: convertors.template_name,
488
- paths: [componentPath],
489
- recursive: false,
490
- silent: true,
491
- });
492
-
493
- // Will replace the TEMPLATE_NAME in uppercase SNAKE_CASE
494
- replace({
495
- regex: 'TEMPLATE_NAME',
496
- replacement: convertors.TEMPLATE_NAME,
497
- paths: [componentPath],
498
- recursive: false,
499
- silent: true,
500
- });
501
-
502
- // Will replace the TEMPLATENAME in uppercase SNAKE_CASE
503
- replace({
504
- regex: 'TEMPLATENAME',
505
- replacement: convertors.TEMPLATENAME,
506
- paths: [componentPath],
507
- recursive: false,
508
- silent: true,
439
+ // Replace all template placeholders with their corresponding component name formats
440
+ Object.entries(convertors).forEach(([pattern, replacement]) => {
441
+ replace({ regex: pattern, replacement, paths: [componentPath], recursive: false, silent: true });
509
442
  });
510
443
  }
511
444
 
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
3
3
 
4
4
  import merge from 'lodash/merge.js';
5
5
  import deepKeys from './deepKeysUtils.js';
6
- import { blank, error, header, outro, success } from './messagesUtils.js';
6
+ import { blank, error, exitWithError, header, outro, success } from './messagesUtils.js';
7
7
 
8
8
  const { accessSync, constants, outputFileSync, readFileSync } = fsExtra;
9
9
  const { prompt } = inquirer;
@@ -40,7 +40,7 @@ const projectLevelQuestions = [
40
40
  type: 'select',
41
41
  name: 'testLibrary',
42
42
  message: 'What testing library does your project use?',
43
- choices: ['Testing Library', 'Enzyme', 'None'],
43
+ choices: ['Testing Library', 'Vitest', 'None'],
44
44
  },
45
45
  ];
46
46
 
@@ -76,7 +76,7 @@ export const componentLevelQuestions = [
76
76
  type: 'confirm',
77
77
  name: 'component.default.withLazy',
78
78
  message:
79
- 'Would you like to create a corresponding lazy file (a file that lazy-loads your component out of the box and enables code splitting: https://reactjs.org/docs/code-splitting.html#code-splitting) with each component you generate?',
79
+ 'Would you like to create a corresponding lazy file (a file that lazy-loads your component out of the box and enables code splitting: https://react.dev/reference/react/lazy) with each component you generate?',
80
80
  },
81
81
  ];
82
82
 
@@ -99,9 +99,9 @@ async function createCLIConfigFile() {
99
99
  outputFileSync('generate-react-cli.json', JSON.stringify(answers, null, 2));
100
100
 
101
101
  blank();
102
- success('Config file created successfully');
102
+ success('Created the generate-react-cli.json config file');
103
103
  blank();
104
- outro('You can always update it manually. Happy Hacking!');
104
+ outro('You can always update the config file manually. Happy Hacking!');
105
105
 
106
106
  return answers;
107
107
  } catch (e) {
@@ -132,9 +132,9 @@ async function updateCLIConfigFile(missingConfigQuestions, currentConfigFile) {
132
132
  );
133
133
 
134
134
  blank();
135
- success('Config file updated successfully');
135
+ success('Updated the generate-react-cli.json config file');
136
136
  blank();
137
- outro('You can always update it manually. Happy Hacking!');
137
+ outro('You can always update the config file manually. Happy Hacking!');
138
138
 
139
139
  return updatedAnswers;
140
140
  } catch (e) {
@@ -186,13 +186,12 @@ export async function getCLIConfigFile() {
186
186
  return await createCLIConfigFile();
187
187
  }
188
188
  } catch {
189
- error('Not in project root', {
189
+ exitWithError('Not in project root', {
190
190
  details: 'Could not find package.json in current directory',
191
191
  suggestions: [
192
192
  'Run this command from your project root directory',
193
193
  'Make sure package.json exists in the current directory',
194
194
  ],
195
195
  });
196
- return process.exit(1);
197
196
  }
198
197
  }
@@ -1,5 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
 
3
+ const DEFAULT_TERMINAL_WIDTH = 80;
4
+
3
5
  // Symbols for consistent visual feedback
4
6
  const symbols = {
5
7
  success: chalk.green('✓'),
@@ -12,10 +14,14 @@ const symbols = {
12
14
 
13
15
  // Create a responsive divider that adapts to terminal width
14
16
  function divider(color = 'cyan') {
15
- const width = Math.min(process.stdout.columns || 80, 80);
17
+ const width = Math.min(process.stdout.columns || DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_WIDTH);
16
18
  return chalk[color]('─'.repeat(width));
17
19
  }
18
20
 
21
+ function pluralize(count, word) {
22
+ return count === 1 ? word : `${word}s`;
23
+ }
24
+
19
25
  // Success message with optional file path
20
26
  export function success(message, filePath) {
21
27
  if (filePath) {
@@ -45,6 +51,12 @@ export function error(message, { details, suggestions } = {}) {
45
51
  console.log();
46
52
  }
47
53
 
54
+ // Error message with exit
55
+ export function exitWithError(message, options = {}, exitCode = 1) {
56
+ error(message, options);
57
+ process.exit(exitCode);
58
+ }
59
+
48
60
  // Warning message
49
61
  export function warning(message) {
50
62
  console.log(`${symbols.warning} ${chalk.yellow(message)}`);
@@ -102,12 +114,11 @@ export function fileSummary(files, basePath, { dryRun = false } = {}) {
102
114
  } else {
103
115
  // Actual run: show what happened
104
116
  if (createdFiles.length > 0) {
105
- console.log(`${symbols.success} ${chalk.green(`Created ${createdFiles.length} file${createdFiles.length === 1 ? '' : 's'} in ${basePath}`)}`);
117
+ console.log(`${symbols.success} ${chalk.green(`Created ${createdFiles.length} ${pluralize(createdFiles.length, 'file')} in ${basePath}`)}`);
106
118
  }
107
119
  if (skippedFiles.length > 0) {
108
- console.log(`${symbols.warning} ${chalk.yellow(`Skipped ${skippedFiles.length} file${skippedFiles.length === 1 ? '' : 's'} (already exist)`)}`);
120
+ console.log(`${symbols.warning} ${chalk.yellow(`Skipped ${skippedFiles.length} ${pluralize(skippedFiles.length, 'file')} (already exist)`)}`);
109
121
  }
110
- console.log();
111
122
 
112
123
  // Show file tree with status icons
113
124
  files.forEach((file, index) => {
@@ -1,16 +0,0 @@
1
- export default `import React from 'react';
2
- import { shallow } from 'enzyme';
3
- import templatename from './templatename';
4
-
5
- describe('<templatename />', () => {
6
- let component;
7
-
8
- beforeEach(() => {
9
- component = shallow(<templatename />);
10
- });
11
-
12
- test('It should mount', () => {
13
- expect(component.length).toBe(1);
14
- });
15
- });
16
- `;