├── .editorconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── README.md ├── assets ├── gen-react-code-config.png ├── gen-react-code-native.png ├── gen-react-code-redux-core.png ├── gen-react-code-redux.png └── gen-react-code.png ├── index.js ├── package-lock.json ├── package.json ├── src ├── apply-config-file.js ├── create-all-templates.js ├── generator-utilities.js └── test │ ├── apply-config-file.spec.js │ ├── create-all-templates.spec.js │ └── generator-utilities.spec.js ├── templates ├── native │ ├── react-redux │ │ ├── template.container.js │ │ ├── template.reducer.js │ │ ├── template.view.js │ │ └── test │ │ │ ├── template.container.spec.js │ │ │ ├── template.reducer.spec.js │ │ │ └── template.view.spec.js │ └── react │ │ ├── template.view.js │ │ └── test │ │ └── template.view.spec.js ├── redux-core │ ├── action-utilities │ │ ├── action-creator.js │ │ ├── action-type-creator.js │ │ └── test │ │ │ ├── action-creator.spec.js │ │ │ └── action-type-creator.spec.js │ ├── root-reducer.js │ └── store.js └── web │ ├── react-redux │ ├── _template.styles.scss │ ├── template.container.js │ ├── template.reducer.js │ ├── template.view.js │ └── test │ │ ├── template.container.spec.js │ │ ├── template.reducer.spec.js │ │ └── template.view.spec.js │ └── react │ ├── _template.styles.scss │ ├── template.view.js │ └── test │ └── template.view.spec.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = false 14 | 15 | [package.json] 16 | indent_style = space 17 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.idea 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # generated styles 14 | src/**/*.css 15 | 16 | # log files 17 | *.log 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run test 11 | 12 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Generate React Code 2 | =================== 3 | 4 | [![Build Status](https://travis-ci.org/JPStrydom/Generate-React-Code.svg?branch=master)](https://travis-ci.org/JPStrydom/Generate-React-Code) 5 | [![Coverage Status](https://coveralls.io/repos/github/JPStrydom/Generate-React-Code/badge.svg?branch=master)](https://coveralls.io/github/JPStrydom/Generate-React-Code?branch=master) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | 8 | [![js-standard-style](https://cdn.rawgit.com/standard/standard/master/badge.svg)](http://standardjs.com) 9 | 10 | This project utilises a scaffolding framework which generates React or React-Native code along with all the essential 11 | test code... Because who likes writing that themselves?! 12 | 13 | Additionally, it can be used to generate Redux code conforming to the [Redux ducks pattern](https://github.com/erikras/ducks-modular-redux) - and it can also be used to 14 | generate the Redux core files needed for React-Redux projects (`store`, `root-reducer`, and `action-utilities`). 15 | 16 | This generated code conforms to the [Air BnB style guide's](https://github.com/airbnb/javascript) naming and coding-style 17 | conventions, and it is thus highly recommended to make use of this tool when creating new React or React-Redux components. 18 | 19 | This package also allows users to add a configuration file containing default parameters. That way users would not have to specify these parameters every time they wish to generate code. 20 | 21 | _IMPORTANT NOTE:_ 22 | * _This package assumes the use of [__sass__](https://github.com/sass/sass) for React web projects._ 23 | * _This package assumes the use of [__Redux-Thunk__](https://github.com/gaearon/redux-thunk) as a Redux middleware._ 24 | * _This package assumes the use of [__enzyme__](https://github.com/airbnb/enzyme) and [__jest__](https://github.com/facebook/jest) for React testing._ 25 | 26 | ## Installation 27 | 28 | To install and save this npm package, navigate to your project's root directory in console and execute the following command: 29 | ```shell 30 | npm install generate-react-code --save-dev 31 | ``` 32 | 33 | Then add the following script to your `packages.json` file: 34 | 35 | ```json 36 | { 37 | "scripts": { 38 | "gen-react-code": "generate-react-code" 39 | } 40 | } 41 | ``` 42 | 43 | ## Generation Command 44 | 45 | The following command can be used to generate code: 46 | ```shell 47 | npm run gen-react-code -- -n example-component -d src/example/dir -r 48 | ``` 49 | 50 | ## Command Parameter Description: 51 | 52 | |Parameter|Description|Default Value| 53 | |---------|-----------|-------| 54 | | **`-n`** OR
**`--name`** | This is the lower kebab case name of the feature/component you would like to generate (e.g. `kebab-example-name`). | **`kebab-example-name`** | 55 | | **`-d`** OR
**`--directory`** | This is the relative directory where the generated component will be placed (e.g `src/components`). | **`src/components`** | 56 | | **`-N`** OR
**`--native`** | If you wish to generate code for React-Native, add this parameter - else React web code will be generated. | **`false`** | 57 | | **`-r`** OR
**`--redux`** | If you wish to generate Redux code in the duck pattern, add this parameter - else regular React code will be generated. | **`false`** | 58 | | **`-o`** OR
**`--omit-comments`** | If you wish to hide the comments within the generated files, add this parameter - else descriptive comments will be left in the generated code. | **`false`** | 59 | | **`-R`** OR
**`--redux-core`** | If you would like to generate the Redux core files (`store`, `root-reducer`, and `action-utilities`), add this parameter. These files are used to connect your application with Redux. | **`false`** | 60 | | **`-D`** OR
**`--redux-core-directory`** | This is the relative directory where the generated Redux core file will be placed (e.g `src/redux`). It is recommended to leave this as the default. | **`src/redux`** | 61 | | **`-h`** OR
**`--help`** | Output help usage information. | | 62 | 63 | ## Configuration File 64 | 65 | If you wish to store default parameters for your project, you'll have to add an optional `grcc.json` (_generate react code config_) file to your project's root directory. 66 | The file must have the following structure: 67 | ```json 68 | { 69 | "native": true, 70 | "redux": true, 71 | "omitComments": true 72 | } 73 | ``` 74 | 75 | _IMPORTANT NOTE:_ 76 | * _All these parameters are `false` by default. The parameters you wish to remain so, may be omitted from the file._ 77 | * _If you specify any of these parameters in your generation command, the generation command parameters will take priority over the ones in the `grcc.json` file._ 78 | * _The `grcc.json` file is completely optional and does not have to be added._ 79 | 80 | 81 | ## Generated Output Examples 82 | 83 | ### React Example 84 | 85 | Given the following example code generation command: 86 | ```shell 87 | npm run gen-react-code -- -n example-component -d src/components 88 | ``` 89 | The following file/folder structure will be generated (_take note that the `example-component` directory is generated without you having to specify it explicitly_): 90 | ``` 91 | project 92 | └───src 93 | └───components 94 | └───example-component 95 | │ example-component.view.js 96 | │ _example-component.styles.scss 97 | └───test 98 | │ example-component.view.spec.js 99 | ``` 100 | Within these files the majority of the React code will be completed for you - which contains detailed comments on how to add your 101 | functionality and general best practices. 102 | 103 | _IMPORTANT NOTE:_ 104 | * _Remember to add generated style sheets to the main style sheet, which is usually located in `src/index.scss`_ 105 | 106 | ![Demo](assets/gen-react-code.png) 107 | 108 | ### React Example With Config 109 | 110 | Given the following `grcc.json` config file: 111 | ```json 112 | { 113 | "native": true, 114 | "redux": true 115 | } 116 | ``` 117 | And given the following example code generation command: 118 | ```shell 119 | npm run gen-react-code -- -n example-component -d src/components 120 | ``` 121 | The following file/folder structure will be generated (_take note that the `example-component` directory is generated without you having to specify it explicitly_): 122 | ``` 123 | project 124 | └───src 125 | └───components 126 | └───example-component 127 | │ example-component.view.js 128 | │ _example-component.styles.scss 129 | └───test 130 | │ example-component.view.spec.js 131 | ``` 132 | Within these files the majority of the React code will be completed for you - which contains detailed comments on how to add your 133 | functionality and general best practices. 134 | 135 | _IMPORTANT NOTE:_ 136 | * _Remember to add generated style sheets to the main style sheet, which is usually located in `src/index.scss`_ 137 | 138 | ![Demo](assets/gen-react-code-config.png) 139 | 140 | ### React Native Example 141 | 142 | Given the following example code generation command: 143 | ```shell 144 | npm run gen-react-code -- -n example-component -d src/components -N 145 | ``` 146 | The following file/folder structure will be generated (_take note that the `example-component` directory is generated without you having to specify it explicitly_): 147 | ``` 148 | project 149 | └───src 150 | └───components 151 | └───example-component 152 | │ example-component.view.js 153 | └───test 154 | │ example-component.view.spec.js 155 | ``` 156 | Within these files the majority of the React-Native code will be completed for you - which contains detailed comments on how to add your 157 | functionality and general best practices. 158 | 159 | ![Demo](assets/gen-react-code-native.png) 160 | 161 | 162 | ### React With Redux Example 163 | 164 | Given the following example code generation command: 165 | ```shell 166 | npm run gen-react-code -- -n example-component -d src/components -r 167 | ``` 168 | The following file/folder structure will be generated (_take note that the `example-component` directory is generated without you having to specify it explicitly_): 169 | ``` 170 | project 171 | └───src 172 | └───components 173 | └───example-component 174 | │ example-component.container.js 175 | │ example-component.reducer.js 176 | │ example-component.view.js 177 | │ _example-component.styles.scss 178 | └───test 179 | │ example-component.container.spec.js 180 | │ example-component.reducer.spec.js 181 | │ example-component.view.spec.js 182 | ``` 183 | Within these files the majority of the React web code will be completed for you - which contains detailed comments on how to add your 184 | functionality and general best practices. 185 | 186 | _IMPORTANT NOTE:_ 187 | * _Remember to add generated reducers to the root reducer, which is usually located in `src/redux/root-reducer.js`_ 188 | * _Remember to add generated style sheets to the main style sheet, which is usually located in `src/index.scss`_ 189 | 190 | ![Demo](assets/gen-react-code-redux.png) 191 | 192 | ### Redux Core Files Example 193 | 194 | Given the following example code generation command: 195 | ```shell 196 | npm run gen-react-code -- -R 197 | ``` 198 | The following file/folder structure will be generated (_take note that adding the `-R` parameter will cause only the 199 | core files to be generated, irrespective of the other parameters_): 200 | ``` 201 | project 202 | └───src 203 | └───redux 204 | │ store.js 205 | │ root-reducer.js 206 | └───action-creator 207 | │ build-action-type.js 208 | │ create-action.js 209 | └───test 210 | │ build-action-type.spec.js 211 | │ create-action.spec.js 212 | ``` 213 | Within these files the majority of the React-Redux core code will be completed for you - which contains detailed comments on how to add your 214 | reducers and general best practices. 215 | 216 | _IMPORTANT NOTE:_ 217 | * _Remember to add your `store` to your Redux `Provider` where you're rendering your main app, which is usually located in `src/index.js`_ 218 | 219 | ![Demo](assets/gen-react-code-redux-core.png) 220 | 221 | 222 | ## Extra Material 223 | A great example on how to use `generate-react-code` can be found [here](https://github.com/zulucoda/generate-react-code-test) (courtesy of [ZuluCoda](https://github.com/zulucoda)). 224 | -------------------------------------------------------------------------------- /assets/gen-react-code-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPStrydom/Generate-React-Code/e276ba285d3d8bcbcd566e23ad981d9cc1bc7b1f/assets/gen-react-code-config.png -------------------------------------------------------------------------------- /assets/gen-react-code-native.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPStrydom/Generate-React-Code/e276ba285d3d8bcbcd566e23ad981d9cc1bc7b1f/assets/gen-react-code-native.png -------------------------------------------------------------------------------- /assets/gen-react-code-redux-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPStrydom/Generate-React-Code/e276ba285d3d8bcbcd566e23ad981d9cc1bc7b1f/assets/gen-react-code-redux-core.png -------------------------------------------------------------------------------- /assets/gen-react-code-redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPStrydom/Generate-React-Code/e276ba285d3d8bcbcd566e23ad981d9cc1bc7b1f/assets/gen-react-code-redux.png -------------------------------------------------------------------------------- /assets/gen-react-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPStrydom/Generate-React-Code/e276ba285d3d8bcbcd566e23ad981d9cc1bc7b1f/assets/gen-react-code.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const program = require('commander'); 6 | const chalk = require('chalk'); 7 | 8 | const createAllTemplates = require('./src/create-all-templates'); 9 | const applyConfig = require('./src/apply-config-file'); 10 | 11 | const DEFAULT_NAME = 'kebab-example-name'; 12 | const DEFAULT_COMPONENT_DIRECTORY = 'src/components'; 13 | const DEFAULT_REDUX_CORE_DIRECTORY = 'src/redux'; 14 | 15 | program 16 | .option( 17 | '-n, --name [name]', 18 | `This is the lower kebab case name of the feature/component you would like to generate (e.g. ${DEFAULT_NAME}).`, 19 | DEFAULT_NAME 20 | ) 21 | .option( 22 | '-d, --directory [directory]', 23 | `This is the relative directory where the generated component will be placed (e.g ${DEFAULT_COMPONENT_DIRECTORY}).`, 24 | DEFAULT_COMPONENT_DIRECTORY 25 | ) 26 | .option( 27 | '-N, --native [native]', 28 | 'If you wish to generate code for React-Native, add this parameter - else React web code will be generated.', 29 | false 30 | ) 31 | .option( 32 | '-r, --redux [redux]', 33 | 'If you wish to generate Redux code in the duck pattern, add this parameter - else regular React code will be generated.', 34 | false 35 | ) 36 | .option( 37 | '-o, --omit-comments [omitComments]', 38 | 'If you wish to hide the comments within the generated files, add this parameter - else descriptive comments will be left in the generated code.', 39 | false 40 | ) 41 | .option( 42 | '-R, --redux-core [reduxCore]', 43 | "If you would like to generate the Redux core files ('store', 'root-reducer', and 'action-utilities'), add this parameter. These files are used to connect your application with Redux.", 44 | false 45 | ) 46 | .option( 47 | '-D, --redux-core-directory [reduxCoreDirectory]', 48 | `This is the relative directory where the generated Redux core file will be placed (e.g ${DEFAULT_REDUX_CORE_DIRECTORY}). It is recommended to leave this as the default.`, 49 | DEFAULT_REDUX_CORE_DIRECTORY 50 | ) 51 | .parse(process.argv); 52 | 53 | applyConfig( 54 | program, 55 | ({ 56 | name, 57 | directory, 58 | native = false, 59 | redux = false, 60 | omitComments = false, 61 | reduxCore = false, 62 | reduxCoreDirectory 63 | }) => { 64 | console.log( 65 | chalk.bold.underline.cyan('Parameters:'), 66 | chalk.bold.magenta('\nname:\t\t\t'), 67 | chalk.yellow(name), 68 | chalk.bold.magenta('\ndirectory:\t\t'), 69 | chalk.yellow(directory), 70 | chalk.bold.magenta('\nnative:\t\t\t'), 71 | chalk.yellow(native), 72 | chalk.bold.magenta('\nredux:\t\t\t'), 73 | chalk.yellow(redux), 74 | chalk.bold.magenta('\nomitComments:\t\t'), 75 | chalk.yellow(omitComments), 76 | chalk.bold.magenta('\nreduxCore:\t\t'), 77 | chalk.yellow(reduxCore), 78 | chalk.bold.magenta('\nreduxCoreDirectory:\t'), 79 | chalk.yellow(reduxCoreDirectory), 80 | '\n' 81 | ); 82 | 83 | createAllTemplates( 84 | name, 85 | directory, 86 | native, 87 | redux, 88 | omitComments, 89 | reduxCore, 90 | reduxCoreDirectory 91 | ); 92 | } 93 | ); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate-react-code", 3 | "version": "2.0.0", 4 | "author": { 5 | "name": "JP Strydom", 6 | "email": "jpstrydom8@gmail.com" 7 | }, 8 | "contributors": [ 9 | { 10 | "name": "Muzikayise Flynn Buthelezi (zuluCoda)", 11 | "email": "muzi@mfbproject.co.za", 12 | "url": "https://mfbproject.co.za" 13 | } 14 | ], 15 | "description": 16 | "An automated React code generation tool for React web, React-Native, and/or Redux in the ducks modular pattern.", 17 | "license": "ISC", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/JPStrydom/Generate-React-Code.git" 21 | }, 22 | "bin": { 23 | "generate-react-code": "./index.js" 24 | }, 25 | "main": "index.js", 26 | "keywords": ["React", "React-Native", "Redux", "Ducks", "code generator"], 27 | "scripts": { 28 | "gen-react-code": "node index.js", 29 | "test": "jest --coverage", 30 | "test-watch": "jest --watch --verbose", 31 | "prettier": "prettier --write \"**/*.{js,scss}\"", 32 | "precommit": "lint-staged" 33 | }, 34 | "devDependencies": { 35 | "coveralls": "3.0.0", 36 | "husky": "0.14.3", 37 | "jest": "22.2.2", 38 | "lint-staged": "6.1.0" 39 | }, 40 | "dependencies": { 41 | "chalk": "2.3.1", 42 | "commander": "2.14.1", 43 | "lodash": "4.17.5", 44 | "shelljs": "0.8.1", 45 | "prettier": "1.10.2" 46 | }, 47 | "lint-staged": { 48 | "*.js": ["node_modules/.bin/prettier --write", "git add"] 49 | }, 50 | "jest": { 51 | "collectCoverageFrom": ["src/**/*.{js,jsx}", "!**/assets/**", "!**/templates/**"], 52 | "testMatch": ["/src/test/?(*.)(spec|test).js?(x)"], 53 | "testEnvironment": "node", 54 | "testURL": "http://localhost", 55 | "transformIgnorePatterns": ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/apply-config-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const chalk = require('chalk'); 6 | 7 | const { getRootPath } = require('./generator-utilities'); 8 | 9 | const GRCC = 'grcc.json'; 10 | 11 | function applyConfig(params, callback) { 12 | const configDirectory = path.join(getRootPath(), GRCC); 13 | 14 | if (fs.existsSync(configDirectory)) { 15 | fs.readFile(configDirectory, 'utf8', (err, data) => { 16 | if (err) throw err; 17 | 18 | let configData; 19 | try { 20 | configData = JSON.parse(data); 21 | } catch (error) { 22 | console.log( 23 | chalk.red('Error reading config file at'), 24 | chalk.gray(configDirectory), 25 | '\nMake sure your file has the correct format' 26 | ); 27 | return callback(params); 28 | } 29 | 30 | params.native = Boolean(params.native || configData.native); 31 | params.redux = Boolean(params.redux || configData.redux); 32 | params.omitComments = Boolean(params.omitComments || configData.omitComments); 33 | 34 | console.log( 35 | chalk.bold.underline.cyan('Config Loaded:'), 36 | chalk.bold.magenta('\nnative:\t\t'), 37 | chalk.yellow(params.native), 38 | chalk.bold.magenta('\nredux:\t\t'), 39 | chalk.yellow(params.redux), 40 | chalk.bold.magenta('\nomitComments:\t'), 41 | chalk.yellow(params.omitComments), 42 | '\n' 43 | ); 44 | 45 | return callback(params); 46 | }); 47 | } else { 48 | return callback(params); 49 | } 50 | } 51 | 52 | module.exports = applyConfig; 53 | -------------------------------------------------------------------------------- /src/create-all-templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const _ = require('lodash'); 5 | 6 | const { 7 | createTemplate, 8 | getAllDirectories, 9 | getAllPlaceholderNames 10 | } = require('./generator-utilities'); 11 | 12 | function createAllTemplates( 13 | name, 14 | directory, 15 | native, 16 | redux, 17 | omitComments, 18 | reduxCore, 19 | reduxCoreDirectory 20 | ) { 21 | const directories = getAllDirectories( 22 | name, 23 | directory, 24 | native, 25 | redux, 26 | reduxCore, 27 | reduxCoreDirectory 28 | ); 29 | const placeholderNames = getAllPlaceholderNames(name); 30 | 31 | _.forEach(directories, (directory, key) => { 32 | createTemplate(directory, placeholderNames, omitComments, () => { 33 | console.log( 34 | chalk.bold.blue(key), 35 | chalk.bold.green('file successfully created in'), 36 | chalk.bold.gray(directory.generated) 37 | ); 38 | }); 39 | }); 40 | } 41 | 42 | module.exports = createAllTemplates; 43 | -------------------------------------------------------------------------------- /src/generator-utilities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const shell = require('shelljs'); 5 | const path = require('path'); 6 | const prettier = require('prettier'); 7 | const commander = require.resolve('commander'); 8 | const _ = require('lodash'); 9 | 10 | const NODE_MODULES_PATH = 'node_modules'; 11 | const ROOT_PATH = getRootPath(); 12 | const PRETTIER_CONFIG = { 13 | tabWidth: 4, 14 | singleQuote: true, 15 | printWidth: 100 16 | }; 17 | 18 | function getAllDirectories(name, directory, native, redux, reduxCore, reduxCoreDirectory) { 19 | if (reduxCore) { 20 | return getReduxCoreDirs(reduxCoreDirectory); 21 | } 22 | return getReactComponentDirs(name, directory, native, redux); 23 | } 24 | 25 | function getReduxCoreDirs(reduxCoreDirectory) { 26 | const templateDirectory = path.join(__dirname, '..', 'templates', 'redux-core'); 27 | const generatedDirectory = path.join(ROOT_PATH, reduxCoreDirectory); 28 | 29 | if (!fs.existsSync(generatedDirectory)) { 30 | shell.mkdir('-p', path.join(generatedDirectory, 'action-utilities', 'test')); 31 | } 32 | 33 | return { 34 | store: { 35 | template: path.join(templateDirectory, 'store.js'), 36 | generated: path.join(generatedDirectory, 'store.js') 37 | }, 38 | rootReducer: { 39 | template: path.join(templateDirectory, 'root-reducer.js'), 40 | generated: path.join(generatedDirectory, 'root-reducer.js') 41 | }, 42 | createAction: { 43 | template: path.join(templateDirectory, 'action-utilities', 'action-creator.js'), 44 | generated: path.join(generatedDirectory, 'action-utilities', 'action-creator.js') 45 | }, 46 | createActionType: { 47 | template: path.join(templateDirectory, 'action-utilities', 'action-type-creator.js'), 48 | generated: path.join(generatedDirectory, 'action-utilities', 'action-type-creator.js') 49 | }, 50 | createActionTest: { 51 | template: path.join( 52 | templateDirectory, 53 | 'action-utilities', 54 | 'test', 55 | 'action-creator.spec.js' 56 | ), 57 | generated: path.join( 58 | generatedDirectory, 59 | 'action-utilities', 60 | 'test', 61 | 'action-creator.spec.js' 62 | ) 63 | }, 64 | createActionTypeTest: { 65 | template: path.join( 66 | templateDirectory, 67 | 'action-utilities', 68 | 'test', 69 | 'action-type-creator.spec.js' 70 | ), 71 | generated: path.join( 72 | generatedDirectory, 73 | 'action-utilities', 74 | 'test', 75 | 'action-type-creator.spec.js' 76 | ) 77 | } 78 | }; 79 | } 80 | 81 | function getReactComponentDirs(name, directory, native, redux) { 82 | const subDir = native ? 'native' : 'web'; 83 | const subSubDir = redux ? 'react-redux' : 'react'; 84 | 85 | const templateDirectory = path.join(__dirname, '..', 'templates', subDir, subSubDir); 86 | const generatedDirectory = path.join(ROOT_PATH, directory, name); 87 | 88 | if (!fs.existsSync(generatedDirectory)) { 89 | shell.mkdir('-p', path.join(generatedDirectory, 'test')); 90 | } 91 | 92 | const reactDirs = { 93 | view: { 94 | template: path.join(templateDirectory, 'template.view.js'), 95 | generated: path.join(generatedDirectory, `${name}.view.js`) 96 | }, 97 | viewTest: { 98 | template: path.join(templateDirectory, 'test', 'template.view.spec.js'), 99 | generated: path.join(generatedDirectory, 'test', `${name}.view.spec.js`) 100 | } 101 | }; 102 | 103 | if (!native) { 104 | reactDirs.stylesheet = { 105 | template: path.join(templateDirectory, '_template.styles.scss'), 106 | generated: path.join(generatedDirectory, `_${name}.styles.scss`) 107 | }; 108 | } 109 | 110 | if (!redux) { 111 | return reactDirs; 112 | } 113 | 114 | const reduxDirs = { 115 | container: { 116 | template: path.join(templateDirectory, 'template.container.js'), 117 | generated: path.join(generatedDirectory, `${name}.container.js`) 118 | }, 119 | containerTest: { 120 | template: path.join(templateDirectory, 'test', 'template.container.spec.js'), 121 | generated: path.join(generatedDirectory, 'test', `${name}.container.spec.js`) 122 | }, 123 | reducer: { 124 | template: path.join(templateDirectory, 'template.reducer.js'), 125 | generated: path.join(generatedDirectory, `${name}.reducer.js`) 126 | }, 127 | reducerTest: { 128 | template: path.join(templateDirectory, 'test', 'template.reducer.spec.js'), 129 | generated: path.join(generatedDirectory, 'test', `${name}.reducer.spec.js`) 130 | } 131 | }; 132 | 133 | return _.assign(reactDirs, reduxDirs); 134 | } 135 | 136 | function getAllPlaceholderNames(kebab) { 137 | const lowerCamel = _.camelCase(kebab); 138 | const upperCamel = _.upperFirst(lowerCamel); 139 | return { kebab, lowerCamel, upperCamel }; 140 | } 141 | 142 | function removeComments(s) { 143 | return s.replace(/([\s\S]*?)\/\*[\s\S]*?\*\//g, '$1'); 144 | } 145 | 146 | function createTemplate(directory, placeholderNames, omitComments, callback) { 147 | fs.readFile(directory.template, 'utf8', (err, data) => { 148 | if (err) throw err; 149 | 150 | data = _.replace(data, /TEMPLATE_KEBAB_CASE_NAME/g, placeholderNames.kebab); 151 | data = _.replace(data, /TEMPLATE_LOWER_CAMEL_CASE_NAME/g, placeholderNames.lowerCamel); 152 | data = _.replace(data, /TEMPLATE_UPPER_CAMEL_CASE_NAME/g, placeholderNames.upperCamel); 153 | 154 | if (omitComments) { 155 | data = removeComments(data); 156 | } 157 | 158 | const formattedCode = formatCodeWithPrettier(data, directory); 159 | 160 | fs.writeFile(directory.generated, formattedCode, err => { 161 | if (err) throw err; 162 | return callback(); 163 | }); 164 | }); 165 | } 166 | 167 | function formatCodeWithPrettier(data, directory) { 168 | const parser = _.endsWith(directory.generated, '.scss') ? 'scss' : 'babylon'; 169 | const config = { ...PRETTIER_CONFIG, parser }; 170 | 171 | return prettier.format(data, config); 172 | } 173 | 174 | function getRootPath() { 175 | return commander.slice(0, commander.indexOf(NODE_MODULES_PATH.toLowerCase())); 176 | } 177 | 178 | module.exports = { 179 | createTemplate, 180 | getAllDirectories, 181 | getAllPlaceholderNames, 182 | getRootPath 183 | }; 184 | -------------------------------------------------------------------------------- /src/test/apply-config-file.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const shell = require('shelljs'); 6 | 7 | const { getRootPath } = require('../generator-utilities'); 8 | 9 | const applyConfig = require('../apply-config-file'); 10 | 11 | const ROOT_PATH = getRootPath(); 12 | 13 | describe('Apply config file - Unit Test', () => { 14 | describe('applyConfig', () => { 15 | it('should apply config file parameters to passed in parameters', done => { 16 | const passedIn = { 17 | name: 'some-name' 18 | }; 19 | const expected = { 20 | name: 'some-name', 21 | native: true, 22 | redux: true, 23 | omitComments: true 24 | }; 25 | fs.writeFile( 26 | path.join(ROOT_PATH, 'grcc.json'), 27 | '{ "native": true, "redux": true, "omitComments": true }', 28 | () => 29 | applyConfig(passedIn, params => { 30 | expect(params).toEqual(expected); 31 | shell.rm('-rf', path.join(ROOT_PATH, 'grcc.json')); 32 | done(); 33 | }) 34 | ); 35 | }); 36 | 37 | it('should not overwrite passed in parameters when a user specifies them', done => { 38 | const passedIn = { 39 | name: 'some-name', 40 | native: true, 41 | redux: true, 42 | omitComments: true 43 | }; 44 | fs.writeFile( 45 | path.join(ROOT_PATH, 'grcc.json'), 46 | '{ "native": false, "redux": false, "omitComments": false }', 47 | () => 48 | applyConfig(passedIn, params => { 49 | expect(params).toEqual(passedIn); 50 | shell.rm('-rf', path.join(ROOT_PATH, 'grcc.json')); 51 | done(); 52 | }) 53 | ); 54 | }); 55 | 56 | it('should return unaltered parameters when the config file does not exist', done => { 57 | const passedIn = { 58 | name: 'some-name', 59 | native: false, 60 | redux: false 61 | }; 62 | 63 | applyConfig(passedIn, params => { 64 | expect(params).toEqual(passedIn); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should return unaltered parameters when the config file exists but is not in the correct format', done => { 70 | const passedIn = { 71 | name: 'some-name', 72 | native: false 73 | }; 74 | fs.writeFile(path.join(ROOT_PATH, 'grcc.json'), '{ oops it aint json }', () => 75 | applyConfig(passedIn, params => { 76 | expect(params).toEqual(passedIn); 77 | shell.rm('-rf', path.join(ROOT_PATH, 'grcc.json')); 78 | done(); 79 | }) 80 | ); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/test/create-all-templates.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const shell = require('shelljs'); 6 | 7 | const { getRootPath } = require('../generator-utilities'); 8 | const createAllTemplates = require('../create-all-templates'); 9 | 10 | const ROOT_PATH = getRootPath(); 11 | 12 | describe('Create All Templates - Unit Test', () => { 13 | describe('createAllTemplates', () => { 14 | it('should create all template', done => { 15 | const name = 'some-complete-template'; 16 | const directory = 'some-complete-template'; 17 | 18 | createAllTemplates(name, directory); 19 | 20 | setTimeout(() => { 21 | expect(fs.existsSync(path.join(ROOT_PATH, directory))).toBeTruthy(); 22 | expect( 23 | fs.existsSync(path.join(ROOT_PATH, directory, name, `${name}.view.js`)) 24 | ).toBeTruthy(); 25 | done(); 26 | }, 500); 27 | }); 28 | }); 29 | 30 | afterAll(() => { 31 | shell.rm('-rf', path.join(ROOT_PATH, 'some-complete-template')); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/generator-utilities.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const shell = require('shelljs'); 6 | const prettier = require('prettier'); 7 | 8 | const resolvePath = require.resolve('commander'); 9 | 10 | const { 11 | getRootPath, 12 | getAllDirectories, 13 | getAllPlaceholderNames, 14 | createTemplate 15 | } = require('../generator-utilities'); 16 | 17 | const ROOT_PATH = getRootPath(); 18 | 19 | describe('Generator Utilities - Unit Test', () => { 20 | describe('createTemplate', () => { 21 | it('should create template with comments', done => { 22 | const name = 'some-directory-create-template'; 23 | const templatePath = path.join(ROOT_PATH, 'templates', 'web', 'react'); 24 | const directory = path.join(ROOT_PATH, name); 25 | 26 | if (!fs.existsSync(directory)) { 27 | shell.mkdir('-p', path.join(directory)); 28 | } 29 | 30 | const testTemplate = { 31 | template: path.join(templatePath, 'template.view.js'), 32 | generated: path.join(directory, `${name}.view.js`) 33 | }; 34 | 35 | const testPlaceHolder = getAllPlaceholderNames(name); 36 | 37 | function assert() { 38 | expect(fs.existsSync(directory)).toBeTruthy(); 39 | expect(fs.existsSync(testTemplate.generated)).toBeTruthy(); 40 | expect(fs.readFileSync(testTemplate.generated, 'utf8')).toContain( 41 | 'Import all external modules here.' 42 | ); 43 | done(); 44 | } 45 | 46 | createTemplate(testTemplate, testPlaceHolder, false, assert); 47 | }); 48 | 49 | it('should create template without comments', done => { 50 | const name = 'some-directory-create-template-without-comments'; 51 | const templatePath = path.join(ROOT_PATH, 'templates', 'web', 'react'); 52 | const directory = path.join(ROOT_PATH, name); 53 | 54 | if (!fs.existsSync(directory)) { 55 | shell.mkdir('-p', path.join(directory)); 56 | } 57 | 58 | const testTemplate = { 59 | template: path.join(templatePath, 'template.view.js'), 60 | generated: path.join(directory, `${name}.view.js`) 61 | }; 62 | 63 | const testPlaceHolder = getAllPlaceholderNames(name); 64 | 65 | function assert() { 66 | expect(fs.existsSync(directory)).toBeTruthy(); 67 | expect(fs.existsSync(testTemplate.generated)).toBeTruthy(); 68 | expect(fs.readFileSync(testTemplate.generated, 'utf8')).not.toContain( 69 | 'Import all external modules here.' 70 | ); 71 | done(); 72 | } 73 | 74 | createTemplate(testTemplate, testPlaceHolder, true, assert); 75 | }); 76 | 77 | it('should format javascript code using prettier after it has been generated', done => { 78 | const name = 'some-directory-create-template'; 79 | const templatePath = path.join(ROOT_PATH, 'templates', 'web', 'react'); 80 | const directory = path.join(ROOT_PATH, name); 81 | 82 | if (!fs.existsSync(directory)) { 83 | shell.mkdir('-p', path.join(directory)); 84 | } 85 | 86 | const testTemplate = { 87 | template: path.join(templatePath, 'template.view.js'), 88 | generated: path.join(directory, `${name}.view.js`) 89 | }; 90 | 91 | const testPlaceHolder = getAllPlaceholderNames(name); 92 | 93 | const prettierSpy = jest.spyOn(prettier, 'format'); 94 | 95 | function assert() { 96 | expect(prettierSpy).toHaveBeenCalled(); 97 | prettierSpy.mockRestore(); 98 | done(); 99 | } 100 | 101 | createTemplate(testTemplate, testPlaceHolder, false, assert); 102 | }); 103 | 104 | it('should format sass code using prettier after it has been generated', done => { 105 | const name = 'some-directory-create-template'; 106 | const templatePath = path.join(ROOT_PATH, 'templates', 'web', 'react'); 107 | const directory = path.join(ROOT_PATH, name); 108 | 109 | if (!fs.existsSync(directory)) { 110 | shell.mkdir('-p', path.join(directory)); 111 | } 112 | 113 | const testTemplate = { 114 | template: path.join(templatePath, '_template.styles.scss'), 115 | generated: path.join(directory, `${name}.styles.scss`) 116 | }; 117 | 118 | const testPlaceHolder = getAllPlaceholderNames(name); 119 | 120 | const prettierSpy = jest.spyOn(prettier, 'format'); 121 | 122 | function assert() { 123 | expect(prettierSpy).toHaveBeenCalled(); 124 | prettierSpy.mockRestore(); 125 | done(); 126 | } 127 | 128 | createTemplate(testTemplate, testPlaceHolder, false, assert); 129 | }); 130 | }); 131 | 132 | describe('getAllDirectories', () => { 133 | describe('getReactComponentDirs', () => { 134 | describe('for web', () => { 135 | describe('NOT redux', () => { 136 | it('should return all templates with directories set name and create folder name, with test', () => { 137 | const name = 'some-name'; 138 | const directory = 'some-directory'; 139 | 140 | const actual = getAllDirectories(name, directory); 141 | 142 | const templates = path.join(ROOT_PATH, 'templates', 'web', 'react'); 143 | const folderName = name; 144 | const expected = { 145 | view: { 146 | template: path.join(templates, 'template.view.js'), 147 | generated: path.join( 148 | ROOT_PATH, 149 | directory, 150 | folderName, 151 | `${name}.view.js` 152 | ) 153 | }, 154 | viewTest: { 155 | template: path.join(templates, 'test', 'template.view.spec.js'), 156 | generated: path.join( 157 | ROOT_PATH, 158 | directory, 159 | folderName, 160 | 'test', 161 | `${name}.view.spec.js` 162 | ) 163 | }, 164 | stylesheet: { 165 | template: path.join(templates, '_template.styles.scss'), 166 | generated: path.join( 167 | ROOT_PATH, 168 | directory, 169 | folderName, 170 | `_${name}.styles.scss` 171 | ) 172 | } 173 | }; 174 | 175 | expect(actual).toEqual(expected); 176 | expect( 177 | fs.existsSync(path.join(ROOT_PATH, directory, folderName)) 178 | ).toBeTruthy(); 179 | }); 180 | }); 181 | 182 | describe('WITH redux', () => { 183 | it('should return all templates with directories set name and create folder name, with test', () => { 184 | const name = 'some-name-redux'; 185 | const directory = 'some-directory-redux'; 186 | 187 | const actual = getAllDirectories(name, directory, false, true); 188 | 189 | const templates = path.join(ROOT_PATH, 'templates', 'web', 'react-redux'); 190 | const folderName = name; 191 | const expected = { 192 | view: { 193 | template: path.join(templates, 'template.view.js'), 194 | generated: path.join( 195 | ROOT_PATH, 196 | directory, 197 | folderName, 198 | `${name}.view.js` 199 | ) 200 | }, 201 | viewTest: { 202 | template: path.join(templates, 'test', 'template.view.spec.js'), 203 | generated: path.join( 204 | ROOT_PATH, 205 | directory, 206 | folderName, 207 | 'test', 208 | `${name}.view.spec.js` 209 | ) 210 | }, 211 | stylesheet: { 212 | template: path.join(templates, '_template.styles.scss'), 213 | generated: path.join( 214 | ROOT_PATH, 215 | directory, 216 | folderName, 217 | `_${name}.styles.scss` 218 | ) 219 | }, 220 | container: { 221 | template: path.join(templates, 'template.container.js'), 222 | generated: path.join( 223 | ROOT_PATH, 224 | directory, 225 | folderName, 226 | `${name}.container.js` 227 | ) 228 | }, 229 | containerTest: { 230 | template: path.join( 231 | templates, 232 | 'test', 233 | 'template.container.spec.js' 234 | ), 235 | generated: path.join( 236 | ROOT_PATH, 237 | directory, 238 | folderName, 239 | 'test', 240 | `${name}.container.spec.js` 241 | ) 242 | }, 243 | 244 | reducer: { 245 | template: path.join(templates, 'template.reducer.js'), 246 | generated: path.join( 247 | ROOT_PATH, 248 | directory, 249 | folderName, 250 | `${name}.reducer.js` 251 | ) 252 | }, 253 | reducerTest: { 254 | template: path.join(templates, 'test', 'template.reducer.spec.js'), 255 | generated: path.join( 256 | ROOT_PATH, 257 | directory, 258 | folderName, 259 | 'test', 260 | `${name}.reducer.spec.js` 261 | ) 262 | } 263 | }; 264 | 265 | expect(actual).toEqual(expected); 266 | expect( 267 | fs.existsSync(path.join(ROOT_PATH, directory, folderName)) 268 | ).toBeTruthy(); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('for native', () => { 274 | describe('NOT redux', () => { 275 | it('should return all templates with directories set name and create folder name, with test', () => { 276 | const name = 'some-name-native'; 277 | const directory = 'some-directory-native'; 278 | 279 | const actual = getAllDirectories(name, directory, true); 280 | 281 | const templates = path.join(ROOT_PATH, 'templates', 'native', 'react'); 282 | const folderName = name; 283 | const expected = { 284 | view: { 285 | template: path.join(templates, 'template.view.js'), 286 | generated: path.join( 287 | ROOT_PATH, 288 | directory, 289 | folderName, 290 | `${name}.view.js` 291 | ) 292 | }, 293 | viewTest: { 294 | template: path.join(templates, 'test', 'template.view.spec.js'), 295 | generated: path.join( 296 | ROOT_PATH, 297 | directory, 298 | folderName, 299 | 'test', 300 | `${name}.view.spec.js` 301 | ) 302 | } 303 | }; 304 | 305 | expect(actual).toEqual(expected); 306 | expect( 307 | fs.existsSync(path.join(ROOT_PATH, directory, folderName)) 308 | ).toBeTruthy(); 309 | }); 310 | }); 311 | 312 | describe('WITH redux', () => { 313 | it('should return all templates with directories set name and create folder name, with test', () => { 314 | const name = 'some-name-redux'; 315 | const directory = 'some-directory-redux'; 316 | 317 | const actual = getAllDirectories(name, directory, true, true); 318 | 319 | const templates = path.join( 320 | ROOT_PATH, 321 | 'templates', 322 | 'native', 323 | 'react-redux' 324 | ); 325 | const folderName = name; 326 | const expected = { 327 | view: { 328 | template: path.join(templates, 'template.view.js'), 329 | generated: path.join( 330 | ROOT_PATH, 331 | directory, 332 | folderName, 333 | `${name}.view.js` 334 | ) 335 | }, 336 | viewTest: { 337 | template: path.join(templates, 'test', 'template.view.spec.js'), 338 | generated: path.join( 339 | ROOT_PATH, 340 | directory, 341 | folderName, 342 | 'test', 343 | `${name}.view.spec.js` 344 | ) 345 | }, 346 | container: { 347 | template: path.join(templates, 'template.container.js'), 348 | generated: path.join( 349 | ROOT_PATH, 350 | directory, 351 | folderName, 352 | `${name}.container.js` 353 | ) 354 | }, 355 | containerTest: { 356 | template: path.join( 357 | templates, 358 | 'test', 359 | 'template.container.spec.js' 360 | ), 361 | generated: path.join( 362 | ROOT_PATH, 363 | directory, 364 | folderName, 365 | 'test', 366 | `${name}.container.spec.js` 367 | ) 368 | }, 369 | 370 | reducer: { 371 | template: path.join(templates, 'template.reducer.js'), 372 | generated: path.join( 373 | ROOT_PATH, 374 | directory, 375 | folderName, 376 | `${name}.reducer.js` 377 | ) 378 | }, 379 | reducerTest: { 380 | template: path.join(templates, 'test', 'template.reducer.spec.js'), 381 | generated: path.join( 382 | ROOT_PATH, 383 | directory, 384 | folderName, 385 | 'test', 386 | `${name}.reducer.spec.js` 387 | ) 388 | } 389 | }; 390 | 391 | expect(actual).toEqual(expected); 392 | expect( 393 | fs.existsSync(path.join(ROOT_PATH, directory, folderName)) 394 | ).toBeTruthy(); 395 | }); 396 | }); 397 | }); 398 | }); 399 | 400 | describe('getReduxCoreDirs', () => { 401 | it('should return all templates with directories, set with name and create folders for redux core', () => { 402 | const directory = 'some-directory-redux-core'; 403 | 404 | let actual = getAllDirectories(null, null, null, null, true, directory); 405 | 406 | const templates = path.join(ROOT_PATH, 'templates', 'redux-core'); 407 | 408 | const expected = { 409 | store: { 410 | template: path.join(templates, 'store.js'), 411 | generated: path.join(ROOT_PATH, directory, 'store.js') 412 | }, 413 | rootReducer: { 414 | template: path.join(templates, 'root-reducer.js'), 415 | generated: path.join(ROOT_PATH, directory, 'root-reducer.js') 416 | }, 417 | createAction: { 418 | template: path.join(templates, 'action-utilities', 'action-creator.js'), 419 | generated: path.join( 420 | ROOT_PATH, 421 | directory, 422 | 'action-utilities', 423 | 'action-creator.js' 424 | ) 425 | }, 426 | createActionType: { 427 | template: path.join( 428 | templates, 429 | 'action-utilities', 430 | 'action-type-creator.js' 431 | ), 432 | generated: path.join( 433 | ROOT_PATH, 434 | directory, 435 | 'action-utilities', 436 | 'action-type-creator.js' 437 | ) 438 | }, 439 | createActionTest: { 440 | template: path.join( 441 | templates, 442 | 'action-utilities', 443 | 'test', 444 | 'action-creator.spec.js' 445 | ), 446 | generated: path.join( 447 | ROOT_PATH, 448 | directory, 449 | 'action-utilities', 450 | 'test', 451 | 'action-creator.spec.js' 452 | ) 453 | }, 454 | createActionTypeTest: { 455 | template: path.join( 456 | templates, 457 | 'action-utilities', 458 | 'test', 459 | 'action-type-creator.spec.js' 460 | ), 461 | generated: path.join( 462 | ROOT_PATH, 463 | directory, 464 | 'action-utilities', 465 | 'test', 466 | 'action-type-creator.spec.js' 467 | ) 468 | } 469 | }; 470 | 471 | expect(actual).toEqual(expected); 472 | expect(fs.existsSync(path.join(ROOT_PATH, directory))).toBeTruthy(); 473 | }); 474 | 475 | it('should return all templates with directories, set with name and create folders for redux core even if the directory already exists', () => { 476 | const directory = 'some-directory-redux-core'; 477 | 478 | let actual = getAllDirectories(null, null, null, null, true, directory); 479 | actual = getAllDirectories(null, null, null, null, true, directory); 480 | 481 | const templates = path.join(ROOT_PATH, 'templates', 'redux-core'); 482 | 483 | const expected = { 484 | store: { 485 | template: path.join(templates, 'store.js'), 486 | generated: path.join(ROOT_PATH, directory, 'store.js') 487 | }, 488 | rootReducer: { 489 | template: path.join(templates, 'root-reducer.js'), 490 | generated: path.join(ROOT_PATH, directory, 'root-reducer.js') 491 | }, 492 | createAction: { 493 | template: path.join(templates, 'action-utilities', 'action-creator.js'), 494 | generated: path.join( 495 | ROOT_PATH, 496 | directory, 497 | 'action-utilities', 498 | 'action-creator.js' 499 | ) 500 | }, 501 | createActionType: { 502 | template: path.join( 503 | templates, 504 | 'action-utilities', 505 | 'action-type-creator.js' 506 | ), 507 | generated: path.join( 508 | ROOT_PATH, 509 | directory, 510 | 'action-utilities', 511 | 'action-type-creator.js' 512 | ) 513 | }, 514 | createActionTest: { 515 | template: path.join( 516 | templates, 517 | 'action-utilities', 518 | 'test', 519 | 'action-creator.spec.js' 520 | ), 521 | generated: path.join( 522 | ROOT_PATH, 523 | directory, 524 | 'action-utilities', 525 | 'test', 526 | 'action-creator.spec.js' 527 | ) 528 | }, 529 | createActionTypeTest: { 530 | template: path.join( 531 | templates, 532 | 'action-utilities', 533 | 'test', 534 | 'action-type-creator.spec.js' 535 | ), 536 | generated: path.join( 537 | ROOT_PATH, 538 | directory, 539 | 'action-utilities', 540 | 'test', 541 | 'action-type-creator.spec.js' 542 | ) 543 | } 544 | }; 545 | 546 | expect(actual).toEqual(expected); 547 | expect(fs.existsSync(path.join(ROOT_PATH, directory))).toBeTruthy(); 548 | }); 549 | }); 550 | }); 551 | 552 | describe('getAllPlaceholderNames', () => { 553 | it('should return kebab case name', () => { 554 | const name = 'some-kebab-case-name'; 555 | 556 | const actual = getAllPlaceholderNames(name); 557 | 558 | const expected = { 559 | kebab: name, 560 | lowerCamel: 'someKebabCaseName', 561 | upperCamel: 'SomeKebabCaseName' 562 | }; 563 | 564 | expect(actual).toEqual(expected); 565 | }); 566 | }); 567 | 568 | describe('getRootPath', () => { 569 | it('should return root path', () => { 570 | console.log('resolvePath:', resolvePath.slice(0, resolvePath.indexOf('node_modules'))); 571 | 572 | const actual = getRootPath(); 573 | 574 | const expected = resolvePath.slice(0, resolvePath.indexOf('node_modules')); 575 | 576 | expect(actual).toEqual(expected); 577 | }); 578 | }); 579 | 580 | afterAll(() => { 581 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory')); 582 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory-native')); 583 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory-redux')); 584 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory-redux-core')); 585 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory-create-template')); 586 | shell.rm('-rf', path.join(ROOT_PATH, 'some-directory-create-template-without-comments')); 587 | }); 588 | }); 589 | -------------------------------------------------------------------------------- /templates/native/react-redux/template.container.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from './TEMPLATE_KEBAB_CASE_NAME.view'; 5 | import { 6 | exampleAction, 7 | exampleAsyncAction 8 | /* 9 | Import all the actions you wish to expose to the view here. 10 | */ 11 | } from './TEMPLATE_KEBAB_CASE_NAME.reducer'; 12 | 13 | export function mapStateToProps({ TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer }) { 14 | return { 15 | exampleVariable: TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer.exampleVariable 16 | /* 17 | Add all the state variables you wish to expose to the view here. 18 | */ 19 | }; 20 | } 21 | 22 | export function mapDispatchToProps(dispatch) { 23 | return bindActionCreators( 24 | { 25 | exampleAction, 26 | exampleAsyncAction 27 | /* 28 | Add all the actions you wish to expose to the view here. 29 | */ 30 | }, 31 | dispatch 32 | ); 33 | } 34 | 35 | export default connect(mapStateToProps, mapDispatchToProps)(TEMPLATE_UPPER_CAMEL_CASE_NAMEView); 36 | -------------------------------------------------------------------------------- /templates/native/react-redux/template.reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: Remember to add this reducer to the root reducer found in 'src/redux/root-reducer.js'. 3 | */ 4 | 5 | /* 6 | NOTE: The CreateAction directory my change based on your module's desired location. 7 | */ 8 | import CreateAction from '../../redux/action-utilities/action-creator'; 9 | /* 10 | Import all local modules here. 11 | */ 12 | 13 | const reducerName = 'TEMPLATE_KEBAB_CASE_NAME'; 14 | 15 | /* 16 | Here you can create all your actions with the CreateAction action factory (See example below). 17 | 18 | NOTE: It's important to use this factory to create standard actions so that all actions will be 19 | consistent - this will also eliminate duplicate code. 20 | */ 21 | const example = new CreateAction(reducerName, 'EXAMPLE_ACTION'); 22 | export const exampleAction = example.action; 23 | 24 | /* 25 | Add all Asynchronous actions here. These are used when the action needs to access state or when multiple 26 | actions need to be dispatched (See example below). 27 | 28 | NOTE: It's important to note that 'getState()' returns a shallow copy of state - so if you mutate it, 29 | your state will change along with it. Be careful of this! 30 | */ 31 | export function exampleAsyncAction() { 32 | return (dispatch, getState) => { 33 | const { TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer } = getState(); 34 | dispatch(exampleAction(!TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer.exampleVariable)); 35 | }; 36 | } 37 | 38 | export const initialState = { 39 | exampleVariable: true 40 | /* 41 | Add your reducer's initial state here. 42 | */ 43 | }; 44 | 45 | export default function TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(state = initialState, action) { 46 | switch (action.type) { 47 | case example.actionType: 48 | return { ...state, exampleVariable: action.payload }; 49 | default: 50 | return state; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /templates/native/react-redux/template.view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { StyleSheet, View, Text /*Add other React Native components here*/ } from 'react-native'; 4 | /* 5 | Import all external modules here. 6 | */ 7 | 8 | /* 9 | Import all local modules here. 10 | */ 11 | 12 | export default class TEMPLATE_UPPER_CAMEL_CASE_NAMEView extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | /* 17 | Add all required function binding here. If no binding is required, the constructor may be omitted. 18 | */ 19 | this.exampleHelper = this.exampleHelper.bind(this); 20 | 21 | /* 22 | NOTE: Do NOT use 'this.state'! State management is wat Redux is used for. 23 | */ 24 | } 25 | 26 | componentDidMount() { 27 | /* 28 | If your screen needs to load data via the API immediately after rendering, 29 | call the load functions here in the `componentDidMount` function (See example below) - 30 | else, the `componentDidMount` function may be omitted. 31 | */ 32 | this.props.exampleAsyncAction(); 33 | } 34 | 35 | render() { 36 | return {this.exampleHelper()}; 37 | } 38 | 39 | /* 40 | Add all helper functions here. Remember to 'bind' functions where 'this' 41 | needs to be used (See example below). 42 | */ 43 | exampleHelper() { 44 | this.props.exampleAction(true); 45 | if (!this.props.exampleVariable) { 46 | return {'My template has been fiddled with'}; 47 | } 48 | return {'Hello World! Welcome to my template'}; 49 | } 50 | } 51 | 52 | /* 53 | Add all the props (variables and functions) being passe through to this component (by redux or inline). 54 | This will make code completion and IDE inspection possible, and will make it easy to see what data is relevant 55 | in this module. 56 | */ 57 | TEMPLATE_UPPER_CAMEL_CASE_NAMEView.propTypes = { 58 | exampleVariable: PropTypes.bool, 59 | 60 | exampleAction: PropTypes.func, 61 | exampleAsyncAction: PropTypes.func 62 | }; 63 | 64 | /* 65 | Add all the component styling here. 66 | */ 67 | const styles = StyleSheet.create({ 68 | text: { 69 | fontSize: 20, 70 | textAlign: 'center' 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /templates/native/react-redux/test/template.container.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('redux'); 2 | 3 | import { bindActionCreators } from 'redux'; 4 | import { mapStateToProps, mapDispatchToProps } from '../TEMPLATE_KEBAB_CASE_NAME.container'; 5 | import { 6 | exampleAction, 7 | exampleAsyncAction, 8 | /* 9 | Import all the actions you wish to expose to the view here. 10 | */ 11 | initialState 12 | } from '../TEMPLATE_KEBAB_CASE_NAME.reducer'; 13 | 14 | describe('TEMPLATE_LOWER_CAMEL_CASE_NAMEContainer - Unit test', () => { 15 | function stateBefore() { 16 | return { 17 | TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer: { 18 | ...initialState 19 | /* 20 | Setup your initial state for testing here. 21 | */ 22 | } 23 | }; 24 | } 25 | 26 | it('should map state to props', () => { 27 | const actual = mapStateToProps(stateBefore()); 28 | 29 | const expected = { 30 | ...initialState 31 | /* 32 | Setup your initial state for verification here. 33 | */ 34 | }; 35 | 36 | expect(actual).toEqual(expected); 37 | }); 38 | 39 | it('should map dispatch to props', () => { 40 | const dispatch = jest.fn(); 41 | 42 | mapDispatchToProps(dispatch); 43 | 44 | expect(bindActionCreators).toHaveBeenCalledWith( 45 | { 46 | exampleAction, 47 | exampleAsyncAction 48 | /* 49 | Import all the actions you wish to expose to the view here. 50 | */ 51 | }, 52 | dispatch 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /templates/native/react-redux/test/template.reducer.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Add mocking setup here such as in the example: 3 | 4 | jest.mock('../../../service/some-api'); 5 | import someApi from '../../../service/some-api'; 6 | */ 7 | 8 | import TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer, { 9 | exampleAction, 10 | exampleAsyncAction, 11 | /* 12 | Add all the actions you wish to test here. 13 | */ 14 | initialState 15 | } from '../TEMPLATE_KEBAB_CASE_NAME.reducer'; 16 | 17 | describe('TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer - Unit Test', () => { 18 | function stateBefore() { 19 | return { 20 | ...initialState 21 | /* 22 | Setup your initial state for testing here. 23 | */ 24 | }; 25 | } 26 | 27 | it('should return initial state when action is undefined', () => { 28 | const action = {}; 29 | 30 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(undefined, action); 31 | 32 | const expected = { 33 | ...stateBefore() 34 | }; 35 | 36 | expect(actual).toEqual(expected); 37 | }); 38 | 39 | it('should return current state when unknown action is dispatched', () => { 40 | const action = { type: 'SOME_UNKNOWN_ACTION' }; 41 | 42 | const currentState = { 43 | ...stateBefore() 44 | }; 45 | 46 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(currentState, action); 47 | 48 | const expected = { 49 | ...stateBefore() 50 | }; 51 | 52 | expect(actual).toEqual(expected); 53 | }); 54 | 55 | /* 56 | Add all your action tests here. Add each action in its own 'describe' and test 57 | each of the action's scenarios in its own 'it'. Remember to test happy and sad cases. 58 | "Every bug is a test that wasn't written" 59 | */ 60 | describe('exampleAction', () => { 61 | it('should send humans to Mars to recolonise', () => { 62 | const action = exampleAction(false); 63 | 64 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(stateBefore(), action); 65 | 66 | const expected = { 67 | ...stateBefore(), 68 | exampleVariable: false 69 | }; 70 | 71 | expect(actual).toEqual(expected); 72 | }); 73 | }); 74 | 75 | /* 76 | Add all your asynchronous action tests here. Add each asynchronous action in its own 'describe' and test 77 | each of the asynchronous action's scenarios in its own 'it'. Remember to test happy and sad cases. 78 | If you have no asynchronous actions, this code may be omitted. 79 | */ 80 | describe('exampleAsyncAction', () => { 81 | it('should dispatch exampleAction', () => { 82 | /* 83 | Mock dispatch and getState 84 | Note: If your Jest is on version 23 or higher you may use 'mockName' - this helps to 85 | indicate which mock function is being referenced. This can be used as shown in the example below: 86 | 87 | const getState = jest.fn().mockName('getState'); 88 | 89 | const dispatch = jest.fn().mockName('dispatch'); 90 | 91 | */ 92 | const getState = jest.fn().mockImplementation(() => ({ 93 | TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer: { exampleVariable: false } 94 | })); 95 | const dispatch = jest.fn().mockImplementation(); 96 | 97 | /* 98 | Test your async action using the mocked dispatch and getState 99 | */ 100 | exampleAsyncAction()(dispatch, getState); 101 | 102 | /* 103 | Assert that dispatch has been called with example action 104 | Note: If your Jest is on version 23 or higher you may use 'toHaveBeenNthCalledWith' - this 105 | allows you to check the order in which your actions were called. This can be used as 106 | shown in the example below: 107 | 108 | expect(dispatch).toHaveBeenNthCalledWith(1, exampleAction(true)); 109 | */ 110 | expect(dispatch).toHaveBeenCalledWith(exampleAction(true)); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /templates/native/react-redux/test/template.view.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from '../TEMPLATE_KEBAB_CASE_NAME.view'; 5 | 6 | describe('TEMPLATE_UPPER_CAMEL_CASE_NAMEView - DOES IT RENDER', () => { 7 | /* 8 | Here you can mock out any view function props not needed for rendering 9 | (i.e. state read/write functions from the reducer) so as to only test the view. 10 | 11 | These mocks may be omitted if no function props need to be mocked. 12 | */ 13 | const exampleActionMock = jest.fn(); 14 | const exampleAsyncActionMock = jest.fn(); 15 | 16 | it('should render without crashing', () => { 17 | shallow( 18 | 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /templates/native/react/template.view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { StyleSheet, View, Text /*Add other React Native components here*/ } from 'react-native'; 4 | /* 5 | Import all external modules here. 6 | */ 7 | 8 | /* 9 | Import all local modules here. 10 | */ 11 | 12 | export default class TEMPLATE_UPPER_CAMEL_CASE_NAMEView extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | /* 17 | Add all required function binding here. If no binding is required, the constructor may be omitted. 18 | */ 19 | this.renderTextExample = this.renderTextExample.bind(this); 20 | } 21 | 22 | componentDidMount() { 23 | /* 24 | If your screen needs to load data via the API immediately after rendering, 25 | call the load functions here in the `componentDidMount` function (See example below) - 26 | else, the `componentDidMount` function may be omitted. 27 | */ 28 | } 29 | 30 | render() { 31 | return {this.renderTextExample()}; 32 | } 33 | 34 | /* 35 | Add all helper functions here. Remember to 'bind' functions where 'this' 36 | needs to be used (See example below). 37 | */ 38 | renderTextExample() { 39 | const message = `Welcome to our template view ${this.props.exampleVariable || '( ͡° ͜ʖ ͡°)'}`; 40 | return {message}; 41 | } 42 | } 43 | 44 | /* 45 | Add all the props (variables and functions) being passe through to this component (by redux or inline). 46 | This will make code completion and IDE inspection possible, and will make it easy to see what data is relevant 47 | in this module. 48 | */ 49 | TEMPLATE_UPPER_CAMEL_CASE_NAMEView.propTypes = { 50 | exampleVariable: PropTypes.string 51 | }; 52 | 53 | /* 54 | Add all the component styling here. 55 | */ 56 | const styles = StyleSheet.create({ 57 | text: { 58 | fontSize: 20, 59 | textAlign: 'center' 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /templates/native/react/test/template.view.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from '../TEMPLATE_KEBAB_CASE_NAME.view'; 5 | 6 | describe('TEMPLATE_UPPER_CAMEL_CASE_NAMEView - DOES IT RENDER', () => { 7 | /* 8 | Here you can mock out any view function props not needed for rendering 9 | (i.e. state read/write functions from the reducer) so as to only test the view. 10 | 11 | These mocks may be omitted if no function props need to be mocked. 12 | */ 13 | const exampleVariableMock = 'some string'; 14 | 15 | it('should render without crashing', () => { 16 | shallow(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /templates/redux-core/action-utilities/action-creator.js: -------------------------------------------------------------------------------- 1 | import createActionType from './action-type-creator'; 2 | 3 | export default function CreateAction(reducerName, actionName) { 4 | if (!reducerName || !actionName) { 5 | throw 'Please provide a valid reducer and action name'; 6 | } 7 | const actionType = createActionType(reducerName, actionName); 8 | return { 9 | actionType, 10 | action: payload => ({ 11 | type: actionType, 12 | payload 13 | }) 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /templates/redux-core/action-utilities/action-type-creator.js: -------------------------------------------------------------------------------- 1 | /* 2 | Add your app's name here. 3 | */ 4 | export const appName = 'YourReduxApp'; 5 | 6 | export default function createActionType(reducerName = '', actionName = '') { 7 | reducerName = reducerName.toString().trim(); 8 | if (!reducerName) { 9 | throw new Error('Reducer name cannot be blank'); 10 | } 11 | actionName = actionName.toString().trim(); 12 | if (!actionName) { 13 | throw new Error('Action name cannot be blank'); 14 | } 15 | return `${appName}/${reducerName}/${actionName}`; 16 | } 17 | -------------------------------------------------------------------------------- /templates/redux-core/action-utilities/test/action-creator.spec.js: -------------------------------------------------------------------------------- 1 | import CreateAction from '../action-creator'; 2 | import { appName } from '../action-type-creator'; 3 | 4 | describe('Create Action - Unit Test', () => { 5 | it('should create an action for reducer', () => { 6 | const reducerName = 'some_reducer'; 7 | const actionName = 'SOME_ACTION'; 8 | 9 | const actual = new CreateAction(reducerName, actionName); 10 | 11 | const expected = { 12 | actionType: `${appName}/some_reducer/SOME_ACTION`, 13 | action: { 14 | type: `${appName}/some_reducer/SOME_ACTION`, 15 | payload: { 16 | name: 'field name', 17 | value: 'field value' 18 | } 19 | } 20 | }; 21 | 22 | expect(actual.actionType).toEqual(expected.actionType); 23 | expect( 24 | actual.action({ 25 | name: 'field name', 26 | value: 'field value' 27 | }) 28 | ).toEqual(expected.action); 29 | }); 30 | 31 | it('should fail if no reducer name is provided', () => { 32 | expect(CreateAction).toThrow('Please provide a valid reducer and action name'); 33 | }); 34 | 35 | it('should fail if no action name is provided', () => { 36 | expect(() => CreateAction('some_reducer')).toThrow( 37 | 'Please provide a valid reducer and action name' 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /templates/redux-core/action-utilities/test/action-type-creator.spec.js: -------------------------------------------------------------------------------- 1 | import createActionType, { appName } from '../action-type-creator'; 2 | 3 | describe('Build Action Name - Unit Test', () => { 4 | it('should return an action name', () => { 5 | const actual = createActionType('someReducer', 'SOME_ACTION'); 6 | const expected = `${appName}/someReducer/SOME_ACTION`; 7 | expect(actual).toEqual(expected); 8 | }); 9 | 10 | it('should throw error when reducer name not given', () => { 11 | expect(createActionType).toThrow('Reducer name cannot be blank'); 12 | }); 13 | 14 | it('should throw error when action name not given', () => { 15 | expect(() => createActionType('someReducer')).toThrow('Action name cannot be blank'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /templates/redux-core/root-reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | /* 4 | Import all your reducers here. 5 | */ 6 | 7 | export default combineReducers({ 8 | /* Add all your reducers here*/ 9 | }); 10 | -------------------------------------------------------------------------------- /templates/redux-core/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import ReduxThunk from 'redux-thunk'; 3 | 4 | import rootReducer from './root-reducer'; 5 | 6 | /* 7 | Use this as the store for your application (usually added in the `index.js` by using Redux's 8 | Provide to render you app). 9 | */ 10 | export default createStore(rootReducer, applyMiddleware(ReduxThunk)); 11 | -------------------------------------------------------------------------------- /templates/web/react-redux/_template.styles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: Remember to add this style sheet o the main style sheet found in 'src/index.scss'. 3 | */ 4 | 5 | /* 6 | Add all the component styling and nested styling here. 7 | */ 8 | 9 | .TEMPLATE_KEBAB_CASE_NAME-wrapper { 10 | background-color: purple; 11 | text-align: center; 12 | padding: 10px; 13 | 14 | h1 { 15 | color: orange; 16 | font-size: 40px; 17 | } 18 | 19 | .button { 20 | color: black; 21 | font-size: 26px; 22 | border-radius: 5px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/web/react-redux/template.container.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from './TEMPLATE_KEBAB_CASE_NAME.view'; 5 | import { 6 | exampleAction, 7 | exampleAsyncAction 8 | /* 9 | Import all the actions you wish to expose to the view here. 10 | */ 11 | } from './TEMPLATE_KEBAB_CASE_NAME.reducer'; 12 | 13 | export function mapStateToProps({ TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer }) { 14 | return { 15 | exampleVariable: TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer.exampleVariable 16 | /* 17 | Add all the state variables you wish to expose to the view here. 18 | */ 19 | }; 20 | } 21 | 22 | export function mapDispatchToProps(dispatch) { 23 | return bindActionCreators( 24 | { 25 | exampleAction, 26 | exampleAsyncAction 27 | /* 28 | Add all the actions you wish to expose to the view here. 29 | */ 30 | }, 31 | dispatch 32 | ); 33 | } 34 | 35 | export default connect(mapStateToProps, mapDispatchToProps)(TEMPLATE_UPPER_CAMEL_CASE_NAMEView); 36 | -------------------------------------------------------------------------------- /templates/web/react-redux/template.reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: Remember to add this reducer to the root reducer found in 'src/redux/root-reducer.js'. 3 | */ 4 | 5 | /* 6 | NOTE: The CreateAction directory my change based on your module's desired location. 7 | */ 8 | import CreateAction from '../../redux/action-utilities/action-creator'; 9 | /* 10 | Import all local modules here. 11 | */ 12 | 13 | const reducerName = 'TEMPLATE_KEBAB_CASE_NAME'; 14 | 15 | /* 16 | Here you can create all your actions with the CreateAction action factory (See example below). 17 | 18 | NOTE: It's important to use this factory to create standard actions so that all actions will be 19 | consistent - this will also eliminate duplicate code. 20 | */ 21 | const example = new CreateAction(reducerName, 'EXAMPLE_ACTION'); 22 | export const exampleAction = example.action; 23 | 24 | /* 25 | Add all Asynchronous actions here. These are used when the action needs to access state or when multiple 26 | actions need to be dispatched (See example below). 27 | 28 | NOTE: It's important to note that 'getState()' returns a shallow copy of state - so if you mutate it, 29 | your state will change along with it. Be careful of this! 30 | */ 31 | export function exampleAsyncAction() { 32 | return (dispatch, getState) => { 33 | const { TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer } = getState(); 34 | dispatch(exampleAction(!TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer.exampleVariable)); 35 | }; 36 | } 37 | 38 | export const initialState = { 39 | exampleVariable: false 40 | /* 41 | Add your reducer's initial state here. 42 | */ 43 | }; 44 | 45 | export default function TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(state = initialState, action) { 46 | switch (action.type) { 47 | case example.actionType: 48 | return { ...state, exampleVariable: action.payload }; 49 | default: 50 | return state; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /templates/web/react-redux/template.view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | /* 4 | Import all external modules here. 5 | */ 6 | 7 | /* 8 | Import all local modules here. 9 | */ 10 | 11 | export default class TEMPLATE_UPPER_CAMEL_CASE_NAMEView extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | /* 16 | Add all required function binding here. If no binding is required, the constructor may be omitted. 17 | */ 18 | this.renderHeaderExample = this.renderHeaderExample.bind(this); 19 | this.renderButtonExample = this.renderButtonExample.bind(this); 20 | 21 | /* 22 | NOTE: Do NOT use 'this.state'! State management is wat Redux is used for. 23 | */ 24 | } 25 | 26 | componentDidMount() { 27 | /* 28 | If your screen needs to load data via the API immediately after rendering, 29 | call the load functions here in the `componentDidMount` function (See example below) - 30 | else, the `componentDidMount` function may be omitted. 31 | */ 32 | this.props.exampleAsyncAction(); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | {this.renderHeaderExample()} 39 | {this.renderButtonExample()} 40 |
41 | ); 42 | } 43 | 44 | /* 45 | Add all helper functions here. Remember to 'bind' functions where 'this' 46 | needs to be used (See examples below). If 'this' does not need to be accessed, 47 | the function may be made static. 48 | */ 49 | renderHeaderExample() { 50 | if (!this.props.exampleVariable) { 51 | return

{"You've now fiddled with the template ( ͡° ͜ʖ ͡°)"}

; 52 | } 53 | return

{'Welcome to our template (ง •̀_•́)ง'}

; 54 | } 55 | 56 | renderButtonExample() { 57 | return ( 58 | 61 | ); 62 | } 63 | } 64 | 65 | /* 66 | Add all the props (variables and functions) being passe through to this component (by redux or inline). 67 | This will make code completion and IDE inspection possible, and will make it easy to see what data is relevant 68 | in this module. 69 | */ 70 | TEMPLATE_UPPER_CAMEL_CASE_NAMEView.propTypes = { 71 | exampleVariable: PropTypes.bool, 72 | 73 | exampleAction: PropTypes.func, 74 | exampleAsyncAction: PropTypes.func 75 | }; 76 | -------------------------------------------------------------------------------- /templates/web/react-redux/test/template.container.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('redux'); 2 | 3 | import { bindActionCreators } from 'redux'; 4 | import { mapStateToProps, mapDispatchToProps } from '../TEMPLATE_KEBAB_CASE_NAME.container'; 5 | import { 6 | exampleAction, 7 | exampleAsyncAction, 8 | /* 9 | Import all the actions you wish to expose to the view here. 10 | */ 11 | initialState 12 | } from '../TEMPLATE_KEBAB_CASE_NAME.reducer'; 13 | 14 | describe('TEMPLATE_LOWER_CAMEL_CASE_NAMEContainer - Unit test', () => { 15 | function stateBefore() { 16 | return { 17 | TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer: { 18 | ...initialState 19 | /* 20 | Setup your initial state for testing here. 21 | */ 22 | } 23 | }; 24 | } 25 | 26 | it('should map state to props', () => { 27 | const actual = mapStateToProps(stateBefore()); 28 | 29 | const expected = { 30 | ...initialState 31 | /* 32 | Setup your initial state for verification here. 33 | */ 34 | }; 35 | 36 | expect(actual).toEqual(expected); 37 | }); 38 | 39 | it('should map dispatch to props', () => { 40 | const dispatch = jest.fn(); 41 | 42 | mapDispatchToProps(dispatch); 43 | 44 | expect(bindActionCreators).toHaveBeenCalledWith( 45 | { 46 | exampleAction, 47 | exampleAsyncAction 48 | /* 49 | Import all the actions you wish to expose to the view here. 50 | */ 51 | }, 52 | dispatch 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /templates/web/react-redux/test/template.reducer.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Add mocking setup here such as in the example: 3 | 4 | jest.mock('../../../service/some-api'); 5 | import SomeApi from '../../../service/some-api'; 6 | */ 7 | 8 | import TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer, { 9 | exampleAction, 10 | exampleAsyncAction, 11 | /* 12 | Add all the actions you wish to test here. 13 | */ 14 | initialState 15 | } from '../TEMPLATE_KEBAB_CASE_NAME.reducer'; 16 | 17 | describe('TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer - Unit Test', () => { 18 | function stateBefore() { 19 | return { 20 | ...initialState 21 | /* 22 | Setup your initial state for testing here. 23 | */ 24 | }; 25 | } 26 | 27 | it('should return initial state when action is undefined', () => { 28 | const action = {}; 29 | 30 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(undefined, action); 31 | 32 | const expected = { 33 | ...stateBefore() 34 | }; 35 | 36 | expect(actual).toEqual(expected); 37 | }); 38 | 39 | it('should return current state when unknown action is dispatched', () => { 40 | const action = { type: 'SOME_UNKNOWN_ACTION' }; 41 | 42 | const currentState = { 43 | ...stateBefore() 44 | }; 45 | 46 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(currentState, action); 47 | 48 | const expected = { 49 | ...stateBefore() 50 | }; 51 | 52 | expect(actual).toEqual(expected); 53 | }); 54 | 55 | /* 56 | Add all your action tests here. Add each action in its own 'describe' and test 57 | each of the action's scenarios in its own 'it'. Remember to test happy and sad cases. 58 | "Every bug is a test that wasn't written" 59 | */ 60 | describe('exampleAction', () => { 61 | it('should send humans to Mars to recolonise', () => { 62 | const action = exampleAction(false); 63 | 64 | const actual = TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer(stateBefore(), action); 65 | 66 | const expected = { 67 | ...stateBefore(), 68 | exampleVariable: false 69 | }; 70 | 71 | expect(actual).toEqual(expected); 72 | }); 73 | }); 74 | 75 | /* 76 | Add all your asynchronous action tests here. Add each asynchronous action in its own 'describe' and test 77 | each of the asynchronous action's scenarios in its own 'it'. Remember to test happy and sad cases. 78 | If you have no asynchronous actions, this code may be omitted. 79 | */ 80 | describe('exampleAsyncAction', () => { 81 | it('should dispatch exampleAction', () => { 82 | /* 83 | Mock dispatch and getState 84 | Note: If your Jest is on version 23 or higher you may use 'mockName' - this helps to 85 | indicate which mock function is being referenced. This can be used as shown in the example below: 86 | 87 | const getState = jest.fn().mockName('getState'); 88 | 89 | const dispatch = jest.fn().mockName('dispatch'); 90 | 91 | */ 92 | const getState = jest.fn().mockImplementation(() => ({ 93 | TEMPLATE_LOWER_CAMEL_CASE_NAMEReducer: { exampleVariable: false } 94 | })); 95 | const dispatch = jest.fn().mockImplementation(); 96 | 97 | /* 98 | Test your async action using the mocked dispatch and getState 99 | */ 100 | exampleAsyncAction()(dispatch, getState); 101 | 102 | /* 103 | Assert that dispatch has been called with example action 104 | Note: If your Jest is on version 23 or higher you may use 'toHaveBeenNthCalledWith' - this 105 | allows you to check the order in which your actions were called. This can be used as 106 | shown in the example below: 107 | 108 | expect(dispatch).toHaveBeenNthCalledWith(1, exampleAction(true)); 109 | */ 110 | expect(dispatch).toHaveBeenCalledWith(exampleAction(true)); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /templates/web/react-redux/test/template.view.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from '../TEMPLATE_KEBAB_CASE_NAME.view'; 5 | 6 | describe('TEMPLATE_UPPER_CAMEL_CASE_NAMEView - Unit Test', () => { 7 | /* 8 | Here you can mock out any view function props not needed for rendering 9 | (i.e. state read/write functions from the reducer) so as to only test the view. 10 | 11 | These mocks may be omitted if no function props need to be mocked. 12 | */ 13 | const exampleActionMock = jest.fn(); 14 | const exampleAsyncActionMock = jest.fn(); 15 | 16 | it('should render without crashing', () => { 17 | shallow( 18 | 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /templates/web/react/_template.styles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | NOTE: Remember to add this style sheet o the main style sheet found in 'src/index.scss'. 3 | */ 4 | 5 | /* 6 | Add all the component styling and nested styling here. 7 | */ 8 | 9 | .TEMPLATE_KEBAB_CASE_NAME-wrapper { 10 | background-color: purple; 11 | text-align: center; 12 | padding: 10px; 13 | 14 | h1 { 15 | color: orange; 16 | font-size: 40px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/web/react/template.view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | /* 4 | Import all external modules here. 5 | */ 6 | 7 | /* 8 | Import all local modules here. 9 | */ 10 | 11 | export default class TEMPLATE_UPPER_CAMEL_CASE_NAMEView extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | /* 16 | Add all required function binding here. If no binding is required, the constructor may be omitted. 17 | */ 18 | this.renderTextExample = this.renderTextExample.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | /* 23 | If your screen needs to load data via the API immediately after rendering, 24 | call the load functions here in the `componentDidMount` function (See example below) - 25 | else, the `componentDidMount` function may be omitted. 26 | */ 27 | } 28 | 29 | render() { 30 | return
{this.renderTextExample()}
; 31 | } 32 | 33 | /* 34 | Add all helper functions here. Remember to 'bind' functions where 'this' 35 | needs to be used (See examples below). If 'this' does not need to be accessed, 36 | the function may be made static. 37 | */ 38 | renderTextExample() { 39 | const message = `Welcome to our template view ${this.props.exampleVariable || '( ͡° ͜ʖ ͡°)'}`; 40 | return

{message}

; 41 | } 42 | } 43 | 44 | /* 45 | Add all the props (variables and functions) being passe through to this component (by redux or inline). 46 | This will make code completion and IDE inspection possible, and will make it easy to see what data is relevant 47 | in this module. 48 | */ 49 | TEMPLATE_UPPER_CAMEL_CASE_NAMEView.propTypes = { 50 | exampleVariable: PropTypes.string 51 | }; 52 | -------------------------------------------------------------------------------- /templates/web/react/test/template.view.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import TEMPLATE_UPPER_CAMEL_CASE_NAMEView from '../TEMPLATE_KEBAB_CASE_NAME.view'; 5 | 6 | describe('TEMPLATE_UPPER_CAMEL_CASE_NAMEView - Unit Test', () => { 7 | /* 8 | Here you can mock out any view function props not needed for rendering 9 | (i.e. state read/write functions passed in as props) so as to only test the view. 10 | 11 | These mocks may be omitted if no function props need to be mocked. 12 | */ 13 | 14 | it('should render without crashing', () => { 15 | shallow(); 16 | }); 17 | }); 18 | --------------------------------------------------------------------------------