├── CHANGELOG.md
├── .editorconfig
├── .eslintrc.js
├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── copyDeclarations.js
├── defaultConfig.cjs
├── docs
├── arrival.png
├── createcomponentexample.png
├── kontur.png
├── reactcci.xml
└── webstormIntegration.md
├── index.ts
├── jest.config.js
├── package.json
├── readme-example.gif
├── rollup.config.mjs
├── src
├── buildComponent.ts
├── checkComponentExistence.ts
├── checkConfig.ts
├── constants.ts
├── generateFiles.ts
├── getFinalAgreement.ts
├── getModuleRootPath.ts
├── getProjectRootPath.ts
├── getQuestionsSettings.ts
├── getTemplate.ts
├── getTemplateFile.ts
├── getTemplateNamesToCreate.ts
├── getTemplateNamesToUpdate.ts
├── helpers.ts
├── initialize.ts
├── parseDestinationPath.ts
├── processAfterGeneration.ts
├── processCommandLineFlags.ts
├── setComponentNames.ts
├── setComponentTemplate.ts
├── setConfig.ts
├── setPath.ts
├── setProject.ts
└── types.ts
├── templates
├── class.tmp
├── fc.tmp
├── index.tmp
├── stories.tmp
└── test.tmp
├── tests
├── generateFiles.test.ts
├── getFinalAgreement.test.ts
├── getProjectRootPath.test.ts
├── getQuestionsSettings.test.ts
├── getTemplateFile.test.ts
├── getTemplateNamesToCreate.test.ts
├── helpers.test.ts
├── initialize.test.ts
├── setComponentNames.test.ts
├── setComponentTemplate.test.ts
├── setPath.test.ts
├── setProject.test.ts
└── testUtils.ts
├── tsconfig.json
└── yarn.lock
/ CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.11.1
2 | * add links to website
3 |
4 | ## 1.11.0
5 | * add checkExistenceOnCreate flag
6 |
7 | ## 1.10.0
8 | * updated default templates file extensions
9 |
10 | ## 1.9.0
11 | * refactoring to make project more stable and strict
12 |
13 | ## 1.8.4
14 | * update templates and config to support "processFileAndFolderName" param
15 |
16 | ## 1.8.3
17 | * add new stringToCase method to placeholder function
18 |
19 | ## 1.8.2
20 | * fix processing commander options
21 |
22 | ## 1.8.1
23 | * fix check files to update with different case
24 | * update dependencies
25 | * update imports in templates
26 |
27 | ## 1.8.0
28 | * fixed resolving placeholders. Avoid unnecessary executions
29 | * afterCreation filepath resolving fix
30 | * new placeholder variables (filePrefix, folderName)
31 | * fixed xml-settings for webstorm
32 | * fixed filename processing for multiple placeholder uses
33 | * fixed processing for snake_case and dash-case
34 | * new command-line flag for vs-rcci
35 |
36 | ## 1.7.6
37 | * New string types for processFileAndFolderName parameter
38 |
39 | ## 1.7.4
40 | * File type selection by command line flag
41 |
42 | ## 1.7.3
43 | * Fix root destination parsing issue
44 |
45 | ## 1.7.0
46 | * Added smart destination path parsing
47 | * Added `--skip-search`, `-s` flags to turn of interactive selection with `--dest` flag
48 | * Added `--files`, `-f` flags to set optional files to skip interactive selection step
49 | * Added `--sls` flag to skip last step like `skipFinalStep` config
50 |
51 | ## 1.6.0
52 | * Removed initialization flag with initialization on first run
53 |
54 | ## 1.5.3
55 | * Added files and folder name processing feature
56 |
57 | ## 1.5.2
58 | * Added template command line flag
59 |
60 | ## 1.5.0
61 | * Added update component feature
62 |
63 | ## 1.4.5
64 | * New improved path search
65 |
66 | ## 1.4.1
67 | * Added join function for placeholders
68 |
69 | ## 1.4.0
70 | * Added multi-component creation feature
71 |
72 | ## 1.3.1
73 | * Added rcci shortcut
74 |
75 | ## 1.3.0
76 | * Added extended template placeholders functionality
77 |
78 | ## 1.2.8
79 | * Fixed template folder name replacing
80 |
81 | ## 1.2.1
82 | * Fixed shorting POSIX paths
83 |
84 | ## 1.2.0
85 | * Added multi-template feature
86 |
87 | ## 1.1.5
88 | * Added template subfolder feature
89 |
90 | ## 1.1.4
91 | * Fixed POSIX path resolving in initialize mode
92 |
93 | ## 1.1.3
94 | * Fixed POSIX path resolving
95 |
96 | ## 1.1.2
97 | * Fixed folderPath processing
98 |
99 | ## 1.1.1
100 | * Fixed afterCreation script check
101 |
102 | ## 1.1.0
103 | * Added configuration mode
104 |
105 | ## 1.0.6
106 | * Fixed size of package
107 |
108 | ## 1.0.5
109 | * Fixed folder path cutter
110 |
111 | ## 1.0.4
112 | * Added folder search feature
113 | * `dist` flag renamed to `dest`
114 |
115 | ## 1.0.3
116 | * Added `name` and `project` command line flags
117 | * Replaced `chalk` to `kleur` because kleur is already used inside prompts
118 |
119 | ## 1.0.2
120 | * Added option to skip final step
121 |
122 | ## 1.0.0
123 | Features:
124 | * interactive creation
125 | * dynamic templates
126 | * afterCreation scripts
127 | * path flag to skip selecting step
128 | * multi-project mode
129 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true
5 | },
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:@typescript-eslint/eslint-recommended',
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:promise/recommended',
11 | 'plugin:import/errors',
12 | 'plugin:import/warnings',
13 | 'plugin:import/typescript',
14 | 'prettier',
15 | 'prettier/@typescript-eslint'
16 | ],
17 | parser: '@typescript-eslint/parser',
18 | parserOptions: {
19 | ecmaFeatures: {
20 | jsx: true
21 | },
22 | ecmaVersion: 2019,
23 | sourceType: 'module'
24 | },
25 | plugins: ['prettier', '@typescript-eslint', 'promise', 'import'],
26 | settings: {
27 | react: {
28 | version: 'detect'
29 | },
30 | 'import/resolver': {
31 | typescript: {}
32 | }
33 | },
34 | rules: {
35 | 'import/order': [
36 | 'warn',
37 | {
38 | groups: ['external', 'internal', 'unknown', 'builtin', 'parent', 'sibling', 'index'],
39 | 'newlines-between': 'always'
40 | }
41 | ],
42 | 'import/default': 'off',
43 | 'import/first': 'warn',
44 | 'import/no-named-as-default': 'off',
45 | 'import/no-named-as-default-member': 'off',
46 | 'import/named': 'off',
47 | 'import/namespace': 'off',
48 | 'prettier/prettier': 'warn',
49 | '@typescript-eslint/explicit-function-return-type': 'off',
50 | '@typescript-eslint/explicit-module-boundary-types': 'off',
51 | '@typescript-eslint/array-type': 'warn',
52 | '@typescript-eslint/explicit-member-accessibility': [
53 | 'warn',
54 | {
55 | overrides: {
56 | constructors: 'off'
57 | }
58 | }
59 | ],
60 | '@typescript-eslint/prefer-for-of': 'warn',
61 | '@typescript-eslint/prefer-function-type': 'warn',
62 | '@typescript-eslint/ban-types': 'off',
63 | '@typescript-eslint/ban-ts-comment': 'off',
64 | 'no-trailing-spaces': 'warn',
65 | 'prefer-const': 'warn',
66 | 'comma-dangle': ['warn', 'never'],
67 | curly: 'warn',
68 | 'dot-notation': 'warn',
69 | 'no-var': 'warn',
70 | 'prefer-object-spread': 'warn',
71 | 'prefer-template': 'warn',
72 | 'promise/catch-or-return': 'warn',
73 | 'promise/always-return': 'off',
74 | radix: 'warn',
75 | yoda: 'warn'
76 | },
77 | overrides: [
78 | {
79 | files: ['*.js'],
80 | rules: {
81 | 'no-undef': 'off'
82 | }
83 | }
84 | ]
85 | };
86 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: tests
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x, 20.5.1]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: yarn --frozen-lockfile
28 | - run: yarn typecheck
29 | - run: yarn test
30 | - run: yarn build
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | build
4 | coverage
5 | rcci.config.js
6 | rcci.config.cjs
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "singleQuote": true,
4 | "semi": true,
5 | "jsxBracketSameLine": false,
6 | "printWidth": 120,
7 | "trailingComma": "none",
8 | "overrides": [
9 | {
10 | "files": "*.scss",
11 | "options": {
12 | "singleQuote": false
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT LICENSE
2 |
3 | Copyright (c) 2017 Alibaba Group Holding Limited and other contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React create component interactive CLI
2 | CLI to create or edit **React** or **React Native** components with your own structure
3 |
4 | - **Plug and play**: Instant config for CRA with typescript
5 | - **Easy to config**: interactive configuration and simple templating
6 | - **Great documentation**: [More info on our website](https://kamenskih.gitbook.io/reactcci/)
7 | - **Works with anything**: Typescript, Storybook, Redux, Jest, ...
8 | - **Lightweight**: *< 40kb + 3 small libraries*
9 | - **Fast search**: very fast selecting place for new component by dynamic search
10 | - **A lot by one command**: ton of files by multi-creation feature
11 | - **Created for React**: better than **plop**, **hygen** or any scaffolding library, because it's for React
12 | - **Multiplatform**: Works on MacOS, Windows, and Linux.
13 | - ⭐ [**VSCode Extension**](https://marketplace.visualstudio.com/items?itemName=KamenskikhDmitriy.vs-rcci) ⭐
14 | - ⭐ [**Webstorm integration**](https://github.com/coolassassin/reactcci/blob/master/docs/webstormIntegration.md) ⭐
15 |
16 | 
17 |
18 | [](https://github.com/coolassassin/reactcci)
19 | [](https://www.npmjs.com/package/reactcci)
20 | [](https://www.npmjs.com/package/reactcci)
21 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fcoolassassin%2Freactcci?ref=badge_shield)
22 |
23 | ## Used by
24 | |[](https://arrival.com)|[](https://kontur.ru)|
25 | |---|---|
26 |
27 | ## Quick Overview
28 | Via yarn and interactive mode
29 | ```
30 | $ yarn add -D reactcci
31 | $ yarn rcci
32 | ```
33 | or via npm and flags
34 | ```
35 | $ npm i -D reactcci
36 | $ npx rcci --name Header Body Footer --dest src/App
37 | ```
38 |
39 | ## Installation
40 | To install via npm:
41 | ```npm install --save-dev reactcci```
42 |
43 | To install via yarn:
44 | ```yarn add --dev reactcci```
45 |
46 | ## Config
47 | On first run CLI will ask you about automatic configuration. Just run a command:
48 | `npx rcci`
49 | This command creates file `rcci.config.js` and template folder with basic template set.
50 |
51 | Config file contains next parameters:
52 | - `folderPath`
53 | Root directory to place you components, or array of paths
54 | Default: `src/`
55 | - `templatesFolder`
56 | Path to your own components templates
57 | Default: `templates`
58 | - `multiProject`
59 | Allows you to set up config for mono-repository with several projects
60 | Default: `false`
61 | - `skipFinalStep`
62 | Allows you to switch off last checking step
63 | Default: `false`
64 | - `checkExistenceOnCreate`
65 | Allows you to switch on check component existence on create to avoid replacing
66 | Default: `false`
67 | - `templates`
68 | Object with structure of your component
69 | - `processFileAndFolderName`
70 | String or function which allows you to remap your component name to folder-name and file-name prefix
71 | Default: `PascalCase`
72 | Types:
73 | - `camelCase`
74 | - `PascalCase`
75 | - `snake_case`
76 | - `dash-case`
77 | - `(name: string, parts: string[], isFolder: boolean) => string`
78 |
79 | Example:
80 | ```javascript
81 | {
82 | ...config,
83 | processFileAndFolderName: (name, parts, isFolder) =>
84 | isFolder ? name : parts.map((part) => part.toLowerCase()).join('-')
85 | }
86 | ```
87 | - `placeholders`
88 | List of placeholders which you can use to build your own component template
89 | Default:
90 | `#NAME#` for a component name
91 | `#STYLE#` for css-module import
92 | `#STORY_PATH#` for storybook title
93 | - `afterCreation`
94 | Object with scripts to process you file after creation
95 | Default: `undefined`
96 | Example:
97 | ```javascript
98 | {
99 | ...config,
100 | afterCreation: {
101 | prettier: {
102 | extensions: ['.ts', '.tsx'], // optional
103 | cmd: 'prettier --write [filepath]'
104 | }
105 | }
106 | }
107 | ```
108 |
109 | ## Placeholders
110 | Each placeholder is a function which get some data to build your own placeholder.
111 | During the multiple component creation all functions will be called for every single component.
112 | Example:
113 | ```javascript
114 | placeholders: {
115 | NAME: ({ componentName }) => componentName
116 | }
117 | ```
118 | After this set up you are able to use this placeholder by `#NAME#` inside your template files.
119 | Below, you can see the list of all available data and functions to create a new one.
120 |
121 | | Field | Description |
122 | |---|---|
123 | | `project` | Project name in multy-project mode |
124 | | `componentName`,
`objectName` | Name of the component or another object in multi-template mode |
125 | | `objectType` | Type of object which was selected by user. It is `component` by default. |
126 | | `pathToObject` | path to objects folder
Example: `src/components` |
127 | | `destinationFolder` | relative path to folder of object which is being created
Example: `App/Header/Logo` |
128 | | `objectFolder` | Absolute path to your object (component) folder |
129 | | `relativeObjectFolder` | Relative path to your object (component) folder |
130 | | `filePrefix` | processed object(component) name for filename |
131 | | `folderName` | processed object(component) name for folder |
132 | | `files` | Object of files which is being created |
133 | | `getRelativePath(to: string)` | Function to get relative path to any another path
Example: `../../src/helpers` |
134 | | `join(...parts: string)` | Function to get joined parts of path.
Example:
`join(project, destinationFolder, componentName)` => `Project/Footer/Email` |
135 | | `stringToCase(str: string, toCase: string)` | Function to map any string to case.
Example:
`stringToCase('dash-case-string', 'PascalCase')` => `DashCaseString` |
136 |
137 |
138 | ## Arguments
139 | - `--update`, `-u`
140 | To activate update mode to add new files to component or to replace existing
141 | - `--dest`, `-d`
142 | To set absolute or destination path to create or update component
143 | - `--skip-search`, `-s`
144 | To skip interactive folder selection to use `dest` argument as is
145 | - `--name`, `-n`
146 | To set component name or names divided by space to create several
147 | - `--files`, `-f`
148 | To set files to create or update, instead interactive selection
149 | Example: `--files style test component[1]`
150 | Will create optional files like `style` and `test` and will select second type of component by index
151 | - `--project`, `-p`
152 | To set project in multi-project mode
153 | - `--sls`
154 | To skip last agreement step like `skipFinalStep` in config file
155 |
156 | ## Multi-template
157 | If you need to generate something else, not components only, you are able to set up array of templates.
158 | Example bellow:
159 | ```javascript
160 | {
161 | ...config,
162 | templates: [
163 | {
164 | name: 'component', // will be added to default folderPath folder
165 | files: {
166 | index: {
167 | name: 'index.ts',
168 | file: 'component.ts'
169 | }
170 | }
171 | },
172 | {
173 | name: 'service',
174 | folderPath: 'services/', // will be added by the specific path
175 | files: {
176 | index: {
177 | name: 'index.ts',
178 | file: 'service.ts'
179 | }
180 | }
181 | }
182 | ]
183 | }
184 | ```
185 |
186 | ## License
187 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fcoolassassin%2Freactcci?ref=badge_large)
188 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | if (api) {
3 | api.cache(true);
4 | }
5 |
6 | return {
7 | presets: ['@babel/preset-typescript', '@babel/preset-env']
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/copyDeclarations.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | fs.mkdirSync(path.resolve(__dirname, 'build'), { recursive: true });
5 | fs.copyFile(path.resolve(__dirname, 'src/types.ts'), path.resolve(__dirname, 'build/types.d.ts'), (err) => {
6 | if (err) {
7 | console.error('Error, during copying the declarations file');
8 | }
9 | });
--------------------------------------------------------------------------------
/defaultConfig.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @type import("reactcci/build/types").Config
3 | */
4 | module.exports = {
5 | multiProject: false /* Enable searching projects with component folder path */,
6 | skipFinalStep: false /* Toggle final step agreement */,
7 | checkExistenceOnCreate: false /* Enable check folder for components which can be replaced */,
8 | folderPath: 'src/' /* Destination path or array of paths to create components */,
9 | templatesFolder: 'templates' /* Folder with templates */,
10 | templates: [
11 | {
12 | name: 'component',
13 | files: {
14 | /* Component folder structure declaration */
15 | index: {
16 | name: 'index.ts',
17 | file: 'index.tmp'
18 | },
19 | component: {
20 | name: '[name].tsx',
21 | file: [
22 | { name: 'fc.tmp', description: 'Functional component' },
23 | { name: 'class.tmp', description: 'Class component' }
24 | ]
25 | },
26 | style: {
27 | name: '[name].module.css',
28 | optional: true
29 | },
30 | stories: {
31 | name: '[name].stories.tsx',
32 | file: 'stories.tmp',
33 | optional: true,
34 | default: false
35 | },
36 | test: {
37 | name: '[name].test.tsx' /*'__tests__/[name].test.tsx' to put tests into subfolder*/,
38 | file: 'test.tmp',
39 | optional: true,
40 | default: false
41 | }
42 | }
43 | }
44 | ],
45 | placeholders: {
46 | /* Template placeholders */
47 | NAME: ({ componentName }) => componentName,
48 | COMPONENT_FILE_PREFIX: ({ filePrefix }) => filePrefix,
49 | STYLE: ({ files }) => (files.style ? `import styles from './${files.style.name}';\n` : ''),
50 | STORY_PATH: ({ join, project, destinationFolder, componentName }) =>
51 | join(project, destinationFolder, componentName)
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/docs/arrival.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coolassassin/reactcci/58a6b2d2e006f2efcef7534847441e0b2c0ee0ba/docs/arrival.png
--------------------------------------------------------------------------------
/docs/createcomponentexample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coolassassin/reactcci/58a6b2d2e006f2efcef7534847441e0b2c0ee0ba/docs/createcomponentexample.png
--------------------------------------------------------------------------------
/docs/kontur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coolassassin/reactcci/58a6b2d2e006f2efcef7534847441e0b2c0ee0ba/docs/kontur.png
--------------------------------------------------------------------------------
/docs/reactcci.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/webstormIntegration.md:
--------------------------------------------------------------------------------
1 | # Webstorm integration
2 | Using [reactcci](https://github.com/coolassassin/reactcci) with Webstorm or any another product by [JetBrains](https://www.jetbrains.com) you are able to create [External Tools](https://www.jetbrains.com/help/webstorm/configuring-third-party-tools.html#web-browsers) to make scaffolding with reactcci more easier.
3 | ## Quick setup
4 | You can set up configuration manually, but you can download settings file instead.
5 | Open folder according to your operating system:
6 | - **macOS**: *~/Library/Application Support/JetBrains/[product][version]/tools*
7 | - **Linux**: *~/.config/JetBrains/[product][version]/tools*
8 | - **Windows**: *%APPDATA%/JetBrains/[product][version]/tools*
9 |
10 | For more information read [the article](https://www.jetbrains.com/help/webstorm/configuring-project-and-ide-settings.html#restore-defaults).
11 |
12 | After that, download [**config file**](https://github.com/coolassassin/reactcci/raw/master/docs/reactcci.xml) and place in this folder like this:
13 | *.../JetBrains/Webstorm2021.3/tools/reactcci.xml*
14 |
15 | After that click on components' folder with right mouse button and in new "reactcci" menu choose what you need
16 |
17 | ## Manual setup
18 | Make few simple steps to set up a new tool:
19 | 1. Open Settings in menu `File -> Settings` (`Ctrl+Alt+S`, `⌘+Alt+S`)
20 | 2. Open External Tools menu `Tools -> External Tools`
21 | 3. Press the plus button ➕
22 | 4. Give the name for you new tool. For example "Create new component".
23 | 5. Fill the `Program` field with `node`
24 | 6. Fill the `Working directory` field with next command: `$ProjectFileDir$`
25 | 7. The last step is filling `Arguments` field. How to make necessary command will be described further.
26 |
27 | ## Arguments
28 | ### Create component
29 | Simple example for creating component looks like this:
30 | `./node_modules/reactcci/build/cli.js --dest "$FileDir$" --name $Prompt$ --files no -s --sls`
31 | Looks a bit complex but let's try to figure what is going on here.
32 | 1. First part is the path to main script of CLI:
33 | `./node_modules/reactcci/build/cli.js`
34 | 2. Next part is `--dest "$FileDir$"`
35 | `--dest` means destination, you can make it shorter via `-d`
36 | `$FileDir$` means current directory in project explorer, we need quotes here because path can contain spaces.
37 | 3. The third argument is name of you component or components `--name`
38 | `$Prompt$` opens window with text field to type there your component name or several divided by space
39 | 4. The fourth is `--files`. We place here `no`, it means that we don't need any optional files.
40 | 5. Next is `-s`. It is shortcut for `--skip-search`. It helps us to select exact folder in second argument without further selecting.
41 | 6. The last one is `--sls`. It means "skip last step". It is analogue for config flag `skipFinalStep`
42 |
43 | So the result will be look like this:
44 | 
45 |
46 | Now, you can click on any folder in your project by right button and in context menu will be able next command: `External Tools -> Create new component`.
47 | Click and enjoy the magic ✨
48 |
49 | ### Create component with optional files
50 | If you want to be able to create component with style file or with tests file you can modify the `--files` flag.
51 | You can type any files divided by space.
52 | For example: `--files style stories test`
53 |
54 | ### Update existing component
55 | Creating component is not the one feature for reactcci. You can update component by adding styles file for example.
56 | Copy you external tool and name it somehow like that: `Add styles`
57 | Then to set the update mode, we need add just on a new flag: `--update` or short variant `-u`.
58 | More than that, in this case we don't need `--name` flag because our folder is the name of our component.
59 | So, our arguments will be look like that:
60 | `./node_modules/reactcci/build/cli.js --dest "$FileDir$" --files styles --update --skip-search --sls`
61 | Easy way to try this is to find an existing component, right-click on it and `External Tools -> Add styles`.
62 | Few moments and we have new file ✨
63 |
64 | ## Bonus
65 | After all this you are able to add hot key for this. Open Settings again:
66 | `File -> Settings` (`Ctrl+Alt+S`, `⌘+Alt+S`)
67 | Then open `Keymap -> External tools -> External tools -> [your tool]`, right click and then add keyboard or mouse shortcut.
68 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import kleur from 'kleur';
4 |
5 | import { checkComponentExistence } from './src/checkComponentExistence';
6 | import { setPath } from './src/setPath';
7 | import { setConfig } from './src/setConfig';
8 | import { setProject } from './src/setProject';
9 | import { initialize } from './src/initialize';
10 | import { checkConfig } from './src/checkConfig';
11 | import { buildComponent } from './src/buildComponent';
12 | import { processCommandLineFlags } from './src/processCommandLineFlags';
13 | import { getModuleRootPath } from './src/getModuleRootPath';
14 | import { setComponentTemplate } from './src/setComponentTemplate';
15 | import { parseDestinationPath } from './src/parseDestinationPath';
16 |
17 | (async () => {
18 | try {
19 | const root = process.cwd();
20 | const moduleRoot = getModuleRootPath();
21 | const commandLineFlags = processCommandLineFlags();
22 |
23 | await initialize({ root, moduleRoot, commandLineFlags });
24 |
25 | let config = await setConfig({ root });
26 | await checkConfig({ config });
27 | const { config: newConfig, templateName } = await setComponentTemplate({ commandLineFlags, config });
28 | config = newConfig;
29 |
30 | const parsedPath = await parseDestinationPath({
31 | root,
32 | commandLineFlags,
33 | config
34 | });
35 | const project = await setProject({ project: parsedPath.project, root, commandLineFlags, config, templateName });
36 |
37 | const { componentNames, resultPath, projectRootPath } = await setPath({
38 | root,
39 | commandLineFlags,
40 | config,
41 | project,
42 | templateName,
43 | resultPathInput: parsedPath.resultPath,
44 | projectRootPathInput: parsedPath.projectRootPath
45 | });
46 |
47 | await checkComponentExistence({ componentNames, commandLineFlags, resultPath, projectRootPath, config });
48 |
49 | await buildComponent({
50 | root,
51 | moduleRoot,
52 | commandLineFlags,
53 | config,
54 | project,
55 | templateName,
56 | componentNames,
57 | resultPath,
58 | projectRootPath
59 | });
60 | } catch (e) {
61 | console.error(kleur.red('Unexpected error'), e);
62 | process.exit();
63 | }
64 | })();
65 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | testPathIgnorePatterns: ['/templates']
3 | };
4 |
5 | module.exports = config;
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactcci",
3 | "version": "1.14.0",
4 | "type": "commonjs",
5 | "description": "React create component CLI",
6 | "author": "Kamenskikh Dmitrii ",
7 | "license": "MIT",
8 | "main": "index.ts",
9 | "repository": "git@github.com:coolassassin/reactcci.git",
10 | "homepage": "https://kamenskih.gitbook.io/reactcci",
11 | "bugs": {
12 | "url": "https://github.com/coolassassin/reactcci/issues",
13 | "email": "coolstreetassassin@gmail.com"
14 | },
15 | "bin": {
16 | "reactcci": "./build/cli.js",
17 | "rcci": "./build/cli.js"
18 | },
19 | "scripts": {
20 | "start": "babel-node -x .ts index.ts",
21 | "build": "rollup -c && node copyDeclarations.js",
22 | "typecheck": "tsc --noEmit",
23 | "run-build": "node build/cli.js",
24 | "prettier": "prettier --write src/*.ts index.ts",
25 | "test": "jest",
26 | "coverage": "yarn test --coverage --collectCoverageFrom=./src/**"
27 | },
28 | "dependencies": {
29 | "commander": "^11.1.0",
30 | "kleur": "^4.1.5",
31 | "prompts": "^2.4.2"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.23.7",
35 | "@babel/node": "^7.22.19",
36 | "@babel/preset-env": "^7.23.7",
37 | "@babel/preset-typescript": "^7.23.3",
38 | "@rollup/plugin-babel": "^6.0.4",
39 | "@rollup/plugin-commonjs": "^25.0.7",
40 | "@rollup/plugin-node-resolve": "^15.2.3",
41 | "@types/jest": "^29.5.11",
42 | "@types/mock-fs": "^4.13.4",
43 | "@types/node": "^20.10.6",
44 | "@types/prompts": "^2.4.9",
45 | "@typescript-eslint/eslint-plugin": "^6.16.0",
46 | "@typescript-eslint/parser": "^6.16.0",
47 | "babel-jest": "^29.7.0",
48 | "babelrc-rollup": "^3.0.0",
49 | "eslint": "^8.56.0",
50 | "eslint-config-prettier": "^9.1.0",
51 | "eslint-import-resolver-typescript": "^3.6.1",
52 | "eslint-plugin-import": "^2.29.1",
53 | "eslint-plugin-prettier": "^5.1.2",
54 | "eslint-plugin-promise": "^6.1.1",
55 | "jest": "^29.7.0",
56 | "mock-fs": "^5.2.0",
57 | "prettier": "^3.1.1",
58 | "rollup": "^4.9.2",
59 | "rollup-plugin-hashbang": "^3.0.0",
60 | "rollup-plugin-terser": "^7.0.2",
61 | "typescript": "^5.3.3"
62 | },
63 | "files": [
64 | "defaultConfig.cjs",
65 | "package.json",
66 | "templates",
67 | "build",
68 | "README.md"
69 | ],
70 | "publishConfig": {
71 | "registry": "https://registry.npmjs.org/"
72 | },
73 | "browserslist": [
74 | "node 18"
75 | ],
76 | "keywords": [
77 | "react",
78 | "component",
79 | "create",
80 | "cli",
81 | "generate",
82 | "configurable",
83 | "gen",
84 | "scaffolding",
85 | "make",
86 | "tool",
87 | "dev",
88 | "build",
89 | "create react component",
90 | "rcci"
91 | ]
92 | }
93 |
--------------------------------------------------------------------------------
/readme-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coolassassin/reactcci/58a6b2d2e006f2efcef7534847441e0b2c0ee0ba/readme-example.gif
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import hashbang from 'rollup-plugin-hashbang';
4 | import { terser } from 'rollup-plugin-terser';
5 | import cjs from '@rollup/plugin-commonjs';
6 |
7 | const extensions = ['.ts', '.js'];
8 |
9 | export default {
10 | input: 'index.ts',
11 | output: {
12 | file: './build/cli.js',
13 | format: 'cjs',
14 | inlineDynamicImports: true
15 | },
16 | plugins: [
17 | resolve({ extensions }),
18 | babel({ extensions: ['.ts', '.js'], exclude: './node_modules/**' }),
19 | cjs(),
20 | // We call default, because hashbang plugin has broken export
21 | hashbang.default(),
22 | terser()
23 | ],
24 | external: ['path', 'fs', 'child_process', 'kleur', 'prompts', 'commander'],
25 | };
26 |
--------------------------------------------------------------------------------
/src/buildComponent.ts:
--------------------------------------------------------------------------------
1 | import kleur from 'kleur';
2 |
3 | import path from 'path';
4 |
5 | import { getTemplateNamesToCreate } from './getTemplateNamesToCreate';
6 | import { generateFiles } from './generateFiles';
7 | import { getTemplateFile } from './getTemplateFile';
8 | import { getFinalAgreement } from './getFinalAgreement';
9 | import { processAfterGeneration } from './processAfterGeneration';
10 | import { CommandLineFlags, ComponentFileList, Config, FilesList, Project, TemplateDescriptionObject } from './types';
11 | import { capitalizeName, generateFileName, getIsFileAlreadyExists, writeToConsole } from './helpers';
12 | import { getTemplateNamesToUpdate } from './getTemplateNamesToUpdate';
13 |
14 | type Properties = {
15 | root: string;
16 | moduleRoot: string;
17 | commandLineFlags: CommandLineFlags;
18 | config: Config;
19 | project: Project;
20 | templateName: string;
21 | componentNames: string[];
22 | projectRootPath: string;
23 | resultPath: string;
24 | };
25 |
26 | export const buildComponent = async (properties: Properties) => {
27 | const { commandLineFlags, config, project, templateName, componentNames, projectRootPath, resultPath } = properties;
28 | const { processFileAndFolderName } = config;
29 |
30 | const templateNames = commandLineFlags.update
31 | ? await getTemplateNamesToUpdate(properties)
32 | : await getTemplateNamesToCreate(properties);
33 |
34 | const fileList: FilesList = {};
35 |
36 | for (const [templateName, { name, file }] of Object.entries(config.templates as TemplateDescriptionObject)) {
37 | const isTemplateSelected = templateNames.includes(templateName);
38 | if (Array.isArray(file) && isTemplateSelected) {
39 | const selectedFile = await getTemplateFile({ commandLineFlags, name: templateName, files: file });
40 | fileList[templateName] = {
41 | name,
42 | file: selectedFile.name,
43 | type: selectedFile.description,
44 | selected: isTemplateSelected
45 | };
46 | } else {
47 | fileList[templateName] = { name, file: file as string, selected: isTemplateSelected };
48 | }
49 | }
50 |
51 | const componentFileList: ComponentFileList = {};
52 |
53 | for (const componentName of componentNames) {
54 | componentFileList[componentName] = Object.fromEntries(
55 | Object.entries(fileList)
56 | .filter(([, fileObject]) => {
57 | return (
58 | fileObject.selected ||
59 | getIsFileAlreadyExists({
60 | ...properties,
61 | fileNameTemplate: fileObject.name,
62 | objectName: componentName,
63 | processFileAndFolderName
64 | })
65 | );
66 | })
67 | .map(([tmpName, fileObject]) => [
68 | tmpName,
69 | {
70 | ...fileObject,
71 | name: generateFileName({
72 | fileNameTemplate: fileObject.name,
73 | objectName: componentName,
74 | processFileAndFolderName
75 | })
76 | }
77 | ])
78 | );
79 | }
80 |
81 | if (!config.skipFinalStep) {
82 | for (const componentName of componentNames) {
83 | if (commandLineFlags.update) {
84 | writeToConsole(`\nUpdating ${templateName} ${kleur.yellow(componentName)}`);
85 | } else {
86 | writeToConsole(`\nCreating ${templateName} ${kleur.yellow(componentName)}`);
87 | }
88 | writeToConsole(
89 | `Files:\n${Object.entries(componentFileList[componentName])
90 | .filter(([, options]) => options.selected)
91 | .map(
92 | ([tmp, options]) =>
93 | `- ${tmp}${options.type ? ` (${kleur.yellow(options.type)})` : ''}${kleur.gray(
94 | ` - ${options.name}`
95 | )}`
96 | )
97 | .join('\n')}`
98 | );
99 | }
100 | writeToConsole(`\nFolder: ${kleur.yellow(path.join(project, projectRootPath, resultPath))}`);
101 | }
102 |
103 | if (config.skipFinalStep || commandLineFlags.sls || (await getFinalAgreement())) {
104 | await generateFiles({
105 | ...properties,
106 | componentFileList
107 | });
108 | await processAfterGeneration({
109 | ...properties,
110 | componentFileList
111 | });
112 | const verb = componentNames.length > 1 ? 's are ' : ` is `;
113 | const action = commandLineFlags.update ? 'updated' : 'created';
114 | writeToConsole(kleur.green(`\n${capitalizeName(templateName)}${verb}${action}!!! \\(•◡ •)/ `));
115 | } else {
116 | writeToConsole("No? Let's build another one! (◉ ◡ ◉ )");
117 | }
118 | };
119 |
--------------------------------------------------------------------------------
/src/checkComponentExistence.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | import { getQuestionsSettings } from './getQuestionsSettings';
7 | import { CommandLineFlags, Config } from './types';
8 |
9 | type Properties = {
10 | componentNames: string[];
11 | projectRootPath: string;
12 | resultPath: string;
13 | commandLineFlags: CommandLineFlags;
14 | config: Config;
15 | };
16 |
17 | export const checkComponentExistence = async ({
18 | componentNames,
19 | projectRootPath,
20 | resultPath,
21 | commandLineFlags: { update },
22 | config: { checkExistenceOnCreate }
23 | }: Properties) => {
24 | if (!checkExistenceOnCreate || update) {
25 | return;
26 | }
27 | const components = componentNames.map((name) => ({
28 | name,
29 | exist: fs.existsSync(path.resolve(projectRootPath, resultPath, name))
30 | }));
31 | const existComponentCount = components.reduce((acc, { exist }) => acc + (exist ? 1 : 0), 0);
32 |
33 | if (existComponentCount <= 0) {
34 | return;
35 | }
36 |
37 | const { agree } = await Prompt(
38 | {
39 | type: 'toggle',
40 | name: 'agree',
41 | message: `Component${existComponentCount > 1 ? 's' : ''} ${components
42 | .filter((t) => t.exist)
43 | .map((t) => t.name)
44 | .join(', ')} ${existComponentCount > 1 ? 'are' : 'is'} already exist. Do you want to replace?`,
45 | initial: false,
46 | active: 'Yes',
47 | inactive: 'No'
48 | },
49 | getQuestionsSettings()
50 | );
51 |
52 | if (!agree) {
53 | process.exit();
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/src/checkConfig.ts:
--------------------------------------------------------------------------------
1 | import kleur from 'kleur';
2 |
3 | import { Config, TypingCases } from './types';
4 |
5 | type Properties = {
6 | config: Config;
7 | };
8 |
9 | export const checkConfig = async ({ config: { templates, afterCreation, processFileAndFolderName } }: Properties) => {
10 | const stopProgram = () => {
11 | process.exit(1);
12 | };
13 |
14 | if (Array.isArray(templates)) {
15 | if (templates.some((tmp) => !tmp.name)) {
16 | console.error(kleur.red(`Template name must be declared`));
17 | stopProgram();
18 | }
19 | if (templates.some((tmp) => templates.filter((t) => t.name === tmp.name).length > 1)) {
20 | console.error(kleur.red(`Template name must be unique, please revise config file`));
21 | stopProgram();
22 | }
23 | }
24 |
25 | if (processFileAndFolderName && typeof processFileAndFolderName !== 'function') {
26 | const cases: TypingCases[] = ['camelCase', 'PascalCase', 'snake_case', 'dash-case'];
27 |
28 | if (!cases.some((c) => c === processFileAndFolderName)) {
29 | console.error(
30 | kleur.red(
31 | `Unknown config type in "processFileAndFolderName" field: ${kleur.yellow(processFileAndFolderName)}`
32 | )
33 | );
34 | console.error(`Available cases:\n- ${cases.join('\n- ')}`);
35 | stopProgram();
36 | }
37 | }
38 |
39 | if (afterCreation) {
40 | for (const [type, command] of Object.entries(afterCreation)) {
41 | if (!command.cmd) {
42 | console.error(kleur.red(`Undeclared "cmd" option for afterCreation script ${kleur.yellow(type)}`));
43 | stopProgram();
44 | }
45 | if (command.extensions && !Array.isArray(command.extensions)) {
46 | console.error(
47 | kleur.red(`The option "extension" for afterCreation script ${kleur.yellow(type)} must be an array`)
48 | );
49 | stopProgram();
50 | }
51 | if (!command.cmd.includes('[filepath]')) {
52 | console.error(kleur.red(`Wrong "cmd" option for afterCreation script ${kleur.yellow(type)}`));
53 | stopProgram();
54 | }
55 | }
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const OLD_CONFIG_FILE_NAME = 'rcci.config.js';
2 | export const CONFIG_FILE_NAME = 'rcci.config.cjs';
3 | export const MODULE_NAME = 'reactcci';
4 |
5 | export const TEMPLATE_NAMES_SELECTING_INSTRUCTIONS =
6 | '\nOptions:\n' +
7 | ' ↑/↓: Select file\n' +
8 | ' ←/→/[Space]: Check/uncheck file\n' +
9 | ' a: Select all\n' +
10 | ' Enter/Return: End settings\n';
11 |
--------------------------------------------------------------------------------
/src/generateFiles.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | import { getTemplate } from './getTemplate';
5 | import { getRelativePath, processPath, processObjectName, mapNameToCase, getIsItemExists } from './helpers';
6 | import { ComponentFileList, Config, Project, templatePlaceholdersData, TypingCases } from './types';
7 |
8 | const isTypingCase = (value: string): value is TypingCases => {
9 | return (['camelCase', 'PascalCase', 'snake_case', 'dash-case'] satisfies TypingCases[]).includes(value as any);
10 | };
11 |
12 | type Properties = {
13 | root: string;
14 | moduleRoot: string;
15 | config: Config;
16 | project: Project;
17 | componentNames: string[];
18 | templateName: string;
19 | projectRootPath: string;
20 | resultPath: string;
21 | componentFileList: ComponentFileList;
22 | };
23 |
24 | export const generateFiles = async ({
25 | root,
26 | moduleRoot,
27 | config,
28 | project,
29 | templateName,
30 | componentNames,
31 | projectRootPath,
32 | resultPath,
33 | componentFileList
34 | }: Properties) => {
35 | const { processFileAndFolderName } = config;
36 |
37 | for (const componentName of componentNames) {
38 | const fileList = componentFileList[componentName];
39 | const folder = path.join(
40 | root,
41 | project,
42 | projectRootPath,
43 | resultPath,
44 | processObjectName({ name: componentName, isFolder: true, processFileAndFolderName })
45 | );
46 |
47 | if (!(await getIsItemExists(folder))) {
48 | await fs.promises.mkdir(folder);
49 | }
50 |
51 | const objectFolder = processPath(path.resolve(root, project, projectRootPath, resultPath, componentName));
52 |
53 | const dataForTemplate: templatePlaceholdersData = {
54 | project,
55 | componentName, // backward compatibility name
56 | objectName: componentName,
57 | objectType: templateName,
58 | pathToObject: processPath(path.join(project, projectRootPath)),
59 | destinationFolder: processPath(resultPath),
60 | objectFolder,
61 | relativeObjectFolder: processPath(path.join(project, projectRootPath, resultPath, componentName)),
62 | filePrefix: processObjectName({ name: componentName, isFolder: false, processFileAndFolderName }),
63 | folderName: processObjectName({ name: componentName, isFolder: true, processFileAndFolderName }),
64 | files: fileList,
65 | getRelativePath: (to: string) => getRelativePath({ root, from: objectFolder, to }),
66 | join: (...parts: string[]) => processPath(path.join(...parts)),
67 | stringToCase: (str: string, toCase: string) => {
68 | if (isTypingCase(toCase)) {
69 | return mapNameToCase(str, toCase);
70 | }
71 | throw new Error('Unknown case');
72 | }
73 | };
74 |
75 | for (const fileOptions of Object.values(fileList)) {
76 | if (!fileOptions.selected) {
77 | continue;
78 | }
79 | const pathParts = path
80 | .join(fileOptions.name)
81 | .split(path.sep)
82 | .filter((part) => part);
83 | const fileName = pathParts[pathParts.length - 1];
84 | const subFolders = pathParts.slice(0, pathParts.length - 1);
85 | if (subFolders.length > 0) {
86 | for (let index = 0; index < subFolders.length; index++) {
87 | const currentFolder = path.join(folder, ...subFolders.slice(0, index + 1));
88 | if (!fs.existsSync(currentFolder)) {
89 | await fs.promises.mkdir(currentFolder);
90 | }
91 | }
92 | dataForTemplate.getRelativePath = (to: string) =>
93 | getRelativePath({ root, from: path.resolve(objectFolder, subFolders.join('/')), to });
94 | }
95 | const template = fileOptions.file
96 | ? (await getTemplate({
97 | root,
98 | moduleRoot,
99 | fileName: fileOptions.file,
100 | insertionData: dataForTemplate,
101 | config
102 | })) ?? ''
103 | : '';
104 | await fs.promises.writeFile(path.join(folder, ...subFolders, fileName), template);
105 | }
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/src/getFinalAgreement.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 |
3 | import { getQuestionsSettings } from './getQuestionsSettings';
4 |
5 | export const getFinalAgreement = async () => {
6 | const { agree } = await Prompt(
7 | {
8 | type: 'toggle',
9 | name: 'agree',
10 | message: 'Is everything correct?',
11 | initial: true,
12 | active: 'Yes',
13 | inactive: 'No'
14 | },
15 | getQuestionsSettings()
16 | );
17 |
18 | return agree;
19 | };
20 |
--------------------------------------------------------------------------------
/src/getModuleRootPath.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import { MODULE_NAME } from './constants';
4 |
5 | export const getModuleRootPath = () => {
6 | const executableFilePathParts = process.argv[1].split(path.sep);
7 | const nodeModulesFolderIndex = executableFilePathParts.findIndex((part) => part === 'node_modules');
8 | if (nodeModulesFolderIndex === -1) {
9 | return executableFilePathParts.slice(0, executableFilePathParts.length - 2).join(path.sep);
10 | }
11 | return [...executableFilePathParts.slice(0, nodeModulesFolderIndex + 1), MODULE_NAME].join(path.sep);
12 | };
13 |
--------------------------------------------------------------------------------
/src/getProjectRootPath.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 |
3 | import { getQuestionsSettings } from './getQuestionsSettings';
4 |
5 | export const getProjectRootPath = async (paths: string[]): Promise => {
6 | const { path } = await Prompt(
7 | {
8 | type: 'select',
9 | name: 'path',
10 | message: `Select path to create component`,
11 | hint: 'Select using arrows and press Enter',
12 | choices: paths.map(
13 | (path): Prompt.Choice => ({
14 | title: path.split('/').reverse().find(Boolean) ?? '',
15 | value: path,
16 | description: path
17 | })
18 | ),
19 | initial: 0
20 | },
21 | getQuestionsSettings()
22 | );
23 |
24 | return path;
25 | };
26 |
--------------------------------------------------------------------------------
/src/getQuestionsSettings.ts:
--------------------------------------------------------------------------------
1 | export const getQuestionsSettings = () => {
2 | return {
3 | onCancel: () => {
4 | process.exit();
5 | }
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/getTemplate.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | import { Config, templatePlaceholdersData } from './types';
5 |
6 | type Properties = {
7 | root: string;
8 | moduleRoot: string;
9 | fileName: string;
10 | insertionData: templatePlaceholdersData;
11 | config: Config;
12 | };
13 |
14 | export const getTemplate = async ({
15 | root,
16 | moduleRoot,
17 | fileName,
18 | insertionData,
19 | config: { templatesFolder, placeholders }
20 | }: Properties) => {
21 | const defaultTemplatesFolder = path.resolve(moduleRoot, 'templates');
22 | const templatesPath = (await fs.existsSync(path.join(root, templatesFolder)))
23 | ? path.resolve(root, templatesFolder)
24 | : defaultTemplatesFolder;
25 |
26 | const templateFilePath = path.resolve(templatesPath, fileName);
27 |
28 | if (!fs.existsSync(templateFilePath)) {
29 | return '';
30 | }
31 |
32 | let templateData = (await fs.promises.readFile(templateFilePath)).toString();
33 | Object.entries(placeholders).forEach(([placeholder, replacer]) => {
34 | const placeholderRegular = new RegExp(`#${placeholder}#`, 'gim');
35 | if (placeholderRegular.test(templateData)) {
36 | templateData = templateData.replace(new RegExp(`#${placeholder}#`, 'gim'), replacer(insertionData));
37 | }
38 | });
39 |
40 | return templateData;
41 | };
42 |
--------------------------------------------------------------------------------
/src/getTemplateFile.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 | import kleur from 'kleur';
3 |
4 | import { getQuestionsSettings } from './getQuestionsSettings';
5 | import { CommandLineFlags, FileOption } from './types';
6 | import { getFileIndexForTemplate } from './helpers';
7 |
8 | type Properties = {
9 | name: string;
10 | files: FileOption[];
11 | commandLineFlags: CommandLineFlags;
12 | };
13 |
14 | export const getTemplateFile = async ({ name, files, commandLineFlags }: Properties) => {
15 | if (commandLineFlags.files) {
16 | const index = getFileIndexForTemplate(commandLineFlags.files, name);
17 |
18 | if (typeof index !== 'undefined') {
19 | if (!files[index]) {
20 | console.error(kleur.red(`Error: ${kleur.yellow(index)} is incorrect index for ${kleur.yellow(name)}`));
21 | console.error(`Max value is: ${kleur.yellow(files.length - 1)}`);
22 | process.exit();
23 | }
24 |
25 | return files[index];
26 | }
27 | }
28 |
29 | const { file } = await Prompt(
30 | {
31 | type: 'select',
32 | name: 'file',
33 | message: `Select type of ${kleur.reset().yellow(name)} file`,
34 | hint: 'Select using arrows and press Enter',
35 | choices: files.map((file) => ({ title: file.description, value: file })),
36 | initial: 0
37 | },
38 | getQuestionsSettings()
39 | );
40 |
41 | return file;
42 | };
43 |
--------------------------------------------------------------------------------
/src/getTemplateNamesToCreate.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 |
3 | import { getQuestionsSettings } from './getQuestionsSettings';
4 | import { TEMPLATE_NAMES_SELECTING_INSTRUCTIONS } from './constants';
5 | import { getFileTemplates } from './helpers';
6 | import { CommandLineFlags, Config } from './types';
7 |
8 | type Properties = {
9 | commandLineFlags: CommandLineFlags;
10 | config: Config;
11 | };
12 |
13 | export const getTemplateNamesToCreate = async ({ commandLineFlags, config: { templates } }: Properties) => {
14 | const { fileTemplates, undefinedFileTemplates, requiredTemplateNames } = getFileTemplates({
15 | commandLineFlags,
16 | templates
17 | });
18 |
19 | let selectedTemplateNames: string[] = [];
20 | if (commandLineFlags.files) {
21 | if (undefinedFileTemplates.length > 0) {
22 | console.error('Error: Undefined file templates:');
23 | console.error(undefinedFileTemplates.join('\n'));
24 | process.exit();
25 | return;
26 | }
27 | if (!fileTemplates.includes('no')) {
28 | selectedTemplateNames = fileTemplates;
29 | }
30 | } else {
31 | const templesToSelect = Object.entries(templates).filter(([, options]) => options.optional);
32 | if (templesToSelect.length) {
33 | selectedTemplateNames = (
34 | await Prompt(
35 | {
36 | type: 'multiselect',
37 | name: 'templateNames',
38 | message: 'Select files to generate',
39 | instructions: TEMPLATE_NAMES_SELECTING_INSTRUCTIONS,
40 | choices: templesToSelect.map(([name, options]) => {
41 | const { default: selected = true } = options;
42 | return { title: name, value: name, selected };
43 | })
44 | },
45 | getQuestionsSettings()
46 | )
47 | ).templateNames;
48 | }
49 | }
50 |
51 | return [...requiredTemplateNames, ...selectedTemplateNames];
52 | };
53 |
--------------------------------------------------------------------------------
/src/getTemplateNamesToUpdate.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 | import kleur from 'kleur';
3 |
4 | import { getQuestionsSettings } from './getQuestionsSettings';
5 | import { TEMPLATE_NAMES_SELECTING_INSTRUCTIONS } from './constants';
6 | import { CommandLineFlags, Config, Project, TemplateDescriptionObject } from './types';
7 | import { generateFileName, getFileTemplates, getIsFileAlreadyExists } from './helpers';
8 |
9 | type Properties = {
10 | root: string;
11 | commandLineFlags: CommandLineFlags;
12 | config: Config;
13 | project: Project;
14 | componentNames: string[];
15 | resultPath: string;
16 | projectRootPath: string;
17 | };
18 |
19 | export const getTemplateNamesToUpdate = async ({
20 | root,
21 | commandLineFlags,
22 | config: { templates, processFileAndFolderName },
23 | project,
24 | componentNames,
25 | resultPath,
26 | projectRootPath
27 | }: Properties) => {
28 | const componentName = componentNames[0];
29 | const { fileTemplates, undefinedFileTemplates } = getFileTemplates({ commandLineFlags, templates });
30 |
31 | if (commandLineFlags.files) {
32 | if (undefinedFileTemplates.length > 0) {
33 | console.error('Error: Undefined file templates:');
34 | console.error(undefinedFileTemplates.join('\n'));
35 | process.exit();
36 | return;
37 | }
38 |
39 | return fileTemplates;
40 | }
41 |
42 | const choices = Object.entries(templates as TemplateDescriptionObject).map(([tmpFileName, options]) => {
43 | const { default: isDefault = true, optional: isOptional = false, name } = options;
44 | const fileName = generateFileName({
45 | fileNameTemplate: name,
46 | objectName: componentName,
47 | processFileAndFolderName
48 | });
49 | const isAlreadyExists = getIsFileAlreadyExists({
50 | root,
51 | fileNameTemplate: name,
52 | objectName: componentName,
53 | processFileAndFolderName,
54 | project,
55 | resultPath,
56 | projectRootPath
57 | });
58 | return {
59 | title: `${tmpFileName}${kleur.reset(
60 | ` (${isAlreadyExists ? 'Replace' : 'Create'}: ${kleur.yellow(fileName)})`
61 | )}`,
62 | value: tmpFileName,
63 | selected: (isOptional && isDefault && !isAlreadyExists) || (!isOptional && !isAlreadyExists),
64 | exists: isAlreadyExists
65 | };
66 | });
67 |
68 | let initialized = false;
69 | return (
70 | await Prompt(
71 | {
72 | type: 'multiselect',
73 | name: 'templateNames',
74 | message: 'Select files to replace or create',
75 | instructions: TEMPLATE_NAMES_SELECTING_INSTRUCTIONS,
76 | choices,
77 | // @ts-ignore
78 | onRender() {
79 | if (!initialized) {
80 | const choiceToSelect = choices.findIndex((choice) => choice.selected);
81 | // @ts-ignore
82 | this.cursor = choiceToSelect !== -1 ? choiceToSelect : 0;
83 | initialized = true;
84 | }
85 | }
86 | },
87 | getQuestionsSettings()
88 | )
89 | ).templateNames;
90 | };
91 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | import { CommandLineFlags, Config, Project, TypingCases } from './types';
5 |
6 | export const isDirectory = (source: string) => fs.lstatSync(source).isDirectory();
7 |
8 | export const capitalizeName = (value: string) => value.replace(/^./g, value[0].toUpperCase());
9 |
10 | export const processPath = (path: string): string => {
11 | return path.replace(/(^[\\/]|[\\/]$)/g, '').replace(/\\/g, '/');
12 | };
13 |
14 | export const makePathShort = (path: string): string => {
15 | const sourcePath = processPath(path);
16 | const pathArray = sourcePath.split('/');
17 | if (pathArray.length <= 4) {
18 | return sourcePath;
19 | }
20 | return pathArray
21 | .reduce((acc: string[], value, index, arr) => {
22 | if (index < 1 || index > arr.length - 4) {
23 | if (index === arr.length - 3) {
24 | acc.push('...');
25 | }
26 | acc.push(value);
27 | }
28 | return acc;
29 | }, [])
30 | .join('/');
31 | };
32 |
33 | export const writeToConsole = (str: string) => {
34 | process.stdout.write(`${str}\n`);
35 | };
36 |
37 | type getRelativePathProperties = {
38 | root: string;
39 | from: string;
40 | to: string;
41 | };
42 |
43 | export const getRelativePath = ({ root, from, to }: getRelativePathProperties): string => {
44 | const destination = path.isAbsolute(to) ? processPath(to) : processPath(path.resolve(root, processPath(to)));
45 | return processPath(path.relative(from, destination));
46 | };
47 |
48 | export const processCommandLineArguments = (args: string[]): string[] => {
49 | let isCollecting = false;
50 | return args.reduce((acc: string[], value, index, arr) => {
51 | if (['-n', '--name', '-f', '--files'].includes(arr[index - 1])) {
52 | isCollecting = true;
53 | acc.push(value);
54 | return acc;
55 | } else if (value.startsWith('-')) {
56 | isCollecting = false;
57 | }
58 | if (isCollecting) {
59 | acc[acc.length - 1] += ` ${value}`;
60 | return acc;
61 | }
62 | acc.push(value);
63 | return acc;
64 | }, []);
65 | };
66 |
67 | export const processComponentNameString = (name: string | undefined): string[] | undefined => {
68 | if (typeof name === 'undefined') {
69 | return;
70 | }
71 |
72 | if (!name.includes(' ')) {
73 | return [name];
74 | }
75 |
76 | return name
77 | .trim()
78 | .replace(/\s{1,}/g, ' ')
79 | .split(' ')
80 | .reduce((acc: string[], value) => {
81 | if (!acc.some((v) => v === value)) {
82 | acc.push(value);
83 | }
84 | return acc;
85 | }, []);
86 | };
87 |
88 | export const splitStringByCapitalLetter = (value?: string): string[] | undefined => {
89 | if (!value || value.length === 0) {
90 | return undefined;
91 | }
92 | return value.split('').reduce((acc: string[], letter) => {
93 | if (acc.length === 0 || (letter === letter.toUpperCase() && /\w/.test(letter))) {
94 | acc.push(letter);
95 | return acc;
96 | }
97 | acc[acc.length - 1] += letter;
98 | return acc;
99 | }, []);
100 | };
101 |
102 | export const getObjectNameParts = (name: string): string[] => {
103 | return name
104 | .replace(/(\d\D|\D\d)/g, (str) => `${str[0]}-${str[1]}`)
105 | .replace(/([A-Z])/g, '-$1')
106 | .replace(/[^a-zA-Z0-9]/g, '-')
107 | .split('-')
108 | .filter((l) => l);
109 | };
110 |
111 | type processObjectNameProperties = {
112 | name: string;
113 | isFolder?: boolean;
114 | toComponent?: boolean;
115 | processFileAndFolderName: Config['processFileAndFolderName'];
116 | };
117 |
118 | export const processObjectName = ({
119 | name,
120 | isFolder = false,
121 | toComponent = false,
122 | processFileAndFolderName
123 | }: processObjectNameProperties): string => {
124 | if (processFileAndFolderName) {
125 | if (toComponent) {
126 | return mapNameToCase(name, 'PascalCase');
127 | }
128 |
129 | if (typeof processFileAndFolderName === 'function') {
130 | return processFileAndFolderName(name, getObjectNameParts(name), isFolder);
131 | } else {
132 | return mapNameToCase(name, processFileAndFolderName);
133 | }
134 | }
135 |
136 | return name;
137 | };
138 |
139 | export const mapNameToCase = (name: string, mapCase: TypingCases): string => {
140 | const lowerCaseParts = getObjectNameParts(name).map((part) => part.toLocaleLowerCase());
141 |
142 | switch (mapCase) {
143 | case 'camelCase':
144 | return lowerCaseParts.map((part, index) => (index === 0 ? part : capitalizeName(part))).join('');
145 | case 'PascalCase':
146 | return lowerCaseParts.map(capitalizeName).join('');
147 | case 'dash-case':
148 | return lowerCaseParts.join('-');
149 | case 'snake_case':
150 | return lowerCaseParts.join('_');
151 | }
152 | };
153 |
154 | type generateFileNameProperties = {
155 | fileNameTemplate: string;
156 | objectName: string;
157 | processFileAndFolderName: Config['processFileAndFolderName'];
158 | };
159 |
160 | export const generateFileName = ({
161 | fileNameTemplate,
162 | objectName,
163 | processFileAndFolderName
164 | }: generateFileNameProperties) => {
165 | return fileNameTemplate.replace(/\[name]/g, processObjectName({ name: objectName, processFileAndFolderName }));
166 | };
167 |
168 | type getIsFileAlreadyExistsProperties = {
169 | root: string;
170 | fileNameTemplate: string;
171 | objectName: string;
172 | project: Project;
173 | processFileAndFolderName: Config['processFileAndFolderName'];
174 | projectRootPath: string;
175 | resultPath: string;
176 | };
177 |
178 | export const getIsFileAlreadyExists = ({
179 | fileNameTemplate,
180 | objectName,
181 | root,
182 | project,
183 | processFileAndFolderName,
184 | resultPath,
185 | projectRootPath
186 | }: getIsFileAlreadyExistsProperties) => {
187 | const folder = path.join(
188 | root,
189 | project,
190 | projectRootPath,
191 | resultPath,
192 | processObjectName({ name: objectName, isFolder: true, processFileAndFolderName })
193 | );
194 | const fileName = generateFileName({
195 | fileNameTemplate,
196 | objectName,
197 | processFileAndFolderName
198 | });
199 | return fs.existsSync(path.resolve(folder, fileName));
200 | };
201 |
202 | type getFileTemplatesProperties = {
203 | withRequired?: boolean;
204 | commandLineFlags: CommandLineFlags;
205 | templates: Config['templates'];
206 | };
207 |
208 | export const getFileTemplates = ({
209 | withRequired = false,
210 | commandLineFlags: { files },
211 | templates
212 | }: getFileTemplatesProperties) => {
213 | const requiredTemplateNames = Object.entries(templates)
214 | .filter(([, options]) => !options.optional)
215 | .map(([name]) => name);
216 |
217 | let fileTemplates = files.replace(/\[\d*?]/g, '').split(' ');
218 |
219 | if (!withRequired) {
220 | fileTemplates = fileTemplates.filter((tmp) => !requiredTemplateNames.includes(tmp));
221 | }
222 |
223 | const undefinedFileTemplates = fileTemplates.filter(
224 | (tmp) => !Object.prototype.hasOwnProperty.call(templates, tmp) && tmp !== 'no'
225 | );
226 |
227 | return {
228 | fileTemplates,
229 | undefinedFileTemplates,
230 | requiredTemplateNames
231 | };
232 | };
233 |
234 | export const getFileIndexForTemplate = (files: string, template: string): number | undefined => {
235 | const names = files.split(' ');
236 | const elementIndex = names.findIndex(
237 | (name) => name.startsWith(`${template}[`) && /\[\d+?]/.test(name.replace(template, ''))
238 | );
239 | if (elementIndex === -1) {
240 | return;
241 | }
242 | return parseInt((/\d+/.exec(names[elementIndex]) ?? ['0'])[0], 10);
243 | };
244 |
245 | export const getIsItemExists = (item: string) =>
246 | fs.promises.access(item).then(
247 | () => true,
248 | () => false
249 | );
250 |
--------------------------------------------------------------------------------
/src/initialize.ts:
--------------------------------------------------------------------------------
1 | import Prompt from 'prompts';
2 | import kleur from 'kleur';
3 |
4 | import fs from 'fs';
5 | import path from 'path';
6 |
7 | import { CONFIG_FILE_NAME, OLD_CONFIG_FILE_NAME } from './constants';
8 | import { getQuestionsSettings } from './getQuestionsSettings';
9 | import { getIsItemExists, writeToConsole } from './helpers';
10 | import { CommandLineFlags } from './types';
11 |
12 | type Properties = {
13 | root: string;
14 | moduleRoot: string;
15 | commandLineFlags: CommandLineFlags;
16 | };
17 |
18 | export const initialize = async ({ root, moduleRoot, commandLineFlags }: Properties) => {
19 | const localConfigPath = path.resolve(root, CONFIG_FILE_NAME);
20 | if (await getIsItemExists(localConfigPath)) {
21 | return;
22 | }
23 |
24 | const oldLocalConfig = path.resolve(root, OLD_CONFIG_FILE_NAME);
25 | if (await getIsItemExists(oldLocalConfig)) {
26 | writeToConsole(`Please rename file ${kleur.yellow(OLD_CONFIG_FILE_NAME)} to ${kleur.yellow(CONFIG_FILE_NAME)}`);
27 | process.exit();
28 | return;
29 | }
30 |
31 | writeToConsole(`Hello!\nWe haven't find configuration file (${kleur.yellow(CONFIG_FILE_NAME)}).`);
32 | writeToConsole("It seems like a first run, doesn't it?");
33 |
34 | const { agree } = await Prompt(
35 | {
36 | type: 'toggle',
37 | name: 'agree',
38 | message: `Would you like to start configuration? It will be quick!`,
39 | initial: true,
40 | active: 'Yes',
41 | inactive: 'No'
42 | },
43 | getQuestionsSettings()
44 | );
45 |
46 | if (!agree) {
47 | writeToConsole('See you next time!');
48 | process.exit();
49 | return;
50 | }
51 |
52 | writeToConsole(`${kleur.gray('By default, CLI use basic templates to create a component.')}`);
53 | const { templatesAgreement } = await Prompt(
54 | {
55 | type: 'toggle',
56 | name: 'templatesAgreement',
57 | message: 'Would you like to create a template folder to set them up?',
58 | initial: true,
59 | active: 'Yes',
60 | inactive: 'No'
61 | },
62 | getQuestionsSettings()
63 | );
64 | let templateFolderName = 'templates';
65 |
66 | if (templatesAgreement) {
67 | templateFolderName = (
68 | await Prompt(
69 | {
70 | type: 'text',
71 | name: 'templateFolderName',
72 | message: 'What is a template folder name?',
73 | initial: templateFolderName
74 | },
75 | getQuestionsSettings()
76 | )
77 | ).templateFolderName;
78 | }
79 |
80 | const defaultConfigPath = path.resolve(moduleRoot, 'defaultConfig.cjs');
81 | const defaultConfig = (await fs.promises.readFile(defaultConfigPath)).toString();
82 | await fs.promises.writeFile(
83 | path.join(root, CONFIG_FILE_NAME),
84 | defaultConfig.replace(/(templatesFolder: ')(\w*?)(')/g, `$1${templateFolderName}$3`)
85 | );
86 | writeToConsole(`Config file ${kleur.yellow(CONFIG_FILE_NAME)} is created.`);
87 |
88 | if (templatesAgreement) {
89 | const templateFolderPath = path.resolve(root, templateFolderName);
90 | if (!(await getIsItemExists(templateFolderPath))) {
91 | await fs.promises.mkdir(templateFolderPath);
92 | }
93 | const defaultTempleFolder = path.resolve(moduleRoot, 'templates');
94 | const templateNames = await fs.promises.readdir(defaultTempleFolder);
95 | writeToConsole('Generated templates:');
96 | for (const templateName of templateNames) {
97 | const tmp = (await fs.promises.readFile(path.join(defaultTempleFolder, templateName))).toString();
98 | await fs.promises.writeFile(path.join(templateFolderPath, templateName), tmp);
99 | writeToConsole(` - ${templateFolderName}/${templateName}`);
100 | }
101 | }
102 |
103 | writeToConsole(kleur.green(`Well done! Configuration is finished!`));
104 |
105 | if (!commandLineFlags.nfc) {
106 | const { firstComponentAgreement } = await Prompt(
107 | {
108 | type: 'toggle',
109 | name: 'firstComponentAgreement',
110 | message: `Would you like to create your first component?`,
111 | initial: true,
112 | active: 'Yes',
113 | inactive: 'No'
114 | },
115 | getQuestionsSettings()
116 | );
117 |
118 | if (!firstComponentAgreement) {
119 | writeToConsole('Well, see you next time!');
120 | writeToConsole(`You can set up everything you need in the ${kleur.yellow(CONFIG_FILE_NAME)} file.`);
121 | writeToConsole('After configuration just run me again (◉ ◡ ◉ )');
122 | process.exit();
123 | return;
124 | }
125 | } else {
126 | process.exit();
127 | return;
128 | }
129 |
130 | return;
131 | };
132 |
--------------------------------------------------------------------------------
/src/parseDestinationPath.ts:
--------------------------------------------------------------------------------
1 | import kleur from 'kleur';
2 |
3 | import path from 'path';
4 | import fs from 'fs';
5 |
6 | import { processPath } from './helpers';
7 | import { CommandLineFlags, Config, Project } from './types';
8 |
9 | type Properties = {
10 | root: string;
11 | commandLineFlags: CommandLineFlags;
12 | config: Config;
13 | };
14 |
15 | type Output = {
16 | project: Project;
17 | projectRootPath?: string;
18 | resultPath?: string;
19 | };
20 |
21 | export const parseDestinationPath = async ({
22 | root,
23 | commandLineFlags: { dest },
24 | config: { folderPath, multiProject }
25 | }: Properties): Promise