generate-react-cli 10.0.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.
@@ -1,24 +1,25 @@
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
+ ],
16
+ });
16
17
 
17
18
  return true;
18
19
  }
19
20
 
20
21
  return false;
21
- };
22
+ }
22
23
 
23
24
  // --- Check if user is running Node 12 or higher.
24
25
 
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
2
  "name": "generate-react-cli",
3
- "version": "10.0.0",
3
+ "version": "11.0.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
31
  "node": ">=22",
32
32
  "npm": ">=10"
@@ -48,7 +48,7 @@
48
48
  "replace": "1.2.2"
49
49
  },
50
50
  "devDependencies": {
51
- "@antfu/eslint-config": "4.12.0",
51
+ "@antfu/eslint-config": "7.0.1",
52
52
  "@commitlint/cli": "20.3.1",
53
53
  "@commitlint/config-conventional": "20.3.1",
54
54
  "@semantic-release/commit-analyzer": "13.0.1",
@@ -58,7 +58,7 @@
58
58
  "@semantic-release/release-notes-generator": "14.1.0",
59
59
  "eslint": "9.39.2",
60
60
  "husky": "9.1.7",
61
- "lint-staged": "16.1.0",
61
+ "lint-staged": "16.2.7",
62
62
  "semantic-release": "25.0.2"
63
63
  },
64
64
  "lint-staged": {
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
 
@@ -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
  }
@@ -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 {}
@@ -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';
19
15
  import componentTestTestingLibraryTemplate from '../templates/component/componentTestTestingLibraryTemplate.js';
16
+ import componentTestVitestTemplate from '../templates/component/componentTestVitestTemplate.js';
17
+ import componentTsLazyTemplate from '../templates/component/componentTsLazyTemplate.js';
18
+ import componentTsTemplate from '../templates/component/componentTsTemplate.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
 
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,16 +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
- );
45
-
46
- process.exit(1);
37
+ const availableTypes = Object.keys(cliConfigFile.component).join(', ');
38
+ exitWithError(`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
+ });
47
45
  }
48
46
 
49
47
  // Otherwise return it.
@@ -57,28 +55,25 @@ export function getComponentByType(args, cliConfigFile) {
57
55
  }
58
56
 
59
57
  export function getCorrespondingComponentFileTypes(component) {
60
- return Object.keys(component).filter((key) => key.split('with').length > 1);
58
+ return Object.keys(component).filter(key => key.split('with').length > 1);
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');
68
66
  const filename = path.basename(templatePath).replace(/template[_-]?name/i, componentName);
69
67
 
70
68
  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
- );
80
-
81
- return process.exit(1);
69
+ } catch {
70
+ exitWithError(`Custom template not found: "${templatePath}"`, {
71
+ suggestions: [
72
+ 'Verify the template path in your generate-react-cli.json config',
73
+ 'Check that the file exists and is readable',
74
+ 'Use an absolute path or a path relative to project root',
75
+ ],
76
+ });
82
77
  }
83
78
  }
84
79
 
@@ -93,20 +88,20 @@ function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, fi
93
88
  cliConfigFile.component.default.customDirectory,
94
89
  cliConfigFile.component[cmd.type].customDirectory,
95
90
  cmd.customDirectory,
96
- ].filter((e) => Boolean(e) && typeof e === 'string');
91
+ ].filter(e => Boolean(e) && typeof e === 'string');
97
92
 
98
93
  if (customDirectoryConfigs.length > 0) {
99
94
  const customDirectory = customDirectoryConfigs.slice(-1).toString();
100
95
 
101
- // Double check if the customDirectory is templatable
102
- 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
- );
108
-
109
- process.exit(-2);
96
+ // Check if the customDirectory contains a template placeholder
97
+ if (!TEMPLATE_NAME_REGEX.test(customDirectory)) {
98
+ exitWithError(`Invalid customDirectory: "${customDirectory}"`, {
99
+ details: 'customDirectory must contain a template placeholder',
100
+ suggestions: [
101
+ 'Use templatename, TemplateName, template-name, or template_name',
102
+ 'Example: "{{templatename}}" or "TemplateName"',
103
+ ],
104
+ });
110
105
  }
111
106
 
112
107
  for (const convertor in convertors) {
@@ -128,7 +123,7 @@ function componentDirectoryNameGenerator({ cmd, componentName, cliConfigFile, fi
128
123
 
129
124
  function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convertors }) {
130
125
  // @ts-ignore
131
- const { usesStyledComponents, cssPreprocessor, testLibrary, usesCssModule, usesTypeScript } = cliConfigFile;
126
+ const { cssPreprocessor, testLibrary, usesCssModule, usesTypeScript } = cliConfigFile;
132
127
  const { customTemplates } = cliConfigFile.component[cmd.type];
133
128
  let template = null;
134
129
  let filename = null;
@@ -136,35 +131,36 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
136
131
  // Check for a custom component template.
137
132
 
138
133
  if (customTemplates && customTemplates.component) {
139
- // --- Load and use the custom component template
134
+ // Load and use the custom component template
140
135
 
141
136
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
142
137
  componentName,
143
- customTemplates.component
138
+ customTemplates.component,
144
139
  );
145
140
 
146
141
  template = customTemplate;
147
142
  filename = customTemplateFilename;
148
143
  } else {
149
- // --- Else use GRC built-in component template
144
+ // Else use GRC built-in component template
150
145
 
151
146
  template = usesTypeScript ? componentTsTemplate : componentJsTemplate;
152
147
  filename = usesTypeScript ? `${componentName}.tsx` : `${componentName}.js`;
153
148
 
154
- // --- 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
155
150
 
156
- if (testLibrary !== 'Testing Library' || !cmd.withTest) {
151
+ const usesTestId = testLibrary === 'Testing Library' || testLibrary === 'Vitest';
152
+ if (!usesTestId || !cmd.withTest) {
157
153
  template = template.replace(` data-testid="templatename"`, '');
158
154
  }
159
155
 
160
- // --- If it has a corresponding stylesheet
156
+ // If it has a corresponding stylesheet
161
157
 
162
158
  if (cmd.withStyle) {
163
159
  if (cliConfigFile.usesStyledComponents) {
164
160
  const cssPath = `${componentName}.styled`;
165
161
  template = template.replace(
166
162
  `import styles from './templatename.module.css'`,
167
- `import { templatenameWrapper } from './${cssPath}'`
163
+ `import { templatenameWrapper } from './${cssPath}'`,
168
164
  );
169
165
  template = template.replace(` className={styles.templatename}`, '');
170
166
  template = template.replace(` <div`, '<templatenameWrapper');
@@ -173,7 +169,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
173
169
  const module = usesCssModule ? '.module' : '';
174
170
  const cssPath = `${componentName}${module}.${cssPreprocessor}`;
175
171
 
176
- // --- 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
177
173
 
178
174
  if (module.length) {
179
175
  template = template.replace(`'./templatename.module.css'`, `'./${cssPath}'`);
@@ -183,7 +179,7 @@ function componentTemplateGenerator({ cmd, componentName, cliConfigFile, convert
183
179
  }
184
180
  }
185
181
  } else {
186
- // --- If no stylesheet, remove className attribute and style import from jsTemplate
182
+ // If no stylesheet, remove className attribute and style import from jsTemplate
187
183
 
188
184
  template = template.replace(` className={styles.templatename}`, '');
189
185
  template = template.replace(`import styles from './templatename.module.css';`, '');
@@ -205,11 +201,11 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, co
205
201
  // Check for a custom style template.
206
202
 
207
203
  if (customTemplates && customTemplates.style) {
208
- // --- Load and use the custom style template
204
+ // Load and use the custom style template
209
205
 
210
206
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
211
207
  componentName,
212
- customTemplates.style
208
+ customTemplates.style,
213
209
  );
214
210
 
215
211
  template = customTemplate;
@@ -223,7 +219,7 @@ function componentStyleTemplateGenerator({ cliConfigFile, cmd, componentName, co
223
219
  const module = usesCssModule ? '.module' : '';
224
220
  const cssFilename = `${componentName}${module}.${cssPreprocessor}`;
225
221
 
226
- // --- Else use GRC built-in style template
222
+ // Else use GRC built-in style template
227
223
 
228
224
  template = componentCssTemplate;
229
225
  filename = cssFilename;
@@ -246,11 +242,11 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, con
246
242
  // Check for a custom test template.
247
243
 
248
244
  if (customTemplates && customTemplates.test) {
249
- // --- Load and use the custom test template
245
+ // Load and use the custom test template
250
246
 
251
247
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
252
248
  componentName,
253
- customTemplates.test
249
+ customTemplates.test,
254
250
  );
255
251
 
256
252
  template = customTemplate;
@@ -258,12 +254,10 @@ function componentTestTemplateGenerator({ cliConfigFile, cmd, componentName, con
258
254
  } else {
259
255
  filename = usesTypeScript ? `${componentName}.test.tsx` : `${componentName}.test.js`;
260
256
 
261
- if (testLibrary === 'Enzyme') {
262
- // --- Else use GRC built-in test template based on test library type
263
-
264
- template = componentTestEnzymeTemplate;
265
- } else if (testLibrary === 'Testing Library') {
257
+ if (testLibrary === 'Testing Library') {
266
258
  template = componentTestTestingLibraryTemplate;
259
+ } else if (testLibrary === 'Vitest') {
260
+ template = componentTestVitestTemplate;
267
261
  } else {
268
262
  template = componentTestDefaultTemplate;
269
263
  }
@@ -285,17 +279,17 @@ function componentStoryTemplateGenerator({ cliConfigFile, cmd, componentName, co
285
279
  // Check for a custom story template.
286
280
 
287
281
  if (customTemplates && customTemplates.story) {
288
- // --- Load and use the custom story template
282
+ // Load and use the custom story template
289
283
 
290
284
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
291
285
  componentName,
292
- customTemplates.story
286
+ customTemplates.story,
293
287
  );
294
288
 
295
289
  template = customTemplate;
296
290
  filename = customTemplateFilename;
297
291
  } else {
298
- // --- Else use GRC built-in story template
292
+ // Else use GRC built-in story template
299
293
 
300
294
  template = componentStoryTemplate;
301
295
  filename = usesTypeScript ? `${componentName}.stories.tsx` : `${componentName}.stories.js`;
@@ -317,17 +311,17 @@ function componentLazyTemplateGenerator({ cmd, componentName, cliConfigFile, con
317
311
  // Check for a custom lazy template.
318
312
 
319
313
  if (customTemplates && customTemplates.lazy) {
320
- // --- Load and use the custom lazy template
314
+ // Load and use the custom lazy template
321
315
 
322
316
  const { template: customTemplate, filename: customTemplateFilename } = getCustomTemplate(
323
317
  componentName,
324
- customTemplates.lazy
318
+ customTemplates.lazy,
325
319
  );
326
320
 
327
321
  template = customTemplate;
328
322
  filename = customTemplateFilename;
329
323
  } else {
330
- // --- Else use GRC built-in lazy template
324
+ // Else use GRC built-in lazy template
331
325
 
332
326
  template = usesTypeScript ? componentTsLazyTemplate : componentLazyTemplate;
333
327
  filename = usesTypeScript ? `${componentName}.lazy.tsx` : `${componentName}.lazy.js`;
@@ -349,23 +343,20 @@ function customFileTemplateGenerator({ componentName, cmd, cliConfigFile, compon
349
343
  // Check for a valid custom template for the corresponding custom component file.
350
344
 
351
345
  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
- );
360
-
361
- return process.exit(1);
346
+ exitWithError(`Missing custom template for "${fileType}"`, {
347
+ details: 'Custom component files require a matching custom template',
348
+ suggestions: [
349
+ `Add a "${fileType}" template path to customTemplates in your config`,
350
+ 'Check that the template file exists at the specified path',
351
+ ],
352
+ });
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,
368
- customTemplates[fileType]
359
+ customTemplates[fileType],
369
360
  );
370
361
 
371
362
  template = customTemplate;
@@ -378,7 +369,7 @@ Please make sure you're pointing to the right custom template path in your gener
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,
@@ -400,25 +391,27 @@ const componentTemplateGeneratorMap = {
400
391
 
401
392
  export function generateComponent(componentName, cmd, cliConfigFile) {
402
393
  const componentFileTypes = ['component', ...getCorrespondingComponentFileTypes(cmd)];
394
+ const generatedFiles = [];
395
+ let basePath = '';
403
396
 
404
397
  componentFileTypes.forEach((componentFileType) => {
405
- // --- 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,
406
399
  // or if the template type is "component"
407
400
 
408
401
  if (
409
- (cmd[componentFileType] && cmd[componentFileType].toString() === 'true') ||
410
- componentFileType === buildInComponentFileTypes.COMPONENT
402
+ (cmd[componentFileType] && cmd[componentFileType].toString() === 'true')
403
+ || componentFileType === buildInComponentFileTypes.COMPONENT
411
404
  ) {
412
405
  const generateTemplate = componentTemplateGeneratorMap[componentFileType] || customFileTemplateGenerator;
413
406
 
414
407
  const convertors = {
415
- templatename: componentName,
416
- TemplateName: startCase(camelCase(componentName)).replace(/ /g, ''),
417
- templateName: camelCase(componentName),
408
+ 'templatename': componentName,
409
+ 'TemplateName': startCase(camelCase(componentName)).replace(/ /g, ''),
410
+ 'templateName': camelCase(componentName),
418
411
  'template-name': kebabCase(componentName),
419
- template_name: snakeCase(componentName),
420
- TEMPLATE_NAME: snakeCase(componentName).toUpperCase(),
421
- TEMPLATENAME: componentName.toUpperCase(),
412
+ 'template_name': snakeCase(componentName),
413
+ 'TEMPLATE_NAME': snakeCase(componentName).toUpperCase(),
414
+ 'TEMPLATENAME': componentName.toUpperCase(),
422
415
  };
423
416
 
424
417
  const { componentPath, filename, template } = generateTemplate({
@@ -429,90 +422,41 @@ export function generateComponent(componentName, cmd, cliConfigFile) {
429
422
  convertors,
430
423
  });
431
424
 
432
- // --- Make sure the component does not already exist in the path directory.
425
+ // Extract base path for summary
426
+ if (!basePath) {
427
+ basePath = path.dirname(componentPath);
428
+ }
429
+
430
+ // Make sure the component does not already exist in the path directory.
433
431
 
434
432
  if (existsSync(componentPath)) {
435
- console.error(chalk.red(`${filename} already exists in this path "${componentPath}".`));
433
+ generatedFiles.push({ filename, status: 'skipped', path: componentPath });
436
434
  } else {
437
435
  try {
438
436
  if (!cmd.dryRun) {
439
437
  outputFileSync(componentPath, template);
440
438
 
441
- // Will replace the templatename in whichever format the user typed the component name in the command.
442
- replace({
443
- regex: 'templatename',
444
- replacement: convertors['templatename'],
445
- paths: [componentPath],
446
- recursive: false,
447
- silent: true,
448
- });
449
-
450
- // Will replace the TemplateName in PascalCase
451
- replace({
452
- regex: 'TemplateName',
453
- replacement: convertors['TemplateName'],
454
- paths: [componentPath],
455
- recursive: false,
456
- silent: true,
457
- });
458
-
459
- // Will replace the templateName in camelCase
460
- replace({
461
- regex: 'templateName',
462
- replacement: convertors['templateName'],
463
- paths: [componentPath],
464
- recursive: false,
465
- silent: true,
466
- });
467
-
468
- // Will replace the template-name in kebab-case
469
- replace({
470
- regex: 'template-name',
471
- replacement: convertors['template-name'],
472
- paths: [componentPath],
473
- recursive: false,
474
- silent: true,
475
- });
476
-
477
- // Will replace the template_name in snake_case
478
- replace({
479
- regex: 'template_name',
480
- replacement: convertors['template_name'],
481
- paths: [componentPath],
482
- recursive: false,
483
- silent: true,
484
- });
485
-
486
- // Will replace the TEMPLATE_NAME in uppercase SNAKE_CASE
487
- replace({
488
- regex: 'TEMPLATE_NAME',
489
- replacement: convertors['TEMPLATE_NAME'],
490
- paths: [componentPath],
491
- recursive: false,
492
- silent: true,
493
- });
494
-
495
- // Will replace the TEMPLATENAME in uppercase SNAKE_CASE
496
- replace({
497
- regex: 'TEMPLATENAME',
498
- replacement: convertors['TEMPLATENAME'],
499
- paths: [componentPath],
500
- recursive: false,
501
- 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 });
502
442
  });
503
443
  }
504
444
 
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);
445
+ generatedFiles.push({ filename, status: 'created', path: componentPath });
446
+ } catch (err) {
447
+ generatedFiles.push({ filename, status: 'failed', path: componentPath });
448
+ error(`Failed to create ${filename}`, {
449
+ details: err.message,
450
+ suggestions: [
451
+ 'Check that you have write permissions to the target directory',
452
+ 'Verify the path is valid',
453
+ ],
454
+ });
509
455
  }
510
456
  }
511
457
  }
512
458
  });
513
459
 
514
- if (cmd.dryRun) {
515
- console.log();
516
- console.log(chalk.yellow(`NOTE: The "dry-run" flag means no changes were made.`));
517
- }
460
+ // Show summary
461
+ fileSummary(generatedFiles, basePath, { dryRun: cmd.dryRun });
518
462
  }
@@ -1,9 +1,9 @@
1
- import chalk from 'chalk';
2
1
  import fsExtra from 'fs-extra';
3
2
  import inquirer from 'inquirer';
4
3
 
5
4
  import merge from 'lodash/merge.js';
6
5
  import deepKeys from './deepKeysUtils.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
 
@@ -89,85 +89,39 @@ const grcConfigQuestions = [
89
89
 
90
90
  async function createCLIConfigFile() {
91
91
  try {
92
- console.log();
93
- console.log(
94
- chalk.cyan(
95
- '--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------',
96
- ),
92
+ header(
93
+ 'Welcome to Generate React CLI!',
94
+ 'Answer a few questions to customize the CLI for your project.',
97
95
  );
98
- console.log(
99
- chalk.cyan(
100
- 'It looks like this is the first time that you\'re running generate-react-cli within this project.',
101
- ),
102
- );
103
- console.log();
104
- console.log(
105
- chalk.cyan(
106
- '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).',
107
- ),
108
- );
109
- console.log(
110
- chalk.cyan(
111
- '--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------',
112
- ),
113
- );
114
- console.log();
115
96
 
116
97
  const answers = await prompt(grcConfigQuestions);
117
98
 
118
99
  outputFileSync('generate-react-cli.json', JSON.stringify(answers, null, 2));
119
100
 
120
- console.log();
121
- console.log(
122
- chalk.cyan(
123
- 'The "generate-react-cli.json" config file has been successfully created on the root level of your project.',
124
- ),
125
- );
126
-
127
- console.log('');
128
- console.log(chalk.cyan('You can always go back and update it as needed.'));
129
- console.log('');
130
- console.log(chalk.cyan('Happy Hacking!'));
131
- console.log('');
132
- console.log('');
101
+ blank();
102
+ success('Created the generate-react-cli.json config file');
103
+ blank();
104
+ outro('You can always update the config file manually. Happy Hacking!');
133
105
 
134
106
  return answers;
135
- }
136
- catch (e) {
137
- console.error(
138
- chalk.red.bold(
139
- 'ERROR: Could not create a "generate-react-cli.json" config file.',
140
- ),
141
- );
107
+ } catch (e) {
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
+ });
142
115
  return e;
143
116
  }
144
117
  }
145
118
 
146
119
  async function updateCLIConfigFile(missingConfigQuestions, currentConfigFile) {
147
120
  try {
148
- console.log('');
149
- console.log(
150
- chalk.cyan(
151
- '------------------------------------------------------------------------------------------------------------------------------',
152
- ),
153
- );
154
- console.log(
155
- chalk.cyan(
156
- 'Generate React CLI has been updated and has a few new features from the last time you ran it within this project.',
157
- ),
158
- );
159
- console.log('');
160
- console.log(
161
- chalk.cyan(
162
- 'Please answer a few questions to update the "generate-react-cli.json" config file.',
163
- ),
121
+ header(
122
+ 'Generate React CLI has new features!',
123
+ 'Answer a few questions to update your config file.',
164
124
  );
165
- console.log(
166
- chalk.cyan(
167
- '------------------------------------------------------------------------------------------------------------------------------',
168
- ),
169
- );
170
- console.log('');
171
125
 
172
126
  const answers = await prompt(missingConfigQuestions);
173
127
  const updatedAnswers = merge({}, currentConfigFile, answers);
@@ -177,30 +131,20 @@ async function updateCLIConfigFile(missingConfigQuestions, currentConfigFile) {
177
131
  JSON.stringify(updatedAnswers, null, 2),
178
132
  );
179
133
 
180
- console.log();
181
- console.log(
182
- chalk.cyan(
183
- 'The ("generate-react-cli.json") has successfully updated for this project.',
184
- ),
185
- );
186
-
187
- console.log();
188
- console.log(
189
- chalk.cyan('You can always go back and manually update it as needed.'),
190
- );
191
- console.log();
192
- console.log(chalk.cyan('Happy Hacking!'));
193
- console.log();
194
- console.log();
134
+ blank();
135
+ success('Updated the generate-react-cli.json config file');
136
+ blank();
137
+ outro('You can always update the config file manually. Happy Hacking!');
195
138
 
196
139
  return updatedAnswers;
197
- }
198
- catch (e) {
199
- console.error(
200
- chalk.red.bold(
201
- 'ERROR: Could not update the "generate-react-cli.json" config file.',
202
- ),
203
- );
140
+ } catch (e) {
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
+ });
204
148
  return e;
205
149
  }
206
150
  }
@@ -238,17 +182,16 @@ export async function getCLIConfigFile() {
238
182
  }
239
183
 
240
184
  return currentConfigFile;
241
- }
242
- catch {
185
+ } catch {
243
186
  return await createCLIConfigFile();
244
187
  }
245
- }
246
- catch {
247
- console.error(
248
- chalk.red.bold(
249
- 'ERROR: Please make sure that you\'re running the generate-react-cli commands from the root level of your React project',
250
- ),
251
- );
252
- return process.exit(1);
188
+ } catch {
189
+ exitWithError('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
+ });
253
196
  }
254
197
  }
@@ -0,0 +1,148 @@
1
+ import chalk from 'chalk';
2
+
3
+ const DEFAULT_TERMINAL_WIDTH = 80;
4
+
5
+ // Symbols for consistent visual feedback
6
+ const symbols = {
7
+ success: chalk.green('✓'),
8
+ error: chalk.red('✖'),
9
+ warning: chalk.yellow('⚠'),
10
+ info: chalk.blue('ℹ'),
11
+ arrow: chalk.cyan('→'),
12
+ bullet: chalk.dim('•'),
13
+ };
14
+
15
+ // Create a responsive divider that adapts to terminal width
16
+ function divider(color = 'cyan') {
17
+ const width = Math.min(process.stdout.columns || DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_WIDTH);
18
+ return chalk[color]('─'.repeat(width));
19
+ }
20
+
21
+ function pluralize(count, word) {
22
+ return count === 1 ? word : `${word}s`;
23
+ }
24
+
25
+ // Success message with optional file path
26
+ export function success(message, filePath) {
27
+ if (filePath) {
28
+ console.log(` ${symbols.success} ${chalk.green(message)}`);
29
+ console.log(` ${symbols.arrow} ${chalk.dim(filePath)}`);
30
+ } else {
31
+ console.log(`${symbols.success} ${chalk.green(message)}`);
32
+ }
33
+ }
34
+
35
+ // Error message with optional details and suggestions
36
+ export function error(message, { details, suggestions } = {}) {
37
+ console.log();
38
+ console.log(`${symbols.error} ${chalk.red.bold('ERROR:')} ${chalk.red(message)}`);
39
+
40
+ if (details) {
41
+ console.log(` ${chalk.dim(details)}`);
42
+ }
43
+
44
+ if (suggestions && suggestions.length > 0) {
45
+ console.log();
46
+ console.log(chalk.dim(' Try one of:'));
47
+ suggestions.forEach((suggestion) => {
48
+ console.log(` ${symbols.bullet} ${chalk.dim(suggestion)}`);
49
+ });
50
+ }
51
+ console.log();
52
+ }
53
+
54
+ // Error message with exit
55
+ export function exitWithError(message, options = {}, exitCode = 1) {
56
+ error(message, options);
57
+ process.exit(exitCode);
58
+ }
59
+
60
+ // Warning message
61
+ export function warning(message) {
62
+ console.log(`${symbols.warning} ${chalk.yellow(message)}`);
63
+ }
64
+
65
+ // Info message
66
+ export function info(message) {
67
+ console.log(`${symbols.info} ${chalk.cyan(message)}`);
68
+ }
69
+
70
+ // Header for sections (like config setup intro)
71
+ export function header(title, subtitle) {
72
+ console.log();
73
+ console.log(divider());
74
+ console.log(chalk.cyan.bold(title));
75
+ if (subtitle) {
76
+ console.log(chalk.dim(subtitle));
77
+ }
78
+ console.log(divider());
79
+ console.log();
80
+ }
81
+
82
+ // Summary after file generation
83
+ export function fileSummary(files, basePath, { dryRun = false } = {}) {
84
+ console.log();
85
+
86
+ const createdFiles = files.filter(f => f.status === 'created');
87
+ const skippedFiles = files.filter(f => f.status === 'skipped');
88
+
89
+ if (dryRun) {
90
+ // Dry-run mode: show what would happen
91
+ console.log(`${symbols.info} ${chalk.cyan('Dry-run mode')} ${chalk.dim('- no files were created')}`);
92
+ console.log();
93
+
94
+ if (createdFiles.length > 0) {
95
+ console.log(chalk.dim(`Would create in ${basePath}:`));
96
+ createdFiles.forEach((file, index) => {
97
+ const isLast = index === createdFiles.length - 1 && skippedFiles.length === 0;
98
+ const prefix = isLast ? '└──' : '├──';
99
+ console.log(` ${chalk.dim(prefix)} ${file.filename}`);
100
+ });
101
+ }
102
+
103
+ if (skippedFiles.length > 0) {
104
+ if (createdFiles.length > 0) {
105
+ console.log();
106
+ }
107
+ console.log(chalk.dim('Already exist (would be skipped):'));
108
+ skippedFiles.forEach((file, index) => {
109
+ const isLast = index === skippedFiles.length - 1;
110
+ const prefix = isLast ? '└──' : '├──';
111
+ console.log(` ${chalk.dim(prefix)} ${symbols.warning} ${chalk.dim(file.filename)}`);
112
+ });
113
+ }
114
+ } else {
115
+ // Actual run: show what happened
116
+ if (createdFiles.length > 0) {
117
+ console.log(`${symbols.success} ${chalk.green(`Created ${createdFiles.length} ${pluralize(createdFiles.length, 'file')} in ${basePath}`)}`);
118
+ }
119
+ if (skippedFiles.length > 0) {
120
+ console.log(`${symbols.warning} ${chalk.yellow(`Skipped ${skippedFiles.length} ${pluralize(skippedFiles.length, 'file')} (already exist)`)}`);
121
+ }
122
+
123
+ // Show file tree with status icons
124
+ files.forEach((file, index) => {
125
+ const isLast = index === files.length - 1;
126
+ const prefix = isLast ? '└──' : '├──';
127
+ const statusIcon = file.status === 'created'
128
+ ? symbols.success
129
+ : file.status === 'skipped'
130
+ ? symbols.warning
131
+ : symbols.error;
132
+ console.log(` ${chalk.dim(prefix)} ${statusIcon} ${file.filename}`);
133
+ });
134
+ }
135
+
136
+ console.log();
137
+ }
138
+
139
+ // Closing message (Happy Hacking)
140
+ export function outro(message = 'Happy Hacking!') {
141
+ console.log(chalk.cyan(message));
142
+ console.log();
143
+ }
144
+
145
+ // Blank line helper
146
+ export function blank() {
147
+ console.log();
148
+ }
@@ -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
- `;