├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── commands ├── ajax │ └── index.js ├── rm │ └── index.js └── up │ ├── checkExtends.js │ ├── checkForChanges.js │ ├── checkForConfigFile.js │ ├── checkForDebrisInNameOnly.js │ ├── detectDiff.js │ ├── index.js │ ├── runPrettier.js │ ├── writeActionTests │ ├── index.js │ └── writeActionTests.js │ ├── writeActions │ ├── index.js │ ├── writeActions.js │ └── writeOneAction.js │ ├── writeAllFiles.js │ ├── writeConstants │ ├── index.js │ ├── writeConstants.js │ └── writeOneConstant.js │ ├── writeIndex │ ├── index.js │ ├── writeImportsToIndex.js │ └── writeIndex.js │ ├── writeReducer │ ├── addActionsToInitialReducer.js │ ├── addCustomFunctions.js │ ├── addToCombineReducers.js │ ├── importConstants.js │ ├── index.js │ ├── writeActionsInReducer.js │ ├── writeInitialReducer.js │ └── writeOneReducer.js │ ├── writeReducerTests │ ├── index.js │ └── writeReducerTests.js │ ├── writeSaga │ ├── index.js │ ├── writeNameControlSaga.js │ └── writeSaga.js │ ├── writeSelectorTests │ ├── index.js │ └── writeSelectorTests.js │ └── writeSelectors │ ├── index.js │ └── writeSelectors.js ├── index.js ├── logo.png ├── package-lock.json ├── package.json ├── readme.md ├── tools ├── cases.js ├── checkErrorsInSchema.js ├── checkIfBadBuffer.js ├── checkIfDomainAlreadyPresent.js ├── checkIfNoAllSagas.js ├── checkWarningsInSchema.js ├── constants │ └── reservedKeywords.js ├── parser.js ├── printError.js ├── printMessages.js ├── printWarning.js ├── replaceInNameOnly.js ├── tests │ ├── cases.test.js │ ├── ensureImport.test.js │ ├── mocks │ │ ├── nameOnly.2.correct.js │ │ ├── nameOnly.2.js │ │ ├── nameOnly.js │ │ ├── nameOnlyCorrect.js │ │ └── withInlineImports.js │ ├── parser.test.js │ ├── replaceInNameOnly.test.js │ └── utils.test.js └── utils.js ├── yarn-error.log └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | *.test.js 3 | *mocks -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "quotes": "off", 5 | "no-console": "off", 6 | "operator-linebreak": "off", 7 | "func-names": "off", 8 | "space-before-function-paren": "off", 9 | "arrow-parens": "off", 10 | "prefer-template": "off", 11 | "implicit-arrow-linebreak": "off", 12 | "object-curly-newline": "off", 13 | "function-paren-newline": "off", 14 | "no-use-before-define": "off", 15 | "indent": "off", 16 | "no-nested-ternary": "off", 17 | "no-confusing-arrow": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 80 8 | } -------------------------------------------------------------------------------- /commands/ajax/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { concat, parseCamelCaseToArray } = require('../../tools/utils'); 4 | const Cases = require('../../tools/cases'); 5 | 6 | module.exports = (folder, camelCase, file) => { 7 | let fileToChange = file; 8 | const array = parseCamelCaseToArray(camelCase); 9 | const cases = new Cases(array).all(); 10 | 11 | if (!fileToChange) { 12 | let fixedFolder = folder; 13 | if (folder[folder.length - 1] !== '/') { 14 | fixedFolder += '/'; 15 | } 16 | fileToChange = path.resolve(`${fixedFolder}suit.json`); 17 | } 18 | 19 | let buffer = concat(['{', ' ', '}']); 20 | let includeComma = false; 21 | if (fs.existsSync(fileToChange)) { 22 | const newSchema = fs.readFileSync(fileToChange).toString(); 23 | const anyContent = Object.keys(JSON.parse(newSchema)).length; 24 | includeComma = anyContent; 25 | if (anyContent) buffer = newSchema; 26 | } 27 | console.log(` ${fileToChange} `.bgGreen.black); 28 | console.log( 29 | '\n SUIT: '.green + 30 | (includeComma ? 'writing existing suit.json' : 'writing new suit.json'), 31 | ); 32 | fs.writeFileSync( 33 | fileToChange, 34 | buffer.slice(0, -3) + 35 | concat([ 36 | includeComma ? ',' : null, 37 | ` "${cases.camel}": {`, 38 | ` "describe": "Makes a ${cases.display} API call",`, 39 | ` "initialState": {`, 40 | ` "isLoading": false,`, 41 | ` "hasSucceeded": false,`, 42 | ` "hasError": false,`, 43 | ` "errorMessage": "",`, 44 | ` "data": {}`, 45 | ` },`, 46 | ` "actions": {`, 47 | ` "${cases.camel}Started": {`, 48 | ` "describe": "Begins the ${ 49 | cases.display 50 | } API Call. Passes a payload to the saga.",`, 51 | ` "saga": {`, 52 | ` "onFail": "${cases.camel}Failed",`, 53 | ` "onSuccess": "${cases.camel}Succeeded"`, 54 | ` },`, 55 | ` "passAsProp": true,`, 56 | ` "payload": true,`, 57 | ` "set": {`, 58 | ` "isLoading": true,`, 59 | ` "hasSucceeded": false,`, 60 | ` "hasError": false,`, 61 | ` "errorMessage": "",`, 62 | ` "data": {}`, 63 | ` }`, 64 | ` },`, 65 | ` "${cases.camel}Succeeded": {`, 66 | ` "describe": "Called when the ${ 67 | cases.display 68 | } API call completes, passing the data as a payload.",`, 69 | ` "set": {`, 70 | ` "isLoading": false,`, 71 | ` "data": "payload",`, 72 | ` "hasSucceeded": true`, 73 | ` }`, 74 | ` },`, 75 | ` "${cases.camel}Failed": {`, 76 | ` "describe": "Called when the ${ 77 | cases.display 78 | } API Call fails, delivering a standard error message.",`, 79 | ` "set": {`, 80 | ` "isLoading": false,`, 81 | ` "errorMessage": "${cases.display} has failed",`, 82 | ` "hasError": true`, 83 | ` }`, 84 | ` }`, 85 | ` }`, 86 | ` }`, 87 | ]) + 88 | buffer.slice(-3), 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /commands/rm/index.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); // eslint-disable-line 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { cleanFile } = require('../../tools/utils'); 5 | 6 | const removeSuitFromFile = filename => { 7 | fs.writeFileSync( 8 | filename, 9 | cleanFile(fs.readFileSync(path.resolve(filename)).toString()), 10 | ); 11 | }; 12 | 13 | const rm = (folder, { silent = false } = {}) => { 14 | let fixedFolder = folder; 15 | if (folder[folder.length - 1] !== '/') { 16 | fixedFolder += '/'; 17 | } 18 | 19 | if (!silent) { 20 | console.log(`\n ${fixedFolder} `.white.bgRed); 21 | 22 | console.log('\nCHANGES:'.red); 23 | 24 | console.log('- removing reducers'); 25 | console.log('- removing reducer tests'); 26 | console.log('- removing actions'); 27 | console.log('- removing action tests'); 28 | console.log('- removing constants'); 29 | console.log('- removing selectors'); 30 | console.log('- removing selectors tests'); 31 | console.log('- removing index'); 32 | console.log('- removing saga'); 33 | } 34 | 35 | removeSuitFromFile(`${fixedFolder}/reducer.js`); 36 | 37 | removeSuitFromFile(`${fixedFolder}/tests/reducer.test.js`); 38 | 39 | removeSuitFromFile(`${fixedFolder}/actions.js`); 40 | 41 | removeSuitFromFile(`${fixedFolder}/tests/actions.test.js`); 42 | 43 | removeSuitFromFile(`${fixedFolder}/constants.js`); 44 | 45 | removeSuitFromFile(`${fixedFolder}/selectors.js`); 46 | 47 | removeSuitFromFile(`${fixedFolder}/tests/selectors.test.js`); 48 | 49 | removeSuitFromFile(`${fixedFolder}/index.js`); 50 | 51 | removeSuitFromFile(`${fixedFolder}/saga.js`); 52 | }; 53 | 54 | module.exports = rm; 55 | -------------------------------------------------------------------------------- /commands/up/checkExtends.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const ajax = require('../ajax'); 3 | const { concat } = require('../../tools/utils'); 4 | 5 | module.exports = ({ arrayOfDomains, schemaBuf, schemaFile, folder }) => { 6 | let extendsFound = false; 7 | arrayOfDomains.forEach(domain => { 8 | if (domain.extends === 'ajax') { 9 | let searchTerm = concat([ 10 | `,`, 11 | ` "${domain.domainName}": {`, 12 | ` "extends": "ajax"`, 13 | ` }`, 14 | ]); 15 | let index = schemaBuf.indexOf(searchTerm); 16 | if (index === -1) { 17 | searchTerm = concat([ 18 | ` "${domain.domainName}": {`, 19 | ` "extends": "ajax"`, 20 | ` }`, 21 | ]); 22 | index = schemaBuf.indexOf(searchTerm); 23 | } 24 | if (index !== -1) { 25 | extendsFound = true; 26 | fs.writeFileSync( 27 | schemaFile, 28 | schemaBuf.slice(0, index) + 29 | schemaBuf.slice(index + searchTerm.length), 30 | ); 31 | ajax(folder, domain.domainName, schemaFile); 32 | } 33 | } 34 | }); 35 | return extendsFound; 36 | }; 37 | -------------------------------------------------------------------------------- /commands/up/checkForChanges.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * Checks if there has been any changes between this suit file 6 | * and a previous version. 7 | * 8 | * Returns a boolean saying whether suit up should continue 9 | */ 10 | module.exports = ({ dotSuitFolder, force, quiet, schemaBuf }) => { 11 | const messages = []; 12 | if ( 13 | fs.existsSync(path.resolve(`./.suit/${dotSuitFolder}/suit.old.json`)) && 14 | !force 15 | ) { 16 | if ( 17 | fs 18 | .readFileSync(path.resolve(`./.suit/${dotSuitFolder}/suit.old.json`)) 19 | .toString() === schemaBuf 20 | ) { 21 | if (!quiet) { 22 | messages.push( 23 | `\nNO CHANGES:`.green + 24 | ` No changes found in suit file from previous version. Not editing files.`, 25 | ); 26 | } 27 | return { shouldContinue: false, messages }; 28 | } 29 | } 30 | return { shouldContinue: true, messages }; 31 | }; 32 | -------------------------------------------------------------------------------- /commands/up/checkForConfigFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { transforms } = require('../../tools/utils'); 4 | 5 | module.exports = () => { 6 | const { found, file } = transforms({ found: false, file: null }, [ 7 | ...[path.resolve('./.suitrc.json'), path.resolve('./.suitrc')].map( 8 | fileName => () => { 9 | const wasFound = fs.existsSync(fileName); 10 | return { found: wasFound, file: wasFound ? fileName : null }; 11 | }, 12 | ), 13 | ]); 14 | if (!found) { 15 | return {}; 16 | } 17 | return JSON.parse(fs.readFileSync(file)); 18 | }; 19 | -------------------------------------------------------------------------------- /commands/up/checkForDebrisInNameOnly.js: -------------------------------------------------------------------------------- 1 | const { indexesOf } = require('../../tools/utils'); 2 | 3 | /** Check if any debris in buffer */ 4 | 5 | module.exports = ({ buffer, searchTerms = [], domains, trimFunction }) => 6 | indexesOf('// @suit-name-only-start', buffer) 7 | .map(startIndex => ({ 8 | startIndex, 9 | endIndex: buffer.indexOf('// @suit-name-only-end', startIndex), 10 | })) 11 | .map(sliceObject => ({ 12 | ...sliceObject, 13 | slice: buffer.slice(sliceObject.startIndex, sliceObject.endIndex), 14 | })) 15 | .map(({ slice }) => { 16 | const lines = slice.split('\n'); 17 | const arrayOfDomainNames = domains.map(({ domainName }) => domainName); 18 | /** Returns only the reducers that are in the */ 19 | const reducers = lines.filter( 20 | line => 21 | searchTerms.every(term => line.includes(term)) && 22 | arrayOfDomainNames.filter(name => line.includes(name)).length === 0, 23 | ); 24 | return reducers.map(trimFunction); 25 | }) 26 | // Weeds out any empty errors 27 | .filter(input => input.length > 0); 28 | -------------------------------------------------------------------------------- /commands/up/detectDiff.js: -------------------------------------------------------------------------------- 1 | const diff = require('deep-diff'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Cases = require('../../tools/cases'); 5 | const { parseCamelCaseToArray } = require('../../tools/utils'); 6 | 7 | /** 8 | * Does a deep diff, comparing the schema objects of the old 9 | * and new suit.json 10 | */ 11 | module.exports = ({ dotSuitFolder, schemaBuf }) => { 12 | if (fs.existsSync(path.resolve(`./.suit/${dotSuitFolder}/suit.old.json`))) { 13 | const oldSchemaBuf = fs 14 | .readFileSync(path.resolve(`./.suit/${dotSuitFolder}/suit.old.json`)) 15 | .toString(); 16 | 17 | const differences = 18 | diff(JSON.parse(oldSchemaBuf), JSON.parse(schemaBuf)) || []; 19 | return [ 20 | ...differences 21 | .filter(({ kind }) => kind === 'D' || kind === 'N') 22 | .map(({ path: oldPath }, index) => { 23 | if (!differences[index + 1]) return null; 24 | const newPath = differences[index + 1].path; 25 | return JSON.stringify(oldPath.slice(0, oldPath.length - 1)) === 26 | JSON.stringify(newPath.slice(0, newPath.length - 1)) 27 | ? { 28 | removed: oldPath, 29 | added: newPath, 30 | removedCases: new Cases( 31 | parseCamelCaseToArray(oldPath[oldPath.length - 1]), 32 | ).all(), 33 | addedCases: new Cases( 34 | parseCamelCaseToArray(newPath[newPath.length - 1]), 35 | ).all(), 36 | } 37 | : null; 38 | }) 39 | .filter(n => n !== null), 40 | // Saga changes in actions 41 | ...differences 42 | .filter( 43 | ({ path: diffPath, lhs, rhs }) => 44 | diffPath.includes('saga') && lhs && rhs, 45 | ) 46 | .map(({ lhs, rhs, path: diffPath }) => ({ 47 | removed: diffPath, 48 | removedCases: new Cases(parseCamelCaseToArray(`${lhs}`)).all(), 49 | addedCases: new Cases(parseCamelCaseToArray(`${rhs}`)).all(), 50 | })), 51 | ]; 52 | } 53 | return []; 54 | }; 55 | -------------------------------------------------------------------------------- /commands/up/index.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); // eslint-disable-line 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const checkExtends = require('./checkExtends'); 5 | const printError = require('../../tools/printError'); 6 | const printWarning = require('../../tools/printWarning'); 7 | const printMessages = require('../../tools/printMessages'); 8 | const runPrettier = require('./runPrettier'); 9 | const writeAllFiles = require('./writeAllFiles'); 10 | const { concat, capitalize, fixFolderName } = require('../../tools/utils'); 11 | const checkForConfigFile = require('./checkForConfigFile'); 12 | 13 | const composeSchema = ({ schema, folder }) => { 14 | let newSchema = schema; 15 | /** Look for a compose object on the schema */ 16 | if (schema.compose && schema.compose.length) { 17 | /** For each of the 'compose' array */ 18 | schema.compose.forEach(c => { 19 | const file = `${c}`.includes('.json') ? c : `${c}.json`; 20 | if (fs.existsSync(path.resolve(folder, file))) { 21 | const buf = fs.readFileSync(path.resolve(folder, file)).toString(); 22 | const otherSchema = JSON.parse(buf); 23 | newSchema = { 24 | ...schema, 25 | ...otherSchema, 26 | }; 27 | } 28 | }); 29 | } 30 | return newSchema; 31 | }; 32 | 33 | const up = (schemaFile, { quiet = false, force = false } = {}, watcher) => { 34 | // If no .suit folder exists at the root, create one 35 | if (!fs.existsSync(path.resolve('./.suit'))) { 36 | fs.mkdirSync(path.resolve('./.suit')); 37 | } 38 | 39 | /** If this is not a suit.json file, return */ 40 | if (!schemaFile.includes('suit.json')) return; 41 | fs.readFile(schemaFile, (err, s) => { 42 | const errors = []; 43 | const schemaBuf = s.toString(); 44 | /** Gives us the folder where the schema file lives */ 45 | const folder = schemaFile 46 | .slice(0, -9) 47 | // This replaces all backslashes with forward slashes on Windows 48 | .replace(/\\/g, '/'); 49 | const dotSuitFolder = folder.replace(/\//g, '-'); 50 | 51 | let schema; 52 | try { 53 | schema = JSON.parse(schemaBuf.toString()); 54 | } catch (e) { 55 | console.log('Error: ', e); 56 | return; 57 | } 58 | 59 | let extendsFound = checkExtends({ 60 | arrayOfDomains: Object.keys(schema).map(key => ({ 61 | ...schema[key], 62 | domainName: key, 63 | })), 64 | folder, 65 | schemaFile, 66 | schemaBuf, 67 | }); 68 | 69 | if (extendsFound) return; 70 | 71 | /** Look for a compose object on the schema */ 72 | if (schema.compose && schema.compose.length) { 73 | /** For each of the 'compose' array */ 74 | schema.compose.forEach(c => { 75 | const file = `${c}`.includes('.json') ? c : `${c}.json`; 76 | if (fs.existsSync(path.resolve(`${folder}/${file}`))) { 77 | const buf = fs 78 | .readFileSync(path.resolve(`${folder}/${file}`)) 79 | .toString(); 80 | const otherSchema = JSON.parse(buf); 81 | schema = { 82 | ...schema, 83 | ...otherSchema, 84 | }; 85 | 86 | extendsFound = 87 | extendsFound || 88 | checkExtends({ 89 | arrayOfDomains: Object.keys(otherSchema).map(key => ({ 90 | ...otherSchema[key], 91 | domainName: key, 92 | })), 93 | folder, 94 | schemaFile: path.resolve(`${folder}/${file}`), 95 | schemaBuf: buf, 96 | }); 97 | // Adds it to the watcher 98 | const allWatchedPaths = Object.values(watcher.watched()).reduce( 99 | (a, b) => [...a, ...b], 100 | [], 101 | ); 102 | if (!allWatchedPaths.includes(path.resolve(`${folder}${file}`))) { 103 | watcher.add(path.resolve(`${folder}${file}`)); 104 | } 105 | } else { 106 | errors.push(concat([`Could not find suit.json file ` + `${c}`.cyan])); 107 | } 108 | }); 109 | } 110 | 111 | if (extendsFound) return; 112 | 113 | let imports = []; 114 | 115 | if (schema.import) { 116 | Object.keys(schema.import).forEach(key => { 117 | const importPath = path.resolve(folder, key, './suit.json'); 118 | if (!fs.existsSync(importPath)) { 119 | errors.push(`Could not find imported file: ` + `"${key}"`.cyan); 120 | return; 121 | } 122 | const importedSchema = composeSchema({ 123 | folder: path.resolve(folder, key), 124 | schema: JSON.parse(fs.readFileSync(importPath).toString()), 125 | }); 126 | 127 | Object.keys(schema.import[key]).forEach(domain => { 128 | if (typeof importedSchema[domain] === 'undefined') { 129 | errors.push( 130 | `Could not find reducer` + 131 | ` ${domain} `.cyan + 132 | `in ` + 133 | `${key}`.cyan, 134 | ); 135 | } 136 | }); 137 | 138 | if (errors.length) return; 139 | 140 | imports = [ 141 | ...imports, 142 | ...Object.keys(schema.import[key]) 143 | .map(domain => { 144 | const domainObject = schema.import[key][domain]; 145 | const hasSelectors = 146 | typeof domainObject.selectors !== 'undefined'; 147 | const selectors = !hasSelectors 148 | ? [] 149 | : typeof domainObject.selectors === 'string' 150 | ? [domainObject.selectors] 151 | : domainObject.selectors; 152 | const hasActions = typeof domainObject.actions !== 'undefined'; 153 | const actions = !hasActions 154 | ? [] 155 | : typeof domainObject.actions === 'string' 156 | ? [domainObject.actions] 157 | : domainObject.actions; 158 | return [ 159 | ...selectors.map(selector => { 160 | const importedState = importedSchema[domain].initialState; 161 | if (typeof importedState[selector] === 'undefined') { 162 | errors.push( 163 | `Import failed: ` + 164 | `${selector}`.cyan + 165 | ` not found in the initialState of ` + 166 | `${domain} `.cyan + 167 | `in ` + 168 | `"${key}suit.json"`.cyan, 169 | ); 170 | } 171 | return { 172 | value: `${domain}${capitalize(selector)}`, 173 | property: `makeSelect${capitalize(domain)}${capitalize( 174 | selector, 175 | )}`, 176 | selector: `makeSelect${capitalize(domain)}${capitalize( 177 | selector, 178 | )}`, 179 | path: key + key[key.length - 1] === '/' ? '' : '/', 180 | type: 'selector', 181 | initialValue: importedSchema[domain].initialState[selector], 182 | fileName: `${fixFolderName(key)}selectors`, 183 | }; 184 | }), 185 | ...actions.map(action => { 186 | const importedAction = importedSchema[domain].actions[action]; 187 | if (!importedAction) { 188 | errors.push( 189 | `Import failed: ` + 190 | `${action}`.cyan + 191 | ` not found in ` + 192 | `${domain} `.cyan + 193 | `in ` + 194 | `"${key}suit.json"`.cyan, 195 | ); 196 | return {}; 197 | } 198 | return { 199 | property: `${action}`, 200 | action, 201 | describe: importedAction.describe || '', 202 | path: key, 203 | payload: 204 | importedAction.payload || 205 | ( 206 | importedAction.set && 207 | Object.values(importedAction.set).filter(value => 208 | `${value}`.includes('payload'), 209 | ) 210 | ).length, 211 | type: 'action', 212 | fileName: `${fixFolderName(key)}actions`, 213 | }; 214 | }), 215 | ]; 216 | }) 217 | .reduce((a, b) => [...a, ...b], []), 218 | ]; 219 | }); 220 | } 221 | 222 | if (errors.length) { 223 | console.log(`\n ${folder}suit.json `.white.bgRed); 224 | printError(errors); 225 | return; 226 | } 227 | 228 | const newSchemaBuf = JSON.stringify(schema, null, 2); 229 | 230 | const { 231 | newReducerBuffer, 232 | newReducerTestBuffer, 233 | newActionsBuffer, 234 | newSelectorsBuffer, 235 | newActionTestsBuffer, 236 | newConstantsBuffer, 237 | newSelectorsTestsBuffer, 238 | newIndexBuffer, 239 | saga, 240 | shouldContinue, 241 | errors: newErrors, 242 | warnings, 243 | // messages, 244 | } = writeAllFiles({ 245 | schema, 246 | imports, 247 | errors, 248 | folder, 249 | newSchemaBuf, 250 | config: checkForConfigFile(), 251 | dotSuitFolder, 252 | quiet, 253 | force, 254 | buffers: { 255 | reducer: fs 256 | .readFileSync(path.resolve(`${folder}/reducer.js`)) 257 | .toString(), 258 | actions: fs 259 | .readFileSync(path.resolve(`${folder}/actions.js`)) 260 | .toString(), 261 | constants: fs 262 | .readFileSync(path.resolve(`${folder}/constants.js`)) 263 | .toString(), 264 | selectors: fs 265 | .readFileSync(path.resolve(`${folder}/selectors.js`)) 266 | .toString(), 267 | index: fs.readFileSync(path.resolve(`${folder}/index.js`)).toString(), 268 | saga: fs.readFileSync(path.resolve(`${folder}/saga.js`)).toString(), 269 | reducerTests: fs.existsSync( 270 | path.resolve(`${folder}/tests/reducer.test.js`), 271 | ) 272 | ? fs 273 | .readFileSync(path.resolve(`${folder}/tests/reducer.test.js`)) 274 | .toString() 275 | : '', 276 | actionTests: fs.existsSync( 277 | path.resolve(`${folder}/tests/actions.test.js`), 278 | ) 279 | ? fs 280 | .readFileSync(path.resolve(`${folder}/tests/actions.test.js`)) 281 | .toString() 282 | : '', 283 | selectorsTests: fs.existsSync( 284 | path.resolve(`${folder}/tests/selectors.test.js`), 285 | ) 286 | ? fs 287 | .readFileSync(path.resolve(`${folder}/tests/selectors.test.js`)) 288 | .toString() 289 | : '', 290 | }, 291 | }); 292 | 293 | if (newErrors.length) { 294 | console.log(`\n ${folder}suit.json `.white.bgRed); 295 | printError(newErrors); 296 | return; 297 | } 298 | 299 | if (warnings.length) { 300 | console.log(`\n ${folder}suit.json `.bgYellow.black); 301 | printWarning(warnings); 302 | } 303 | 304 | if (!shouldContinue) { 305 | return; 306 | } 307 | 308 | if (!newErrors.length && !warnings.length) { 309 | console.log(`\n ${folder}suit.json `.bgGreen.black); 310 | } 311 | 312 | printMessages([ 313 | ...saga.messages, 314 | '\nCHANGES:'.green, 315 | 'writing reducers, reducer tests, actions, action tests, constants, selectors, selectors tests, index, saga, saving old suit file in .suit directory', 316 | ]); 317 | fs.writeFileSync(path.resolve(`${folder}/reducer.js`), newReducerBuffer); 318 | fs.writeFileSync( 319 | path.resolve(`${folder}/tests/reducer.test.js`), 320 | newReducerTestBuffer, 321 | ); 322 | fs.writeFileSync(path.resolve(`${folder}/actions.js`), newActionsBuffer); 323 | fs.writeFileSync( 324 | path.resolve(`${folder}/tests/actions.test.js`), 325 | newActionTestsBuffer, 326 | ); 327 | fs.writeFileSync( 328 | path.resolve(`${folder}/constants.js`), 329 | newConstantsBuffer, 330 | ); 331 | fs.writeFileSync( 332 | path.resolve(`${folder}/selectors.js`), 333 | newSelectorsBuffer, 334 | ); 335 | fs.writeFileSync( 336 | path.resolve(`${folder}/tests/selectors.test.js`), 337 | newSelectorsTestsBuffer, 338 | ); 339 | fs.writeFileSync(path.resolve(`${folder}/index.js`), newIndexBuffer); 340 | fs.writeFileSync(path.resolve(`${folder}/saga.js`), saga.buffer); 341 | if (!fs.existsSync(path.resolve(`./.suit/${dotSuitFolder}`))) { 342 | fs.mkdirSync(path.resolve(`./.suit/${dotSuitFolder}`)); 343 | } 344 | fs.writeFileSync( 345 | path.resolve(`./.suit/${dotSuitFolder}/suit.old.json`), 346 | newSchemaBuf, 347 | ); 348 | 349 | /** Runs prettier and checks for prettier warnings */ 350 | const prettierWarnings = runPrettier(folder); 351 | 352 | printWarning(prettierWarnings); 353 | }); 354 | }; 355 | 356 | module.exports = up; 357 | -------------------------------------------------------------------------------- /commands/up/runPrettier.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { exec } = require('child_process'); 4 | const { concat } = require('../../tools/utils'); 5 | 6 | module.exports = folder => { 7 | const prettierErrors = []; 8 | if (fs.existsSync(path.resolve('./.prettierrc'))) { 9 | try { 10 | exec(`prettier --config ./.prettierrc --write "${folder}/**/*.js"`); 11 | console.log( 12 | `\nPRETTIER: `.green + 13 | `Running prettier on this folder from the root config.`, 14 | ); 15 | } catch (e) { 16 | console.log( 17 | concat([ 18 | 'No version of prettier found. This will make your files uglier.', 19 | `- If you're running suit from npm scripts, run npm i prettier`, 20 | `- If you installed suit by typing npm i -g boilersuit, run npm i -g prettier`, 21 | ]), 22 | ); 23 | } 24 | } else { 25 | prettierErrors.push( 26 | concat([ 27 | `I see you're not using prettier!`, 28 | `- Try adding a .prettierrc to your root directory and suit will make things prettier :)`, 29 | ]), 30 | ); 31 | } 32 | return prettierErrors; 33 | }; 34 | -------------------------------------------------------------------------------- /commands/up/writeActionTests/index.js: -------------------------------------------------------------------------------- 1 | const writeActionTests = require('./writeActionTests'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | parseCamelCaseToArray, 5 | cleanFile, 6 | fixInlineImports, 7 | transforms, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ buffer, arrayOfDomains }) => 11 | transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, initialState, actions }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)); 16 | const allDomainCases = cases.all(); 17 | 18 | const arrayOfActions = Object.keys(actions).map(key => ({ 19 | ...actions[key], 20 | name: key, 21 | cases: new Cases(parseCamelCaseToArray(key)).all(), 22 | })); 23 | 24 | return writeActionTests({ 25 | buffer: b, 26 | domainCases: allDomainCases, 27 | arrayOfActions, 28 | initialState, 29 | }); 30 | }), 31 | ]); 32 | -------------------------------------------------------------------------------- /commands/up/writeActionTests/writeActionTests.js: -------------------------------------------------------------------------------- 1 | const { 2 | transforms, 3 | prettify, 4 | concat, 5 | ensureImport, 6 | } = require('../../../tools/utils'); 7 | 8 | module.exports = ({ buffer, arrayOfActions }) => 9 | transforms(buffer, [ 10 | ...arrayOfActions.map(action => buf => 11 | transforms(buf, [ 12 | ensureImport(action.name, '../actions', { destructure: true }), 13 | ensureImport(action.cases.constant, '../constants', { 14 | destructure: true, 15 | }), 16 | ]), 17 | ), 18 | ...arrayOfActions.map(action => buf => 19 | concat([ 20 | buf, 21 | `// @suit-start`, 22 | `describe('${action.cases.camel}', () => {`, 23 | ` it('should have the correct type', () => {`, 24 | ` expect(${action.cases.camel}().type).toEqual(${ 25 | action.cases.constant 26 | });`, 27 | ` });`, 28 | action.set && 29 | Object.values(action.set).filter(val => `${val}`.includes('payload')) 30 | .length > 0 31 | ? concat([ 32 | ` it('should return the correct payload', () => {`, 33 | ` expect(${ 34 | action.cases.camel 35 | }('dummyPayload').payload).toEqual('dummyPayload');`, 36 | ` });`, 37 | ]) 38 | : null, 39 | `});`, 40 | `// @suit-end`, 41 | ``, 42 | ]), 43 | ), 44 | prettify, 45 | ]); 46 | -------------------------------------------------------------------------------- /commands/up/writeActions/index.js: -------------------------------------------------------------------------------- 1 | const writeActions = require('./writeActions'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | transforms, 5 | cleanFile, 6 | fixInlineImports, 7 | parseCamelCaseToArray, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ arrayOfDomains, buffer }) => { 11 | const newActionsBuffer = transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, actions }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)); 16 | const allDomainCases = cases.all(); 17 | 18 | return writeActions({ 19 | buffer: b, 20 | cases: allDomainCases, 21 | actions, 22 | }); 23 | }), 24 | ]); 25 | return { buffer: newActionsBuffer }; 26 | }; 27 | -------------------------------------------------------------------------------- /commands/up/writeActions/writeActions.js: -------------------------------------------------------------------------------- 1 | const { concat, transforms, prettify } = require('../../../tools/utils'); 2 | const writeOneAction = require('./writeOneAction'); 3 | 4 | module.exports = ({ buffer, cases, actions }) => 5 | transforms(buffer, [ 6 | /** Adds the domain */ 7 | b => concat([b, `// @suit-start`, ``, `/** ${cases.display} actions */`]), 8 | /** Adds each action */ 9 | ...Object.keys(actions) 10 | .map(key => ({ name: key, ...actions[key] })) 11 | .reverse() 12 | .map(writeOneAction(cases)), 13 | prettify, 14 | ]); 15 | -------------------------------------------------------------------------------- /commands/up/writeActions/writeOneAction.js: -------------------------------------------------------------------------------- 1 | const Cases = require('../../../tools/cases'); 2 | const { 3 | concat, 4 | parseCamelCaseToArray, 5 | ensureImport, 6 | } = require('../../../tools/utils'); 7 | 8 | /** 9 | * Writes one action to the file 10 | * @param {object} domainCases 11 | */ 12 | module.exports = domainCases => ( 13 | { name, set, payload: payloadAttribute, describe }, 14 | i, 15 | ) => buffer => { 16 | const c = new Cases(parseCamelCaseToArray(name)); 17 | const actionCases = c.all(); 18 | /** Ensures the imports of the constants */ 19 | const newBuffer = ensureImport(actionCases.constant, './constants', { 20 | destructure: true, 21 | })(buffer); 22 | 23 | const searchTerm = `/** ${domainCases.display} actions */`; 24 | const index = newBuffer.indexOf(searchTerm) + searchTerm.length; 25 | 26 | let content = ''; 27 | const hasPayload = 28 | (set && 29 | Object.values(set).filter(value => `${value}`.includes('payload')) 30 | .length > 0) || 31 | payloadAttribute; 32 | content += concat([ 33 | ``, 34 | describe ? `// ${describe}` : null, 35 | `export const ${actionCases.camel} = (${ 36 | hasPayload ? 'payload' : '' 37 | }) => ({`, 38 | ` type: ${actionCases.constant},`, 39 | hasPayload ? ` payload,` : null, 40 | `});`, 41 | ]); 42 | if (i === 0) { 43 | content += '\n\n// @suit-end'; 44 | } 45 | 46 | return concat([newBuffer.slice(0, index), content, newBuffer.slice(index)]); 47 | }; 48 | -------------------------------------------------------------------------------- /commands/up/writeAllFiles.js: -------------------------------------------------------------------------------- 1 | const writeIndex = require('./writeIndex'); 2 | const writeSelectors = require('./writeSelectors'); 3 | const writeActions = require('./writeActions'); 4 | const writeConstants = require('./writeConstants'); 5 | const writeReducer = require('./writeReducer'); 6 | const writeSaga = require('./writeSaga'); 7 | const writeReducerTests = require('./writeReducerTests'); 8 | const writeActionTests = require('./writeActionTests'); 9 | const writeSelectorTests = require('./writeSelectorTests'); 10 | const checkIfBadBuffer = require('../../tools/checkIfBadBuffer'); 11 | const checkErrorsInSchema = require('../../tools/checkErrorsInSchema'); 12 | const checkWarningsInSchema = require('../../tools/checkWarningsInSchema'); 13 | const detectDiff = require('./detectDiff'); 14 | const reservedKeywords = require('../../tools/constants/reservedKeywords'); 15 | const checkForChanges = require('./checkForChanges'); 16 | const checkForDebrisInNameOnly = require('./checkForDebrisInNameOnly'); 17 | const { concat } = require('../../tools/utils'); 18 | 19 | module.exports = ({ 20 | schema, 21 | errors: passedErrors = [], 22 | folder, 23 | dotSuitFolder, 24 | buffers, 25 | config = {}, 26 | newSchemaBuf, 27 | imports, 28 | quiet, 29 | force, 30 | }) => { 31 | let errors = passedErrors; 32 | const arrayOfDomains = Object.keys(schema) 33 | .filter(key => !reservedKeywords.includes(key)) 34 | .map(key => ({ 35 | ...schema[key], 36 | domainName: key, 37 | })); 38 | 39 | errors = [ 40 | ...errors, 41 | ...checkErrorsInSchema(schema, folder), 42 | ...checkIfBadBuffer(buffers), 43 | ]; 44 | 45 | if (errors.length) { 46 | return { errors, shouldContinue: false }; 47 | } 48 | 49 | let warnings = checkWarningsInSchema(schema, config); 50 | 51 | /** Check for a previous suit file in folder - force prevents this check */ 52 | const { 53 | shouldContinue: anyChanges, 54 | messages: changeMessages, 55 | } = checkForChanges({ 56 | dotSuitFolder, 57 | quiet, 58 | force, 59 | schemaBuf: newSchemaBuf, 60 | }); 61 | if (!anyChanges) { 62 | return { 63 | shouldContinue: false, 64 | messages: changeMessages, 65 | warnings, 66 | errors, 67 | }; 68 | } 69 | 70 | /** Get a more detailed diff of the changes */ 71 | const keyChanges = detectDiff({ 72 | dotSuitFolder, 73 | schemaBuf: newSchemaBuf, 74 | }); 75 | 76 | /** Write reducer */ 77 | const { buffer: newReducerBuffer, errors: domainErrors } = writeReducer({ 78 | folder, 79 | arrayOfDomains, 80 | keyChanges, 81 | buffer: buffers.reducer, 82 | }); 83 | 84 | if (domainErrors.length) { 85 | return { errors: domainErrors, warnings, shouldContinue: false }; 86 | } 87 | 88 | warnings = [ 89 | ...warnings, 90 | ...checkForDebrisInNameOnly({ 91 | buffer: newReducerBuffer, 92 | searchTerms: ['export const', 'Reducer'], 93 | trimFunction: line => 94 | line 95 | .split(' ') 96 | .find(word => word.includes('Reducer')) 97 | .replace('Reducer', ''), 98 | domains: arrayOfDomains, 99 | }).map(domain => 100 | concat([ 101 | `Useless code in reducers file`, 102 | `Found:`, 103 | domain.map(dom => ` - ` + `${dom}`.cyan + ` Reducer`), 104 | `Remove this, otherwise you'll get errors`, 105 | ]), 106 | ), 107 | ]; 108 | 109 | /** Write Actions */ 110 | const { buffer: newActionsBuffer } = writeActions({ 111 | buffer: buffers.actions, 112 | arrayOfDomains, 113 | }); 114 | 115 | /** Write Constants */ 116 | const { buffer: newConstantsBuffer } = writeConstants({ 117 | folder, 118 | arrayOfDomains, 119 | buffer: buffers.constants, 120 | }); 121 | 122 | /** Write Selectors */ 123 | const newSelectorsBuffer = writeSelectors({ 124 | buffer: buffers.selectors, 125 | folder, 126 | arrayOfDomains, 127 | }); 128 | 129 | /** Write Index */ 130 | const newIndexBuffer = writeIndex({ 131 | indexBuffer: buffers.index, 132 | arrayOfDomains, 133 | keyChanges, 134 | imports, 135 | }); 136 | 137 | /** Write Saga */ 138 | const saga = writeSaga({ 139 | sagaBuffer: buffers.saga, 140 | arrayOfDomains, 141 | keyChanges, 142 | }); 143 | if (saga.errors.length) { 144 | return { errors, shouldContinue: false }; 145 | } 146 | 147 | warnings = [ 148 | ...warnings, 149 | ...checkForDebrisInNameOnly({ 150 | buffer: saga.buffer, 151 | searchTerms: ['function*'], 152 | domains: arrayOfDomains, 153 | // This trims the line from 'function* getNotes({ })'... to 'getNotes' 154 | trimFunction: line => 155 | line 156 | .replace('function*', '') 157 | .replace(' ', '') 158 | .split('(')[0], 159 | }).map(domain => 160 | concat([ 161 | `Useless code in saga file`, 162 | `Found:`, 163 | domain.map(dom => ` - ` + `${dom}`.cyan + ` Saga`), 164 | `Remove this, otherwise you'll get errors`, 165 | ]), 166 | ), 167 | ]; 168 | 169 | /** Write reducer tests */ 170 | const newReducerTestBuffer = writeReducerTests({ 171 | buffer: buffers.reducerTests, 172 | arrayOfDomains, 173 | }); 174 | 175 | /** Write actions tests */ 176 | const newActionTestsBuffer = writeActionTests({ 177 | buffer: buffers.actionTests, 178 | arrayOfDomains, 179 | }); 180 | 181 | /** Write selectors tests */ 182 | const newSelectorsTestsBuffer = writeSelectorTests({ 183 | buffer: buffers.selectorsTests, 184 | arrayOfDomains, 185 | folder, 186 | }); 187 | 188 | return { 189 | shouldContinue: true, 190 | saga, 191 | newReducerBuffer, 192 | newReducerTestBuffer, 193 | newSelectorsTestsBuffer, 194 | newActionTestsBuffer, 195 | newIndexBuffer, 196 | newSelectorsBuffer, 197 | newConstantsBuffer, 198 | newActionsBuffer, 199 | warnings, 200 | errors, 201 | messages: [], 202 | }; 203 | }; 204 | -------------------------------------------------------------------------------- /commands/up/writeConstants/index.js: -------------------------------------------------------------------------------- 1 | const writeConstants = require('./writeConstants'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | transforms, 5 | cleanFile, 6 | fixInlineImports, 7 | parseCamelCaseToArray, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ folder, arrayOfDomains, buffer }) => { 11 | const newConstantsBuffer = transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, actions }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)); 16 | const allDomainCases = cases.all(); 17 | 18 | return writeConstants({ 19 | buffer: b, 20 | cases: allDomainCases, 21 | actions, 22 | folder, 23 | }); 24 | }), 25 | ]); 26 | return { buffer: newConstantsBuffer }; 27 | }; 28 | -------------------------------------------------------------------------------- /commands/up/writeConstants/writeConstants.js: -------------------------------------------------------------------------------- 1 | const { concat, transforms, prettify } = require('../../../tools/utils'); 2 | const writeOneConstant = require('./writeOneConstant'); 3 | 4 | module.exports = ({ buffer, cases, actions, folder }) => 5 | transforms(buffer, [ 6 | /** Adds the domain */ 7 | b => concat([b, `// @suit-start`, ``, `/** ${cases.display} constants */`]), 8 | /** Adds each action */ 9 | ...Object.keys(actions) 10 | .map(key => ({ name: key, ...actions[key] })) 11 | .reverse() 12 | .map(writeOneConstant({ cases, folder })), 13 | prettify, 14 | ]); 15 | -------------------------------------------------------------------------------- /commands/up/writeConstants/writeOneConstant.js: -------------------------------------------------------------------------------- 1 | const Cases = require('../../../tools/cases'); 2 | const { concat, parseCamelCaseToArray } = require('../../../tools/utils'); 3 | 4 | /** Writes one constant into the file */ 5 | module.exports = ({ cases, folder }) => ({ name }, i) => b => { 6 | const c = new Cases(parseCamelCaseToArray(name)); 7 | const actionCases = c.all(); 8 | 9 | const searchTerm = `/** ${cases.display} constants */`; 10 | const index = b.indexOf(searchTerm) + searchTerm.length; 11 | 12 | let content = ''; 13 | content += concat([ 14 | ``, 15 | `export const ${actionCases.constant} =`, 16 | ` '${folder}${actionCases.constant}';`, 17 | ]); 18 | if (i === 0) { 19 | content += '\n\n// @suit-end'; 20 | } 21 | 22 | return concat([b.slice(0, index), content, b.slice(index)]); 23 | }; 24 | -------------------------------------------------------------------------------- /commands/up/writeIndex/index.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); // eslint-disable-line 2 | const writeIndex = require('./writeIndex'); 3 | const writeImportsToIndex = require('./writeImportsToIndex'); 4 | const Cases = require('../../../tools/cases'); 5 | const { 6 | parseCamelCaseToArray, 7 | cleanFile, 8 | fixInlineImports, 9 | transforms, 10 | } = require('../../../tools/utils'); 11 | 12 | module.exports = ({ indexBuffer, arrayOfDomains, keyChanges = [], imports }) => 13 | transforms(indexBuffer, [ 14 | cleanFile, 15 | fixInlineImports, 16 | ...arrayOfDomains 17 | .filter(({ mapToContainer }) => mapToContainer !== false) 18 | .map(({ domainName, actions, initialState }) => b => { 19 | const cases = new Cases(parseCamelCaseToArray(domainName)); 20 | const allDomainCases = cases.all(); 21 | 22 | return writeIndex({ 23 | buffer: b, 24 | cases: allDomainCases, 25 | initialState, 26 | keyChanges, 27 | actions, 28 | }); 29 | }), 30 | b => 31 | writeImportsToIndex({ 32 | imports, 33 | buffer: b, 34 | }), 35 | ]); 36 | -------------------------------------------------------------------------------- /commands/up/writeIndex/writeImportsToIndex.js: -------------------------------------------------------------------------------- 1 | const { 2 | concat, 3 | transforms, 4 | prettify, 5 | ensureImport, 6 | capitalize, 7 | } = require('../../../tools/utils'); 8 | 9 | module.exports = ({ buffer, imports }) => 10 | transforms(buffer, [ 11 | b => 12 | transforms(b, [ 13 | ...imports.map(({ property, fileName }) => 14 | ensureImport(property, fileName, { 15 | destructure: true, 16 | }), 17 | ), 18 | ]), 19 | /** Get Selectors Into mapStateToProps */ 20 | b => { 21 | const searchTerm = 'mapStateToProps = createStructuredSelector({\n'; 22 | const index = b.indexOf(searchTerm) + searchTerm.length; 23 | return ( 24 | b.slice(0, index) + 25 | concat([ 26 | ' // @suit-start', 27 | ...imports 28 | .filter(({ type }) => type === 'selector') 29 | .filter( 30 | ({ selector, value }) => 31 | b.indexOf(`${value}: ${selector}(),`) === -1, 32 | ) 33 | .map(({ selector, value }) => 34 | concat([` ${value}: ${selector}(),`]), 35 | ), 36 | ' // @suit-end', 37 | b.slice(index), 38 | ]) 39 | ); 40 | }, 41 | b => b.replace('\t', ' '), 42 | /** Get actions into mapDispatchToProps */ 43 | b => { 44 | const index = 45 | b.indexOf('return {\n', b.indexOf('mapDispatchToProps')) + 46 | 'return {\n'.length; 47 | if (index === -1 + 'return {\n'.length) { 48 | return b; 49 | } 50 | return ( 51 | b.slice(0, index) + 52 | concat([ 53 | ' // @suit-start', 54 | ...imports 55 | .filter(({ type }) => type === 'action') 56 | .filter( 57 | ({ action }) => 58 | b.indexOf( 59 | `submit${capitalize(action)}: (`, 60 | b.indexOf('mapDispatchToProps'), 61 | ) === -1, 62 | ) 63 | .map(({ action, describe, payload }) => 64 | concat([ 65 | describe ? ` /** ${describe} */` : null, 66 | ` submit${capitalize(action)}: (${ 67 | payload ? 'payload' : '' 68 | }) => dispatch(${action}(${payload ? 'payload' : ''})),`, 69 | ]), 70 | ), 71 | ' // @suit-end', 72 | b.slice(index), 73 | ]) 74 | ); 75 | }, 76 | /** Adds a suit-name-only area to the PropTypes if it doesn't already exist */ 77 | b => { 78 | const startIndex = b.indexOf('propTypes = {') + 'propTypes = {'.length; 79 | const endIndex = b.indexOf('\n};', startIndex); 80 | // Makes a slice of the proptypes to check if keys already exist 81 | const propTypesSlice = b.slice(startIndex, endIndex); 82 | 83 | const noPreviousPropTypes = 84 | propTypesSlice.indexOf('// @suit-name-only-start') === -1; 85 | 86 | // If we've not put in a // @suit-name-only section 87 | if (noPreviousPropTypes) { 88 | return concat([ 89 | b.slice(0, startIndex), 90 | ` // @suit-name-only-start`, 91 | ` // @suit-name-only-end` + b.slice(startIndex), 92 | ]); 93 | } 94 | return b; 95 | }, 96 | /** Writes any remaining propTypes between the // @suit-name-only tags */ 97 | b => { 98 | const startIndex = b.indexOf('propTypes = {') + 'propTypes = {'.length; 99 | const endIndex = b.indexOf('\n};', startIndex); 100 | const propTypesSlice = b.slice(startIndex, endIndex); 101 | const propTypesToAdd = [ 102 | ...imports 103 | .filter(({ type }) => type === 'selector') 104 | .filter( 105 | ({ value }) => propTypesSlice.indexOf(`${value}: PropTypes`) === -1, 106 | ) 107 | .map( 108 | ({ initialValue, value }) => 109 | ` // ${value}: PropTypes.${propTypeFromTypeOf( 110 | typeof initialValue, 111 | )},`, 112 | ), 113 | ...imports 114 | .filter(({ type }) => type === 'action') 115 | .filter( 116 | ({ property }) => 117 | propTypesSlice.indexOf( 118 | `submit${capitalize(property)}: PropTypes.func,`, 119 | ) === -1, 120 | ) 121 | .map( 122 | ({ property }) => 123 | ` // submit${capitalize(property)}: PropTypes.func,`, 124 | ), 125 | ]; 126 | const arrayOfLines = b.split('\n'); 127 | const indexToInsert = arrayOfLines.findIndex(line => 128 | line.includes('// @suit-name-only-start'), 129 | ); 130 | return [ 131 | ...arrayOfLines.filter((_, index) => index <= indexToInsert), 132 | ...propTypesToAdd, 133 | ...arrayOfLines.filter((_, index) => index > indexToInsert), 134 | ].join('\n'); 135 | }, 136 | prettify, 137 | ]); 138 | 139 | const propTypeFromTypeOf = type => { 140 | switch (type) { 141 | case 'function': 142 | return 'func'; 143 | case 'undefined': 144 | case 'object': 145 | return 'any'; 146 | case 'boolean': 147 | return 'bool'; 148 | default: 149 | return type; 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /commands/up/writeIndex/writeIndex.js: -------------------------------------------------------------------------------- 1 | const Cases = require('../../../tools/cases'); 2 | const { 3 | concat, 4 | transforms, 5 | parseCamelCaseToArray, 6 | prettify, 7 | ensureImport, 8 | capitalize, 9 | } = require('../../../tools/utils'); 10 | 11 | module.exports = ({ buffer, cases, initialState, actions, keyChanges }) => { 12 | /** Checks if the passAsProp keyword is present */ 13 | const hasPropFiltering = Object.values(actions).filter(actionValues => 14 | Object.keys(actionValues).includes('passAsProp'), 15 | ).length; 16 | 17 | return transforms(buffer, [ 18 | /** Import all selectors */ 19 | b => 20 | transforms(b, [ 21 | ...Object.keys(initialState).map(key => { 22 | const c = new Cases(parseCamelCaseToArray(key)); 23 | const fieldCases = c.all(); 24 | return ensureImport( 25 | `makeSelect${cases.pascal}${fieldCases.pascal}`, 26 | './selectors', 27 | { 28 | destructure: true, 29 | }, 30 | ); 31 | }), 32 | ]), 33 | /** Import actions */ 34 | b => 35 | transforms(b, [ 36 | ...Object.keys(actions) 37 | .filter(key => { 38 | /** If no prop filtering, give all props */ 39 | if (!hasPropFiltering) return true; 40 | return actions[key].passAsProp; 41 | }) 42 | .map(key => 43 | ensureImport(key, './actions', { 44 | destructure: true, 45 | }), 46 | ), 47 | ]), 48 | /** Get Selectors Into mapStateToProps */ 49 | b => { 50 | const searchTerm = 'mapStateToProps = createStructuredSelector({\n'; 51 | const index = b.indexOf(searchTerm) + searchTerm.length; 52 | return ( 53 | b.slice(0, index) + 54 | concat([ 55 | ' // @suit-start', 56 | ...Object.keys(initialState).map(key => { 57 | const c = new Cases(parseCamelCaseToArray(key)); 58 | const fieldCases = c.all(); 59 | return ` ${cases.camel}${fieldCases.pascal}: makeSelect${ 60 | cases.pascal 61 | }${fieldCases.pascal}(),`; 62 | }), 63 | ' // @suit-end', 64 | b.slice(index), 65 | ]) 66 | ); 67 | }, 68 | /** Get actions into mapDispatchToProps */ 69 | b => { 70 | const index = 71 | b.indexOf('return {\n', b.indexOf('mapDispatchToProps')) + 72 | 'return {\n'.length; 73 | if (index === -1 + 'return {\n'.length) { 74 | return b; 75 | } 76 | return ( 77 | b.slice(0, index) + 78 | concat([ 79 | ' // @suit-start', 80 | ...Object.keys(actions) 81 | .filter(key => { 82 | /** If no prop filtering, give all props */ 83 | if (!hasPropFiltering) return true; 84 | return actions[key].passAsProp; 85 | }) 86 | .map(key => { 87 | const actionCases = new Cases(parseCamelCaseToArray(key)).all(); 88 | const hasPayload = 89 | (actions[key].set && 90 | Object.values(actions[key].set).filter(value => 91 | `${value}`.includes('payload'), 92 | ).length) || 93 | actions[key].payload; 94 | return concat([ 95 | actions[key].describe 96 | ? ` /** ${actions[key].describe} */` 97 | : null, 98 | ` submit${actionCases.pascal}: (${ 99 | hasPayload ? 'payload' : '' 100 | }) => dispatch(${actionCases.camel}(${ 101 | hasPayload ? 'payload' : '' 102 | })),`, 103 | ]); 104 | }), 105 | ' // @suit-end', 106 | b.slice(index), 107 | ]) 108 | ); 109 | }, 110 | /** Adds a suit-name-only area to the PropTypes if it doesn't already exist */ 111 | b => { 112 | const startIndex = b.indexOf('propTypes = {') + 'propTypes = {'.length; 113 | const endIndex = b.indexOf('\n};', startIndex); 114 | // Makes a slice of the proptypes to check if keys already exist 115 | const propTypesSlice = b.slice(startIndex, endIndex); 116 | 117 | const noPreviousPropTypes = 118 | propTypesSlice.indexOf('// @suit-name-only-start') === -1; 119 | 120 | // If we've not put in a // @suit-name-only section 121 | if (noPreviousPropTypes) { 122 | return concat([ 123 | b.slice(0, startIndex), 124 | ` // @suit-name-only-start`, 125 | ` // @suit-name-only-end` + b.slice(startIndex), 126 | ]); 127 | } 128 | return b; 129 | }, 130 | /** Makes changes to proptypes based on keyChanges */ 131 | b => 132 | transforms(b, [ 133 | // Checks the list of new changes for any relevant ones for PropTypes 134 | ...keyChanges 135 | .filter( 136 | ({ removed }) => 137 | removed.includes(cases.camel) && 138 | // If key is in initialstate, or is the name of an action, include it 139 | (removed.includes('initialState') || 140 | removed[removed.length - 2] === 'actions'), 141 | ) 142 | // There are both actions and initialState bits in this area 143 | .map(({ removed, removedCases, addedCases }) => buf => { 144 | const startOfNameOnly = 145 | buf.indexOf('// @suit-name-only-start\n') + 146 | '// @suit-name-only-start\n'.length; 147 | const endOfNameOnly = buf.indexOf( 148 | '// @suit-name-only-end', 149 | startOfNameOnly, 150 | ); 151 | const nameOnlySlice = buf.slice(startOfNameOnly, endOfNameOnly); 152 | // If is a state field, and is in the name only section 153 | const prefix = removed.includes('initialState') 154 | ? cases.camel 155 | : 'submit'; 156 | // If the propType is in the nameOnlySlice, change it 157 | if ( 158 | nameOnlySlice.indexOf(`${prefix}${removedCases.pascal}` !== -1) 159 | ) { 160 | return ( 161 | buf.slice(0, startOfNameOnly) + 162 | nameOnlySlice.replace( 163 | new RegExp(`${prefix}${removedCases.pascal}`, 'g'), 164 | `${prefix}${addedCases.pascal}`, 165 | ) + 166 | buf.slice(endOfNameOnly) 167 | ); 168 | } 169 | return buf; 170 | }), 171 | ]), 172 | /** Writes any remaining propTypes between the // @suit-name-only tags */ 173 | b => { 174 | const startIndex = b.indexOf('propTypes = {') + 'propTypes = {'.length; 175 | const endIndex = b.indexOf('\n};', startIndex); 176 | const propTypesSlice = b.slice(startIndex, endIndex); 177 | const propTypesToAdd = [ 178 | ...Object.keys(initialState) 179 | .map(key => ({ 180 | key, 181 | value: initialState[key], 182 | })) 183 | // If the user has already added it to prop types, don't double add it 184 | .filter( 185 | ({ key }) => 186 | propTypesSlice.indexOf(`${cases.camel}${capitalize(key)}`) === -1, 187 | ) 188 | .map( 189 | ({ value, key }) => 190 | ` // ${cases.camel}${capitalize( 191 | key, 192 | )}: PropTypes.${propTypeFromTypeOf(typeof value)},`, 193 | ), 194 | // Adds functions from dispatchToProps 195 | ...Object.keys(actions) 196 | .filter(key => { 197 | /** If no prop filtering, give all props */ 198 | if (!hasPropFiltering) return true; 199 | return actions[key].passAsProp; 200 | }) 201 | // Filters out actions that have already been included 202 | .filter( 203 | key => propTypesSlice.indexOf(`submit${capitalize(key)}`) === -1, 204 | ) 205 | .map(key => ` // submit${capitalize(key)}: PropTypes.func,`), 206 | ]; 207 | const arrayOfLines = b.split('\n'); 208 | const indexToInsert = arrayOfLines.findIndex(line => 209 | line.includes('// @suit-name-only-start'), 210 | ); 211 | return [ 212 | ...arrayOfLines.filter((_, index) => index <= indexToInsert), 213 | ...propTypesToAdd, 214 | ...arrayOfLines.filter((_, index) => index > indexToInsert), 215 | ].join('\n'); 216 | }, 217 | prettify, 218 | ]); 219 | }; 220 | 221 | const propTypeFromTypeOf = type => { 222 | switch (type) { 223 | case 'function': 224 | return 'func'; 225 | case 'undefined': 226 | case 'object': 227 | return 'any'; 228 | case 'boolean': 229 | return 'bool'; 230 | default: 231 | return type; 232 | } 233 | }; 234 | -------------------------------------------------------------------------------- /commands/up/writeReducer/addActionsToInitialReducer.js: -------------------------------------------------------------------------------- 1 | const Parser = require('../../../tools/parser'); 2 | const Cases = require('../../../tools/cases'); 3 | const writeActionsInReducer = require('./writeActionsInReducer'); 4 | const { concat, parseCamelCaseToArray } = require('../../../tools/utils'); 5 | 6 | module.exports = ({ camel, actions }) => buffer => { 7 | const p = new Parser(buffer); 8 | p.resetTicker(); 9 | p.toNext(`export const ${camel}Reducer =`); 10 | const searchTerm = `switch (type) {`; 11 | const startIndex = p.toNext(searchTerm).index + searchTerm.length; 12 | let content = ''; 13 | Object.keys(actions) 14 | .map(key => ({ ...actions[key], name: key })) 15 | .forEach(action => { 16 | const c = new Cases(parseCamelCaseToArray(action.name)); 17 | const cases = c.all(); 18 | 19 | const operations = writeActionsInReducer({ 20 | action, 21 | }); 22 | 23 | content += concat([ 24 | action.describe ? ` // ${action.describe}` : null, 25 | ` case ${cases.constant}:`, 26 | operations, 27 | ``, 28 | ]); 29 | }); 30 | return ( 31 | concat([ 32 | buffer.slice(0, startIndex), 33 | ` // @suit-start`, 34 | content + ` // @suit-end`, 35 | ]) + 36 | ` ` + 37 | buffer.slice(startIndex) 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /commands/up/writeReducer/addCustomFunctions.js: -------------------------------------------------------------------------------- 1 | const { transforms, concat } = require('../../../tools/utils'); 2 | const replaceInNameOnly = require('../../../tools/replaceInNameOnly'); 3 | 4 | module.exports = ({ actions, keyChanges }) => buffer => 5 | transforms(buffer, [ 6 | ...keyChanges 7 | .filter(({ removed }) => removed.includes('actions')) 8 | .map(({ removedCases, addedCases }) => 9 | replaceInNameOnly( 10 | `${removedCases.camel}CustomFunction`, 11 | `${addedCases.camel}CustomFunction`, 12 | ), 13 | ), 14 | ...Object.keys(actions) 15 | .map(key => ({ name: key, ...actions[key] })) 16 | .filter(({ customFunction }) => typeof customFunction !== 'undefined') 17 | .map(({ name, describe }) => b => { 18 | const index = b.indexOf(`const ${name}CustomFunction`); 19 | if (index !== -1) return b; 20 | return concat([ 21 | b, 22 | `// @suit-name-only-start`, 23 | describe ? `// ${describe}` : null, 24 | `const ${name}CustomFunction = (state, payload) => {`, 25 | ` console.log('${name}CustomFunctionPayload', payload);`, 26 | ` return state;`, 27 | `};`, 28 | `// @suit-name-only-end`, 29 | ``, 30 | ]); 31 | }), 32 | ]); 33 | -------------------------------------------------------------------------------- /commands/up/writeReducer/addToCombineReducers.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('../../../tools/utils'); 2 | 3 | module.exports = camel => buffer => { 4 | const searchTerm = 'combineReducers({'; 5 | const index = buffer.indexOf(searchTerm) + searchTerm.length; 6 | return ( 7 | concat([ 8 | buffer.slice(0, index), 9 | ` ${camel}: ${camel}Reducer, // @suit-line`, 10 | ]) + buffer.slice(index) 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /commands/up/writeReducer/importConstants.js: -------------------------------------------------------------------------------- 1 | const Cases = require('../../../tools/cases'); 2 | const { 3 | transforms, 4 | parseCamelCaseToArray, 5 | ensureImport, 6 | } = require('../../../tools/utils'); 7 | 8 | module.exports = actions => buf => 9 | transforms(buf, [ 10 | ...Object.keys(actions) 11 | .map(key => ({ ...actions[key], name: key })) 12 | .map(action => { 13 | const c = new Cases(parseCamelCaseToArray(action.name)); 14 | const constant = c.constant(); 15 | return ensureImport(constant, './constants', { destructure: true }); 16 | }), 17 | ]); 18 | -------------------------------------------------------------------------------- /commands/up/writeReducer/index.js: -------------------------------------------------------------------------------- 1 | const writeOneReducer = require('./writeOneReducer'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | transforms, 5 | cleanFile, 6 | fixInlineImports, 7 | parseCamelCaseToArray, 8 | } = require('../../../tools/utils'); 9 | const checkIfDomainAlreadyPresent = require('../../../tools/checkIfDomainAlreadyPresent'); 10 | 11 | module.exports = ({ 12 | folder, 13 | arrayOfDomains, 14 | buffer, 15 | checkForErrors = true, 16 | keyChanges, 17 | }) => { 18 | /** Write Reducers */ 19 | let domainErrors = []; 20 | const newReducerBuffer = transforms(buffer, [ 21 | cleanFile, 22 | fixInlineImports, 23 | /** Writes a reducer for each domain */ 24 | ...arrayOfDomains 25 | // Filters out ones that already exist 26 | // .filter(({ domainName }) => { 27 | // const buf = cleanFile(buffer); 28 | // return buf.indexOf(`export const ${domainName}Reducer`) === -1; 29 | // }) 30 | .map(({ domainName, initialState, actions, describe }) => b => { 31 | const cases = new Cases(parseCamelCaseToArray(domainName)); 32 | const allDomainCases = cases.all(); 33 | if (checkForErrors) { 34 | domainErrors = [ 35 | ...domainErrors, 36 | ...checkIfDomainAlreadyPresent(folder, allDomainCases, actions), 37 | ]; 38 | } 39 | 40 | return writeOneReducer({ 41 | buffer: b, 42 | cases: allDomainCases, 43 | initialState, 44 | actions, 45 | keyChanges, 46 | describe, 47 | }); 48 | }), 49 | ]); 50 | return { 51 | buffer: newReducerBuffer, 52 | errors: domainErrors, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /commands/up/writeReducer/writeActionsInReducer.js: -------------------------------------------------------------------------------- 1 | const { concat, printObject } = require('../../../tools/utils'); 2 | 3 | module.exports = ({ action }) => { 4 | if (action.customFunction) { 5 | return concat([ 6 | ` return ${action.name}CustomFunction(state${ 7 | action.payload ? ', payload' : '' 8 | });`, 9 | ]); 10 | } 11 | 12 | if (!action.set) { 13 | return concat([` return state;`]); 14 | } 15 | 16 | return ( 17 | concat([ 18 | ` return state`, 19 | ...Object.entries(action.set) 20 | .map(entry => ({ 21 | key: entry[0], 22 | value: entry[1], 23 | })) 24 | .map(({ key, value }) => { 25 | if (typeof value === 'string' && !value.includes('payload')) { 26 | /* eslint-disable no-param-reassign */ 27 | value = `'${value}'`; 28 | } else if (typeof value === 'object' && value !== null) { 29 | value = printObject(value, ' '); 30 | } 31 | return ` .set('${key}', ${value})`; 32 | }), 33 | ]) + ';' 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /commands/up/writeReducer/writeInitialReducer.js: -------------------------------------------------------------------------------- 1 | const { 2 | concat, 3 | printObject, 4 | actionHasPayload, 5 | transforms, 6 | } = require('../../../tools/utils'); 7 | 8 | /** 9 | * Writes an initial frame of a reducer before the 'export default' section 10 | */ 11 | module.exports = ({ 12 | actions, 13 | display, 14 | describe, 15 | pascal, 16 | camel, 17 | initialState, 18 | }) => buf => 19 | transforms(buf, [ 20 | buffer => { 21 | const startIndex = buffer.indexOf(`export const ${camel}Reducer = `); 22 | const hasPayload = actionHasPayload(actions); 23 | if (startIndex === -1) return buffer; 24 | const slice = buffer.slice( 25 | startIndex, 26 | buffer.indexOf(') => {', startIndex) + 6, 27 | ); 28 | return buffer.replace( 29 | slice, 30 | concat([ 31 | `export const ${camel}Reducer = (state = initial${pascal}State, { type${ 32 | hasPayload ? ', payload' : '' 33 | } }) => {`, 34 | ]), 35 | ); 36 | }, 37 | buffer => { 38 | const index = buffer.lastIndexOf('export default'); 39 | const hasPayload = actionHasPayload(actions); 40 | 41 | /** Detect if it exists */ 42 | 43 | const alreadyExists = buffer.includes(`export const ${camel}Reducer = (`); 44 | if (alreadyExists) { 45 | return buffer; 46 | } 47 | 48 | /** Change payload */ 49 | 50 | /** */ 51 | 52 | return concat([ 53 | buffer.slice(0, index), 54 | `// @suit-name-only-start`, 55 | `export const ${camel}Reducer = (state = initial${pascal}State, { type${ 56 | hasPayload ? ', payload' : '' 57 | } }) => {`, 58 | ` switch (type) {`, 59 | ` default:`, 60 | ` return state;`, 61 | ` }`, 62 | `};`, 63 | `// @suit-name-only-end`, 64 | ``, 65 | buffer.slice(index), 66 | ]); 67 | }, 68 | buffer => { 69 | const index = buffer.indexOf(`export const ${camel}Reducer`); 70 | return concat([ 71 | buffer.slice(0, index), 72 | `// @suit-start`, 73 | `/**`, 74 | ` * ${display} Reducer`, 75 | describe ? ` * - ${describe}` : null, 76 | ` */`, 77 | ``, 78 | `export const initial${pascal}State = fromJS(${printObject( 79 | initialState, 80 | )});`, 81 | ``, 82 | `// @suit-end`, 83 | buffer.slice(index), 84 | ]); 85 | }, 86 | ]); 87 | -------------------------------------------------------------------------------- /commands/up/writeReducer/writeOneReducer.js: -------------------------------------------------------------------------------- 1 | const { transforms, prettify, ensureImport } = require('../../../tools/utils'); 2 | 3 | const writeInitialReducer = require('./writeInitialReducer'); 4 | const addToCombineReducers = require('./addToCombineReducers'); 5 | const addActionsToInitialReducer = require('./addActionsToInitialReducer'); 6 | const importConstants = require('./importConstants'); 7 | const addCustomFunctions = require('./addCustomFunctions'); 8 | 9 | module.exports = ({ 10 | buffer, 11 | cases: { pascal, camel, display }, 12 | initialState, 13 | actions, 14 | describe, 15 | keyChanges, 16 | }) => 17 | transforms(buffer, [ 18 | /** Adds in boilerplate if domain does not exist */ 19 | ensureImport('fromJS', 'immutable', { destructure: true }), 20 | ensureImport('combineReducers', 'redux', { destructure: true }), 21 | writeInitialReducer({ 22 | actions, 23 | display, 24 | describe, 25 | pascal, 26 | camel, 27 | initialState, 28 | }), 29 | /** Adds to combineReducers */ 30 | addToCombineReducers(camel), 31 | /** Adds actions */ 32 | addActionsToInitialReducer({ camel, actions }), 33 | /** Imports constants */ 34 | importConstants(actions), 35 | addCustomFunctions({ actions, keyChanges }), 36 | prettify, 37 | ]); 38 | -------------------------------------------------------------------------------- /commands/up/writeReducerTests/index.js: -------------------------------------------------------------------------------- 1 | const writeReducerTests = require('./writeReducerTests'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | parseCamelCaseToArray, 5 | cleanFile, 6 | fixInlineImports, 7 | transforms, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ buffer, arrayOfDomains }) => 11 | transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, initialState, actions }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)); 16 | const allDomainCases = cases.all(); 17 | 18 | return writeReducerTests({ 19 | buffer: b, 20 | cases: allDomainCases, 21 | actions, 22 | initialState, 23 | }); 24 | }), 25 | ]); 26 | -------------------------------------------------------------------------------- /commands/up/writeReducerTests/writeReducerTests.js: -------------------------------------------------------------------------------- 1 | const { 2 | transforms, 3 | prettify, 4 | concat, 5 | printObject, 6 | ensureImport, 7 | parseCamelCaseToArray, 8 | } = require('../../../tools/utils'); 9 | const Cases = require('../../../tools/cases'); 10 | 11 | module.exports = ({ buffer, cases, actions, initialState }) => 12 | transforms(buffer, [ 13 | ensureImport('fromJS', 'immutable', { destructure: true }), 14 | ensureImport(`${cases.camel}Reducer`, '../reducer', { destructure: true }), 15 | ...Object.keys(actions) 16 | .filter(action => !actions[action].customFunction) 17 | .map(actionName => buf => { 18 | const actionCases = new Cases(parseCamelCaseToArray(actionName)).all(); 19 | return transforms(buf, [ 20 | ensureImport(actionCases.camel, '../actions', { 21 | destructure: true, 22 | }), 23 | ]); 24 | }), 25 | b => 26 | concat([ 27 | b, 28 | `// @suit-start`, 29 | `describe('${cases.camel}Reducer', () => {`, 30 | ` it('returns the initial state', () => {`, 31 | ` expect(${cases.camel}Reducer(undefined, { type: '' }))`, 32 | ` .toEqual(fromJS(${printObject(initialState, ' ')}),`, 33 | ` );`, 34 | ` });`, 35 | /** Test each action in the reducer */ 36 | ...Object.keys(actions) 37 | .map(key => ({ ...actions[key], name: key })) 38 | .map(action => { 39 | /** TODO: make this better */ 40 | if (!action.set) return ``; 41 | const arrayOfSets = Object.keys(action.set) 42 | .map(key => ({ 43 | key, 44 | value: action.set[key], 45 | hasPayload: `${action.set[key]}`.includes('payload'), 46 | })) 47 | .map(set => ({ 48 | ...set, 49 | value: 50 | typeof set.value === 'string' && !set.hasPayload 51 | ? `'${set.value}'` 52 | : typeof set.value === 'object' && set.value !== null 53 | ? printObject(set.value, ' ') 54 | : set.value, 55 | })); 56 | const payloadValues = arrayOfSets 57 | .filter(({ hasPayload }) => hasPayload) 58 | .map(({ value }) => value); 59 | // Checks if payload should be rendered as an object 60 | const payloadIsObject = 61 | payloadValues.length > 1 || 62 | payloadValues.filter(val => val.includes('.')).length; 63 | return concat([ 64 | ` describe('${action.name}', () => {`, 65 | ` it('alters the state as expected', () => {`, 66 | // if only passes one payload, only test one payload 67 | payloadValues.length === 1 && !payloadIsObject 68 | ? ` const payload = 'dummyPayload';` 69 | : null, 70 | // If multiple payloads, define payload as an object and define properties 71 | payloadIsObject ? ` const payload = {};` : null, 72 | payloadIsObject 73 | ? concat([ 74 | ...payloadValues.map( 75 | value => ` ${value} = 'dummyPayload';`, 76 | ), 77 | ]) 78 | : null, 79 | ` const newState = ${cases.camel}Reducer(`, 80 | ` undefined,`, 81 | // If it has a payload, pass it to the action creator 82 | ` ${action.name}(${ 83 | payloadValues.length > 0 ? 'payload' : '' 84 | }),`, 85 | ` );`, 86 | ...arrayOfSets.map(({ key, value }) => 87 | concat([ 88 | ` expect(newState.get('${key}')).toEqual(${value});`, 89 | ]), 90 | ), 91 | ` });`, 92 | ` });`, 93 | ]); 94 | }), 95 | `});`, 96 | `// @suit-end`, 97 | ``, 98 | ]), 99 | prettify, 100 | ]); 101 | -------------------------------------------------------------------------------- /commands/up/writeSaga/index.js: -------------------------------------------------------------------------------- 1 | const writeSaga = require('./writeSaga'); 2 | const writeNameControlSaga = require('./writeNameControlSaga'); 3 | const Cases = require('../../../tools/cases'); 4 | const { 5 | transforms, 6 | cleanFile, 7 | fixInlineImports, 8 | parseCamelCaseToArray, 9 | concat, 10 | } = require('../../../tools/utils'); 11 | const checkIfNoAllSagas = require('../../../tools/checkIfNoAllSagas'); 12 | 13 | module.exports = ({ sagaBuffer, arrayOfDomains, keyChanges }) => { 14 | let sagaErrors = checkIfNoAllSagas(sagaBuffer); 15 | let sagaMessages = []; 16 | const newBuffer = transforms(sagaBuffer, [ 17 | cleanFile, 18 | fixInlineImports, 19 | ...arrayOfDomains.map(({ domainName, actions }) => buffer => { 20 | const cases = new Cases(parseCamelCaseToArray(domainName)); 21 | const allDomainCases = cases.all(); 22 | const actionsWithSagas = Object.keys(actions).filter( 23 | key => typeof actions[key].saga !== 'undefined', 24 | ); 25 | if (actionsWithSagas > 1) { 26 | sagaErrors.push( 27 | concat([ 28 | `More than one action in ${ 29 | allDomainCases.display 30 | } has been given a saga`, 31 | `- Only one action can be assigned a saga per reducer.`, 32 | ]), 33 | ); 34 | return buffer; 35 | } 36 | 37 | if (actionsWithSagas < 1) { 38 | return buffer; 39 | } 40 | 41 | const actionCases = new Cases( 42 | parseCamelCaseToArray(actionsWithSagas[0]), 43 | ).all(); 44 | const actionObject = actions[actionCases.camel]; 45 | 46 | if (actionObject.saga === true) { 47 | const { 48 | buffer: uncontrolledSagaBuffer, 49 | errors: uncontrolledSagaErrors, 50 | messages: uncontrolledSagaMessages, 51 | } = writeSaga({ 52 | buffer, 53 | cases: allDomainCases, 54 | actionCases, 55 | action: actionObject, 56 | }); 57 | sagaErrors = [...sagaErrors, ...uncontrolledSagaErrors]; 58 | sagaMessages = [...sagaMessages, ...uncontrolledSagaMessages]; 59 | return uncontrolledSagaBuffer; 60 | } 61 | 62 | if (!actionObject.saga.onFail) { 63 | sagaErrors.push( 64 | concat([ 65 | `The saga in ${actionCases.display} does not have an 'onFail' key.`, 66 | `- This means it won't report an error when it fails.`, 67 | `- try this:`, 68 | `- {`, 69 | `- "saga": {`, 70 | `- "onFail": "actionToFireOnFail"`, 71 | `- }`, 72 | `- }`, 73 | ]), 74 | ); 75 | } 76 | if (!actionObject.saga.onFail) { 77 | sagaErrors.push( 78 | concat([ 79 | `The saga in ${ 80 | actionCases.display 81 | } does not have an 'onSuccess' key.`, 82 | `- This means it won't report an error when it fails.`, 83 | `- try this:`, 84 | `- {`, 85 | `- "saga": {`, 86 | `- "onSuccess": "actionToFireOnSuccess"`, 87 | `- }`, 88 | `- }`, 89 | ]), 90 | ); 91 | } 92 | if (actionObject.saga.onFail && actionObject.saga.onSuccess) { 93 | const { 94 | buffer: nameControlBuffer, 95 | errors: nameControlErrors, 96 | messages: nameControlMessages, 97 | } = writeNameControlSaga({ 98 | buffer, 99 | domainCases: allDomainCases, 100 | action: actionObject, 101 | actionCases, 102 | failCases: new Cases( 103 | parseCamelCaseToArray(actionObject.saga.onFail), 104 | ).all(), 105 | successCases: new Cases( 106 | parseCamelCaseToArray(actionObject.saga.onSuccess), 107 | ).all(), 108 | keyChanges, 109 | }); 110 | sagaMessages = [...sagaMessages, ...nameControlMessages]; 111 | sagaErrors = [...sagaErrors, ...nameControlErrors]; 112 | return nameControlBuffer; 113 | } 114 | return buffer; 115 | }), 116 | ]); 117 | return { 118 | buffer: newBuffer, 119 | messages: sagaMessages, 120 | errors: sagaErrors, 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /commands/up/writeSaga/writeNameControlSaga.js: -------------------------------------------------------------------------------- 1 | const { 2 | concat, 3 | transforms, 4 | prettify, 5 | ensureImport, 6 | } = require('../../../tools/utils'); 7 | const replaceInNameOnly = require('../../../tools/replaceInNameOnly'); 8 | 9 | module.exports = ({ 10 | buffer, 11 | domainCases, 12 | actionCases, 13 | action, 14 | failCases, 15 | successCases, 16 | keyChanges, 17 | }) => { 18 | const messages = []; 19 | const newBuffer = transforms(buffer, [ 20 | ensureImport(failCases.camel, './actions', { destructure: true }), 21 | ensureImport(successCases.camel, './actions', { destructure: true }), 22 | ensureImport('takeLatest', 'redux-saga/effects', { destructure: true }), 23 | b => { 24 | const index = b.indexOf('{', b.indexOf('export default function*')) + 1; 25 | return concat([ 26 | b.slice(0, index), 27 | ` // @suit-start`, 28 | ` yield takeLatest(${actionCases.constant}, ${domainCases.camel});`, 29 | ` // @suit-end` + b.slice(index), 30 | ]); 31 | }, 32 | // Change sagas if already present 33 | b => 34 | transforms(b, [ 35 | ...keyChanges 36 | .filter( 37 | ({ removed }) => 38 | removed.includes('saga') && removed.includes('actions'), 39 | ) 40 | .map(({ removedCases, addedCases }) => 41 | replaceInNameOnly(removedCases.camel, addedCases.camel), 42 | ), 43 | ]), 44 | b => { 45 | const sagaPresent = b.indexOf(`function* ${domainCases.camel}`) !== -1; 46 | if (sagaPresent) { 47 | messages.push( 48 | `\nSAGA:`.green + 49 | ` ${ 50 | domainCases.camel 51 | } saga already present in file. No edits have been made.`, 52 | ); 53 | return b; 54 | } 55 | const index = b.indexOf(`export default`); 56 | 57 | messages.push( 58 | concat([ 59 | `\nSAGA:`.green + ` ${domainCases.camel} saga not found in file.`, 60 | `- Adding a basic skeleton of a saga. This needs to be updated manually.`, 61 | ]), 62 | ); 63 | 64 | return concat([ 65 | b.slice(0, index), 66 | `// @suit-name-only-start`, 67 | `function* ${domainCases.camel}(${ 68 | action.payload ? '{ payload }' : '' 69 | }) {`, 70 | ` let data = '';`, 71 | ` try {`, 72 | ` data = yield call(null${action.payload ? ', payload' : ''});`, 73 | ` } catch (err) {`, 74 | ` console.log(err); // eslint-disable-line`, 75 | ` yield put(${failCases.camel}());`, 76 | ` }`, 77 | ` if (!data) {`, 78 | ` yield put(${failCases.camel}());`, 79 | ` } else if (!data.error) {`, 80 | ` yield put(${successCases.camel}(data.body));`, 81 | ` } else {`, 82 | ` yield put(${failCases.camel}());`, 83 | ` }`, 84 | `}`, 85 | `// @suit-name-only-end`, 86 | ``, 87 | b.slice(index), 88 | ]); 89 | }, 90 | ensureImport(actionCases.constant, './constants', { destructure: true }), 91 | b => { 92 | if (b.indexOf('call(') !== -1) { 93 | return ensureImport('call', 'redux-saga/effects', { 94 | destructure: true, 95 | })(b); 96 | } 97 | return b; 98 | }, 99 | ensureImport('put', 'redux-saga/effects', { destructure: true }), 100 | prettify, 101 | ]); 102 | return { 103 | buffer: newBuffer, 104 | messages, 105 | errors: [], 106 | }; 107 | }; 108 | -------------------------------------------------------------------------------- /commands/up/writeSaga/writeSaga.js: -------------------------------------------------------------------------------- 1 | const { 2 | concat, 3 | transforms, 4 | prettify, 5 | ensureImport, 6 | } = require('../../../tools/utils'); 7 | 8 | module.exports = ({ buffer, cases, actionCases, action }) => { 9 | const messages = []; 10 | const newBuffer = transforms(buffer, [ 11 | ensureImport('takeLatest', 'redux-saga/effects', { destructure: true }), 12 | b => { 13 | const index = b.indexOf('{', b.indexOf('export default function*')) + 1; 14 | return concat([ 15 | b.slice(0, index), 16 | ` // @suit-start`, 17 | ` yield takeLatest(${actionCases.constant}, ${cases.camel});`, 18 | ` // @suit-end` + b.slice(index), 19 | ]); 20 | }, 21 | b => { 22 | const sagaPresent = b.indexOf(`function* ${cases.camel}`) !== -1; 23 | if (sagaPresent) { 24 | messages.push( 25 | `\nSAGA:`.green + 26 | ` ${ 27 | cases.camel 28 | } saga already present in file. No edits have been made.`, 29 | ); 30 | return b; 31 | } 32 | const index = b.indexOf(`export default`); 33 | 34 | messages.push( 35 | concat([ 36 | `\nSAGA:`.green + ` ${cases.camel} saga not found in file.`, 37 | `- Adding a basic skeleton of a saga. This needs to be updated manually.`, 38 | ]), 39 | ); 40 | 41 | return concat([ 42 | b.slice(0, index), 43 | `/**`, 44 | ` * ${cases.display} Saga`, 45 | ` * This saga was added by boilersuit, but is not managed by boilersuit.`, 46 | ` * That means you need to edit it yourself, and delete it yourself if your`, 47 | ` * actions, constants or reducer name changes.`, 48 | ` */`, 49 | `function* ${cases.camel}(${action.payload ? '{ payload }' : ''}) {`, 50 | ` let data = '';`, 51 | ` try {`, 52 | ` data = yield call(null${action.payload ? ', payload' : ''});`, 53 | ` } catch (err) {`, 54 | ` console.log(err); // eslint-disable-line`, 55 | ` yield put(actionToFireWhen${cases.pascal}Fails());`, 56 | ` }`, 57 | ` if (!data) {`, 58 | ` yield put(actionToFireWhen${cases.pascal}Fails());`, 59 | ` } else if (!data.error) {`, 60 | ` yield put(actionToFireWhen${cases.pascal}Succeeds(data.body));`, 61 | ` } else {`, 62 | ` yield put(actionToFireWhen${cases.pascal}Fails());`, 63 | ` }`, 64 | `}`, 65 | ``, 66 | b.slice(index), 67 | ]); 68 | }, 69 | b => { 70 | if (b.indexOf('call(') !== -1) { 71 | return ensureImport('call', 'redux-saga/effects', { 72 | destructure: true, 73 | })(b); 74 | } 75 | return b; 76 | }, 77 | ensureImport('put', 'redux-saga/effects', { destructure: true }), 78 | ensureImport(actionCases.constant, './constants', { destructure: true }), 79 | prettify, 80 | ]); 81 | return { 82 | buffer: newBuffer, 83 | messages, 84 | errors: [], 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /commands/up/writeSelectorTests/index.js: -------------------------------------------------------------------------------- 1 | const writeSelectorTests = require('./writeSelectorTests'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | parseCamelCaseToArray, 5 | cleanFile, 6 | fixInlineImports, 7 | transforms, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ buffer, arrayOfDomains, folder }) => 11 | transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, initialState }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)).all(); 16 | 17 | return writeSelectorTests({ 18 | buffer: b, 19 | cases, 20 | initialState, 21 | folder, 22 | }); 23 | }), 24 | ]); 25 | -------------------------------------------------------------------------------- /commands/up/writeSelectorTests/writeSelectorTests.js: -------------------------------------------------------------------------------- 1 | const { 2 | transforms, 3 | prettify, 4 | concat, 5 | ensureImport, 6 | parseCamelCaseToArray, 7 | getDomainNameFromFolder, 8 | printObject, 9 | unCapitalize, 10 | } = require('../../../tools/utils'); 11 | const Cases = require('../../../tools/cases'); 12 | 13 | module.exports = ({ buffer, cases, initialState, folder }) => 14 | transforms(buffer, [ 15 | ensureImport('fromJS', 'immutable', { destructure: true }), 16 | /** Creates a mocked-up version of the state for testing */ 17 | b => { 18 | const toInsert = concat([ 19 | `const mockedState = fromJS({`, 20 | ` ${unCapitalize(getDomainNameFromFolder(folder))}: {`, 21 | ]); 22 | if (b.indexOf(toInsert) === -1) { 23 | return concat([ 24 | b, 25 | `// @suit-start`, 26 | toInsert, 27 | ` },`, 28 | `});`, 29 | `// @suit-end`, 30 | ]); 31 | } 32 | return b; 33 | }, 34 | /** Fills in that mocked-up state */ 35 | b => { 36 | const searchTerm = concat([ 37 | `const mockedState = fromJS({`, 38 | ` ${unCapitalize(getDomainNameFromFolder(folder))}: {`, 39 | ]); 40 | const index = b.indexOf(searchTerm) + searchTerm.length; 41 | return concat([ 42 | b.slice(0, index), 43 | ` ${cases.camel}: ${printObject(initialState, ' ')},` + 44 | b.slice(index), 45 | ]); 46 | }, 47 | ensureImport(`makeSelect${cases.pascal}`, '../selectors', { 48 | destructure: true, 49 | }), 50 | b => 51 | concat([ 52 | b, 53 | `// @suit-start`, 54 | `describe('makeSelect${cases.pascal}', () => {`, 55 | ` it('should return the correct value', () => {`, 56 | ` const selector = makeSelect${cases.pascal}();`, 57 | ` expect(selector(mockedState))`, 58 | ` .toEqual(fromJS(mockedState.get('${unCapitalize( 59 | getDomainNameFromFolder(folder), 60 | )}').get('${cases.camel}')));`, 61 | ` });`, 62 | `});`, 63 | `// @suit-end`, 64 | ]), 65 | ...Object.keys(initialState) 66 | .map(key => new Cases(parseCamelCaseToArray(key)).all()) 67 | .map(fieldCases => 68 | ensureImport( 69 | `makeSelect${cases.pascal}${fieldCases.pascal}`, 70 | '../selectors', 71 | { destructure: true }, 72 | ), 73 | ), 74 | ...Object.keys(initialState) 75 | .map(key => ({ 76 | key, 77 | value: 78 | /* eslint-disable no-nested-ternary */ 79 | typeof initialState[key] === 'string' 80 | ? `'${initialState[key]}'` 81 | : typeof initialState[key] === 'object' && 82 | initialState[key] !== null 83 | ? `fromJS(${printObject(initialState[key], ' ')})` 84 | : initialState[key], 85 | cases: new Cases(parseCamelCaseToArray(key)).all(), 86 | })) 87 | .map(field => b => 88 | concat([ 89 | b, 90 | `// @suit-start`, 91 | `describe('makeSelect${cases.pascal}${field.cases.pascal}', () => {`, 92 | ` it('should return the correct value', () => {`, 93 | ` const selector = makeSelect${cases.pascal}${ 94 | field.cases.pascal 95 | }();`, 96 | ` expect(selector(mockedState))`, 97 | ` .toEqual(${field.value});`, 98 | ` });`, 99 | `});`, 100 | `// @suit-end`, 101 | ``, 102 | ]), 103 | ), 104 | prettify, 105 | ]); 106 | -------------------------------------------------------------------------------- /commands/up/writeSelectors/index.js: -------------------------------------------------------------------------------- 1 | const writeSelectors = require('./writeSelectors'); 2 | const Cases = require('../../../tools/cases'); 3 | const { 4 | parseCamelCaseToArray, 5 | cleanFile, 6 | fixInlineImports, 7 | transforms, 8 | } = require('../../../tools/utils'); 9 | 10 | module.exports = ({ arrayOfDomains, folder, buffer }) => 11 | transforms(buffer, [ 12 | cleanFile, 13 | fixInlineImports, 14 | ...arrayOfDomains.map(({ domainName, initialState }) => b => { 15 | const cases = new Cases(parseCamelCaseToArray(domainName)); 16 | const allDomainCases = cases.all(); 17 | 18 | return writeSelectors({ 19 | buffer: b, 20 | cases: allDomainCases, 21 | initialState, 22 | folder, 23 | }); 24 | }), 25 | ]); 26 | -------------------------------------------------------------------------------- /commands/up/writeSelectors/writeSelectors.js: -------------------------------------------------------------------------------- 1 | const Cases = require('../../../tools/cases'); 2 | const { 3 | concat, 4 | transforms, 5 | parseCamelCaseToArray, 6 | prettify, 7 | ensureImport, 8 | capitalize, 9 | getDomainNameFromFolder, 10 | } = require('../../../tools/utils'); 11 | 12 | module.exports = ({ buffer, cases, initialState, folder }) => { 13 | const domainName = getDomainNameFromFolder(folder); 14 | const mainSelector = concat([ 15 | `export const makeSelect${cases.pascal} = () =>`, 16 | ` createSelector(select${capitalize(domainName)}Domain, (substate) => {`, 17 | ` if (!substate) return fromJS({});`, 18 | ` // checks if the domain is immutable, and parses it correctly`, 19 | ` return fromJS(substate.toJS ? substate.get('${ 20 | cases.camel 21 | }') : substate.${cases.camel});`, 22 | ` });`, 23 | ]); 24 | 25 | return transforms(buffer, [ 26 | ensureImport('fromJS', 'immutable', { destructure: true }), 27 | /** Adds the domain */ 28 | b => 29 | concat([ 30 | b, 31 | `// @suit-start`, 32 | ``, 33 | `/** ${cases.display} Selectors */`, 34 | ``, 35 | mainSelector, 36 | !Object.keys(initialState).length ? '// @suit-end' : undefined, 37 | ]), 38 | /** Adds each field */ 39 | ...Object.keys(initialState) 40 | .map(key => ({ name: key, ...initialState[key] })) 41 | .reverse() 42 | .map(({ name }, i) => b => { 43 | const c = new Cases(parseCamelCaseToArray(name)); 44 | const fieldCases = c.all(); 45 | 46 | const index = b.indexOf(mainSelector) + mainSelector.length; 47 | 48 | let content = ''; 49 | content += concat([ 50 | ``, 51 | `export const makeSelect${cases.pascal}${fieldCases.pascal} = () =>`, 52 | ` createSelector(makeSelect${cases.pascal}(), (substate) =>`, 53 | ` substate.get('${fieldCases.camel}'),`, 54 | ` );`, 55 | ]); 56 | if (i === 0) { 57 | content += '\n\n// @suit-end'; 58 | } 59 | 60 | return concat([b.slice(0, index), content, b.slice(index)]); 61 | }), 62 | prettify, 63 | ]); 64 | }; 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const gaze = require('gaze'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const up = require('./commands/up'); 8 | const ajax = require('./commands/ajax'); 9 | const rm = require('./commands/rm'); 10 | const { version } = require('./package.json'); 11 | 12 | program.version(version); 13 | 14 | program 15 | .command('up') 16 | .option('-f, --force', 'Force suit to re-render') 17 | .option('-o, --one ', 'Only render one suit') 18 | .action(cmd => { 19 | /** Calculate directories to watch */ 20 | let watchedDirectories = ['!node_modules/**/*']; 21 | if (cmd.one) { 22 | watchedDirectories = [`${path.resolve(cmd.one)}/**/suit.json`]; 23 | } else if (fs.existsSync(path.resolve('./.suitrc'))) { 24 | const config = JSON.parse( 25 | fs.readFileSync(path.resolve('./.suitrc')).toString(), 26 | ); 27 | if (config.include && config.include.length) { 28 | watchedDirectories = [ 29 | ...watchedDirectories, 30 | config.include.map(suitPath => 31 | path.resolve(`${suitPath}/**/suit.json`), 32 | ), 33 | ]; 34 | } else { 35 | watchedDirectories.push(['app/containers/**/suit.json']); 36 | } 37 | } else { 38 | watchedDirectories.push(['app/containers/**/suit.json']); 39 | } 40 | gaze(watchedDirectories, (err, watcher) => { 41 | // Resets the console 42 | process.stdout.write('\x1Bc'); 43 | const watchedFiles = Object.keys(watcher.relative()).length; 44 | console.log( 45 | `Watching ${watchedFiles} suit.json ${ 46 | watchedFiles > 1 && watchedFiles !== 0 ? 'files' : 'file' 47 | }...`.yellow, 48 | ); 49 | /** This does it the first time */ 50 | Object.entries(watcher.relative()).forEach(entry => { 51 | // This bit of fidgeting allows for suiting up from the same folder 52 | const schemaFile = (entry[0] === '.' ? './' : entry[0]) + entry[1][0]; 53 | up(schemaFile, { force: cmd.force }, watcher); 54 | }); 55 | 56 | let relativePaths = watcher.relative(); 57 | 58 | /** Add new suit.json files to the watched files */ 59 | watcher.on('added', () => { 60 | relativePaths = watcher.relative(); 61 | }); 62 | 63 | /** Then this watches further changes */ 64 | watcher.on('changed', () => { 65 | // Resets the console 66 | process.stdout.write('\x1Bc'); 67 | console.log( 68 | `Watching ${watchedFiles} suit.json ${ 69 | watchedFiles > 1 && watchedFiles !== 0 ? 'files' : 'file' 70 | }...`.yellow, 71 | ); 72 | // Gets the relative path 73 | /** This does it the first time */ 74 | Object.entries(relativePaths).forEach(entry => { 75 | // This bit of fidgeting allows for suiting up from the same folder 76 | const schemaFile = (entry[0] === '.' ? './' : entry[0]) + entry[1][0]; 77 | up(schemaFile, { quiet: true, force: cmd.force }, watcher); 78 | }); 79 | }); 80 | }); 81 | }); 82 | 83 | program.command('ajax ').action((folder, name) => { 84 | ajax(folder, name); 85 | }); 86 | 87 | program.command('remove ').action(folder => { 88 | rm(folder); 89 | }); 90 | 91 | program.command('rm ').action(folder => { 92 | rm(folder); 93 | }); 94 | 95 | program.command('field').action(() => { 96 | console.log('The field command has been deprecated'); 97 | }); 98 | 99 | program.command('domain').action(() => { 100 | console.log('The domain command has been deprecated'); 101 | }); 102 | 103 | program.command('single').action(() => { 104 | console.log('The single command has been deprecated'); 105 | }); 106 | 107 | program.parse(process.argv); 108 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattpocock/boilersuit/6a705168cb1dba93fe8e92b2736c41ac751915c3/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilersuit", 3 | "description": "A CLI tool for generating selectors, reducers, actions, constants and sagas in react-boilerplate", 4 | "version": "0.5.0", 5 | "main": "index.js", 6 | "author": "Matt Pocock ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mattpocock/boilersuit" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "colors": "^1.3.1", 14 | "commander": "^2.16.0", 15 | "deep-diff": "^1.0.2", 16 | "gaze": "^1.1.3" 17 | }, 18 | "pre-commit": [ 19 | "lint" 20 | ], 21 | "bin": { 22 | "suit": "./index.js" 23 | }, 24 | "scripts": { 25 | "test": "jest --watch -o", 26 | "lint": "eslint . -c ./.eslintrc --fix" 27 | }, 28 | "devDependencies": { 29 | "babel-eslint": "^8.2.6", 30 | "chai": "^4.1.2", 31 | "eslint": "^5.5.0", 32 | "eslint-config-airbnb": "15.0.1", 33 | "eslint-config-airbnb-base": "^13.1.0", 34 | "eslint-plugin-import": "^2.14.0", 35 | "jest": "^23.5.0", 36 | "pre-commit": "^1.2.2", 37 | "prettier": "^1.14.2" 38 | }, 39 | "peerDependencies": { 40 | "prettier": "1.14.2" 41 | }, 42 | "jest": { 43 | "coveragePathIgnorePatterns": [ 44 | "/node_modules/", 45 | "/example/" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
boilerplate logo
2 | 3 | 4 | 5 | ## What Is Boilersuit? 6 | 7 | Ever felt like you were writing too much boring, repetitive code just to get react-boilerplate working? 8 | 9 | Ever wasted hours on a debug in a reducer caused by a typo? 10 | 11 | Ever wished that you could edit just one file, instead of ten? 12 | 13 | **Enter Boilersuit**, the blazingly-fast, bug-proof way of working with Redux in react-boilerplate. 14 | 15 | **Don't write ten files, write one.** Define your state and actions in a JSON file and watch as your code writes itself. 16 | 17 | **Instant updates.** Typing `suit up` in your root directory makes boilersuit watch your files for changes. Need to change the name of an action? Just change it in the `suit.json` file and watch it change across your file system. 18 | 19 | **Automagical unit tests.** Working, comprehensive unit tests appear automatically as your JSON file changes. 20 | 21 | **Prevent stupid mistakes.** Suit knows if you've done a silly. Trying to change a piece of state that doesn't exist? Boilersuit will catch it. Got an action that doesn't do anything? Boilersuit will catch it. 22 | 23 | **Instant documentation.** Need to know how a reducer works? Just check the JSON file in the directory. Boilersuit can even be configured to request code comments, enforcing amazing documentation on large projects. 24 | 25 | ## How To Install 26 | 27 | ### Globally 28 | 29 | `npm i -g boilersuit` 30 | 31 | ### Per project 32 | 33 | `npm i boilersuit` 34 | 35 | Add `"suit": "suit up"` to the `"scripts"` object in your package.json. 36 | 37 | Then, you can run `npm run suit` instead of `suit up` below. 38 | 39 | ## How To Run It 40 | 41 | Once it's installed, go into the folder of a container and add a `suit.json` file. 42 | 43 | `suit.json` files always belong in the **FOLDER OF THE CONTAINER YOU WANT TO SUIT UP** - alongside index.js, reducers.js, actions.js etc. 44 | 45 | ``` 46 | actions.js 47 | constants.js 48 | index.js 49 | reducer.js 50 | saga.js 51 | selectors.js 52 | suit.json 53 | ``` 54 | 55 | This file acts as the manifest for boilersuit to make changes. Copy the one below if you fancy it. 56 | 57 | Once it's set up, run `suit up` in the root directory of your project. It will watch for changes in any suit.json file and reflect those changes in the surrounding container. 58 | 59 | ```json 60 | // suit.json 61 | { 62 | "submitTodo": { 63 | "describe": "Makes a Submit Todo API call", 64 | "initialState": { 65 | "isLoading": false, 66 | "hasSucceeded": false, 67 | "hasError": false, 68 | "errorMessage": "", 69 | "data": {} 70 | }, 71 | "actions": { 72 | "submitTodoStarted": { 73 | "describe": "Begins the Submit Todo API Call. Passes the todo as the payload to the saga.", 74 | "saga": { 75 | "onFail": "submitTodoFailed", 76 | "onSuccess": "submitTodoSucceeded" 77 | }, 78 | "passAsProp": true, 79 | "payload": true, 80 | "set": { 81 | "isLoading": true, 82 | "hasSucceeded": false, 83 | "hasError": false, 84 | "errorMessage": "", 85 | "data": {} 86 | } 87 | }, 88 | "submitTodoSucceeded": { 89 | "describe": "Called when the Submit Todo API call completes, passing info to data as a payload.", 90 | "set": { 91 | "isLoading": false, 92 | "hasSucceeded": true, 93 | "data": "payload" 94 | } 95 | }, 96 | "submitTodoFailed": { 97 | "describe": "Called when the Submit Todo API Call fails, delivering a standard error message.", 98 | "set": { 99 | "isLoading": false, 100 | "errorMessage": "Submit Todo has failed", 101 | "hasError": true 102 | } 103 | } 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | ## How Boilersuit works 110 | 111 | Boilersuit acts as a declarative syntax for quickly writing and editing reducers. It cuts down development time, prevents silly errors, and makes documentation easy. 112 | 113 | Boilersuit takes control of parts of your application, using a few little 'tags'. Boilersuit completely controls the code between these tags, which means **any changes you make to boilersuit-controlled code will not persist**. 114 | 115 | The exceptions are the `// @suit-name-only` tags, in which boilersuit only controls the names of functions and properties. 116 | 117 | ```javascript 118 | // @suit-start 119 | const codeBetweenTheseTags = 'Is deleted and re-written each time suit runs.'; 120 | // @suit-end 121 | 122 | const codeBeforeThisTag = 'Is deleted and re-written each time suit runs.'; // @suit-line 123 | 124 | // @suit-name-only-start 125 | const codeBetweenNameOnlyTags = [ 126 | 'Will not be deleted and re-written between runs. Suit will run', 127 | 'a find-and-replace with function or action names that have changed,', 128 | 'but will keep all code you write inside these tags.', 129 | '', 130 | 'Suit will never wholesale delete code between these tags, so you may', 131 | 'need to do some manual deletion if suit cannot work out what to alter.', 132 | 'But this means you can feel free to edit any code between these tags.', 133 | ]; 134 | // @suit-name-only-end 135 | ``` 136 | 137 | ## Commands 138 | 139 | ### Up 140 | 141 | Run this command in the root directory of your project. This will recursively check for changes in any directory below it. 142 | 143 | Usage: `suit up` 144 | 145 | #### --force 146 | 147 | Add the `--force` modifier if you want suit to re-render everything within a directory. 148 | 149 | #### --one 150 | 151 | Run suit in only one directory. Suit will still recursively check for suit files down the project tree, but only within that directory. 152 | 153 | Usage: `suit up --one app/containers/HomePage` 154 | 155 | ### Ajax 156 | 157 | Either generates or adds an ajax call to a suit.json. 158 | 159 | Usage: `suit ajax ` 160 | 161 | Example: `suit ajax app/containers/HomePage getPosts` 162 | 163 | ## Suit Files API 164 | 165 | ### Reducers 166 | 167 | Each `suit.json` file can have multiple reducers, which contain pieces of different state. 168 | 169 | These are defined as keys on the main json object. For example, if you needed three API calls to get some config, some posts and some images: 170 | 171 | ```json 172 | { 173 | "getConfig": { 174 | "initialState": { 175 | //... 176 | }, 177 | "actions": { 178 | //... 179 | } 180 | }, 181 | "getPosts": { 182 | "initialState": { 183 | //... 184 | }, 185 | "actions": { 186 | //... 187 | } 188 | }, 189 | "getImages": { 190 | "initialState": { 191 | //... 192 | }, 193 | "actions": { 194 | //... 195 | } 196 | } 197 | } 198 | ``` 199 | 200 | This will create three reducers: `getConfig`, `getPosts`, and `getImages`, and add them to `combineReducers` in the reducers file. 201 | 202 | ### initialState 203 | 204 | 205 | 206 | This is an object which defines the initial data structure of the reducer's state. 207 | 208 | Suit will create a selector for each of these fields on the initialState, and put them in `mapStateToProps` in your index file. 209 | 210 | ```json 211 | { 212 | "getImages": { 213 | "initialState": { 214 | "isLoading": false, 215 | "hasSucceeded": true, 216 | "data": null, 217 | "errorMessage": "", 218 | "hasError": false 219 | }, 220 | "actions": { 221 | //... 222 | } 223 | } 224 | } 225 | ``` 226 | 227 | ### mapToContainer 228 | 229 | Sometimes, you don't want to actually pass your reducer to the container that it shares a file with. This is especially true when initialising some global functions that lots of subcontainers share, such as configurations. 230 | 231 | When you don't want to pass the reducer to the container, just specify `"mapToContainer": false` on the reducer. This will stop index.js from being written at all by this reducer. 232 | 233 | ```json 234 | { 235 | "getImages": { 236 | "mapToContainer": false, 237 | "initialState": { 238 | "isLoading": false, 239 | "hasSucceeded": true, 240 | "data": null, 241 | "errorMessage": "", 242 | "hasError": false 243 | }, 244 | "actions": { 245 | //... 246 | } 247 | } 248 | } 249 | ``` 250 | 251 | ### actions 252 | 253 | 254 | 255 | Every reducer has an `"actions": {}` attribute, which is required to make the reducer work. 256 | 257 | A reducer can have as many actions as you like. Each action you add will get added to the `actions.js` file, get added to the reducer, get passed to `mapDispatchToProps`, and get a unique entry in the `constants.js` file to make sure it works. 258 | 259 | #### Passing Payloads 260 | 261 | Often, actions need to carry payloads - pieces of data that affect the state. Suit allows you to add payloads to your actions. 262 | 263 | Let's imagine that we have a popup, with an `isVisible` property. This `isVisible` property gets passed down to our container component in `mapStateToProps`. 264 | 265 | But having an `isVisible` property that never changes is pretty useless - we need to be able to change it. 266 | 267 | ```json 268 | { 269 | "popup": { 270 | "initialState": { 271 | "isVisible": false 272 | }, 273 | "actions": { 274 | "changeIsVisible": { 275 | "set": { 276 | "isVisible": "payload" 277 | } 278 | } 279 | } 280 | } 281 | } 282 | ``` 283 | 284 | `"payload"` is treated as special by suit. It means that when you pass a payload to this action, it will get sent to the state. 285 | 286 | If you want to pass an object as a payload, for instance to set the color of the popup as well as the visibility: 287 | 288 | ```json 289 | { 290 | "popup": { 291 | "initialState": { 292 | "isVisible": false, 293 | "color": "#fff" 294 | }, 295 | "actions": { 296 | "changePopup": { 297 | "set": { 298 | "isVisible": "payload.isVisible", 299 | "color": "payload.color" 300 | } 301 | } 302 | } 303 | } 304 | } 305 | ``` 306 | 307 | Then you can call `submitChangePopUp({ isVisible: true, color: 'red' })`, and it'll work. 308 | 309 | #### Payloads and Sagas 310 | 311 | Sometimes, you'll need to pass a payload in an action that won't affect the state. For instance, when you need to pass a payload to a saga. For this case, use `"payload": true` on the action: 312 | 313 | ```json 314 | { 315 | "getImages": { 316 | "initialState": { 317 | //... 318 | }, 319 | "actions": { 320 | "getImagesStarted": { 321 | "payload": true, 322 | "set": { "isLoading": true } 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | #### set 330 | 331 | `type: object` 332 | 333 | Defines how you want the data to be changed after the action is run. 334 | 335 | ```json 336 | { 337 | "getImages": { 338 | "initialState": { 339 | //... 340 | }, 341 | "actions": { 342 | "getImagesStarted": { 343 | "set": { "isLoading": true } 344 | } 345 | } 346 | } 347 | } 348 | ``` 349 | 350 | #### passAsProp 351 | 352 | `type: bool` 353 | 354 | Whether or not you want to pass this action to the container component in `mapDispatchToProps`. 355 | 356 | If passAsProp is not specified on any of your actions, boilersuit will pass **all your actions** to the index file. But adding `"passAsProp": true` to any of your actions will mean only that one gets put in `mapDispatchToProps`. 357 | 358 | ```json 359 | { 360 | "actions": { 361 | "getFieldsStarted": { 362 | "passAsProp": true, 363 | "set": { "isLoading": true } 364 | } 365 | } 366 | } 367 | ``` 368 | 369 | #### saga 370 | 371 | Sagas handle asynchronous calls in redux. In boilersuit, we support a very simple type of saga - one with a fail case and a success case. 372 | 373 | You pass this as an object, as in the example below: 374 | 375 | ```json 376 | { 377 | "actions": { 378 | "initialState": { 379 | //... 380 | }, 381 | "getImagesStarted": { 382 | "saga": { 383 | "onFail": "getImagesFailed", 384 | "onSuccess": "getImagesSucceeded" 385 | }, 386 | "set": { 387 | //... 388 | } 389 | }, 390 | "getImagesFailed": { 391 | "set": { 392 | //... 393 | } 394 | }, 395 | "getImagesSucceeded": { 396 | "set": { 397 | //... 398 | } 399 | } 400 | } 401 | } 402 | ``` 403 | 404 | Also, you can only have one action that creates a saga file per reducer. 405 | 406 | #### describe 407 | 408 | `type: string` 409 | 410 | Allows you to add a description which is added as comments to the code 411 | 412 | ```json 413 | { 414 | "actions": { 415 | "getFieldsStarted": { 416 | "describe": "Why do I exist? Do I pass a payload? What do I do?" 417 | } 418 | } 419 | } 420 | ``` 421 | 422 | #### extends 423 | 424 | 425 | 426 | `type: string` 427 | 428 | Allows you to take a shortcut to writing out a whole suit file. 429 | 430 | Setting extends to "ajax" will generate an ajax call for you in the suit file. This is currently the only case we support. 431 | 432 | ```json 433 | { 434 | "getTweets": { 435 | "extends": "ajax" 436 | } 437 | } 438 | ``` 439 | 440 | #### customFunction 441 | 442 | `"customFunction": true` 443 | 444 | Sometimes, you don't want to just `set` values in a reducer. You may want to manipulate them in more interesting ways, such as merging to objects, or concatenating two arrays. 445 | 446 | Adding `"customFunction": true` to an action will give you a function extracted out from the reducer to manipulate the state however you want. It'll appear in your `reducer.js` file, like this: 447 | 448 | ```javascript 449 | // @suit-name-only-start 450 | const changeSomeValueCustomFunction = (state, payload) => { 451 | console.log('changeSomeValueCustomFunctionPayload', payload); 452 | return state; 453 | }; 454 | // @suit-name-only-end 455 | ``` 456 | 457 | As with any `// @suit-name-only` tags, you can change anything inside the function, but suit will update the name of the function if the action name changes. 458 | 459 | You'll need to add `"payload": true` if you want the action to carry a payload, and the `customFunction` to receive that payload. 460 | 461 | One final note: `"customFunction"` cannot be combined with `"set"` - though this may change in the future if it seems useful. 462 | 463 | ### Compose 464 | 465 | If you feel like your suit file is getting too big, you can split it up into smaller chunks with `compose`. 466 | 467 | Imagine a file structure that looks like this: 468 | 469 | - suits 470 | - getTweets.json 471 | - getTodos.json 472 | - index.js 473 | - reducer.js 474 | - actions.js 475 | - constants.js 476 | - selectors.js 477 | - suit.json 478 | 479 | ```json 480 | // suit.json 481 | { 482 | "compose": ["suits/getTweets", "suits/getTodos"] 483 | } 484 | ``` 485 | 486 | ```json 487 | // suits/getTweets.json 488 | { 489 | "getTweets": { 490 | //... 491 | } 492 | } 493 | ``` 494 | 495 | ```json 496 | // suits/getTodos.json 497 | { 498 | "getTodos": { 499 | //... 500 | } 501 | } 502 | ``` 503 | 504 | `compose` breaks your suit file down into more manageable chunks to help keep things navigable and modular. 505 | 506 | ### Import 507 | 508 | Sometimes, containers get jealous about bits of state held in other containers, and they want a piece of it. You can reference bits of state from a different container using the `import` syntax. 509 | 510 | ```json 511 | // ../HomePage/suit.json 512 | { 513 | "getNavBarConfig": { 514 | "initialState": { 515 | "isLoading": false, 516 | "data": {} 517 | //... 518 | }, 519 | "actions": { 520 | "getNavBarConfigStarted": { 521 | //... 522 | } 523 | } 524 | } 525 | } 526 | ``` 527 | 528 | ```json 529 | // suit.json 530 | { 531 | "import": { 532 | "../HomePage": { 533 | "getNavBarConfig": { 534 | "selectors": ["isLoading", "data"], 535 | "actions": ["getNavBarConfigStarted"] 536 | } 537 | } 538 | } 539 | } 540 | ``` 541 | 542 | This will import the selectors used for `isLoading` and `data` and put them in mapStateToProps. It will also pull in the action and pass it into mapDispatchToProps. 543 | 544 | Bear in mind - this does not do anything clever, like initialize the other reducer or inject the right sagas. If you try to use a selector to a piece of state that has not been initialized yet, you will get errors. 545 | 546 | This is most useful in referencing selectors and actions which you know have been initialized, such as those on the `` reducer. 547 | 548 | ## Configuration 549 | 550 | You can add a .suitrc (or .suitrc.json) file to the root of your folder to configure boilersuit. We're planning on making this a lot more extensible. 551 | 552 | ### showDescribeWarnings 553 | 554 | You can configure suit to give you warnings if you don't specify a 'describe' key. Handy for keeping discipline on large codebases. 555 | 556 | ```json 557 | { 558 | "showDescribeWarnings": true 559 | } 560 | ``` 561 | 562 | ### include 563 | 564 | By default, suit looks in `app/containers` for suit files, but you can change this by adding this to the `.suitrc` config. 565 | 566 | ```json 567 | { 568 | "include": ["app/containers"] 569 | } 570 | ``` 571 | -------------------------------------------------------------------------------- /tools/cases.js: -------------------------------------------------------------------------------- 1 | const Cases = function(array) { 2 | this.array = array; 3 | }; 4 | 5 | Cases.prototype.capitalize = string => 6 | string.charAt(0).toUpperCase() + string.slice(1); 7 | 8 | Cases.prototype.camel = function() { 9 | return this.array 10 | .map((item, index) => { 11 | if (index === 0) { 12 | return item.toLowerCase(); 13 | } 14 | return this.capitalize(item); 15 | }) 16 | .join(''); 17 | }; 18 | 19 | Cases.prototype.constant = function() { 20 | return this.array.map(item => item.toUpperCase()).join('_'); 21 | }; 22 | 23 | Cases.prototype.display = function() { 24 | return this.array.map(item => this.capitalize(item)).join(' '); 25 | }; 26 | 27 | Cases.prototype.pascal = function() { 28 | return this.array.map(item => this.capitalize(item)).join(''); 29 | }; 30 | 31 | Cases.prototype.all = function() { 32 | return { 33 | display: this.display(this.array), 34 | pascal: this.pascal(this.array), 35 | constant: this.constant(this.array), 36 | camel: this.camel(this.array), 37 | }; 38 | }; 39 | 40 | module.exports = Cases; 41 | -------------------------------------------------------------------------------- /tools/checkErrorsInSchema.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('./utils'); 2 | const rm = require('../commands/rm'); 3 | const reservedKeywords = require('../tools/constants/reservedKeywords'); 4 | 5 | module.exports = (schema, folder) => { 6 | const errors = []; 7 | const domainKeys = Object.keys(schema).filter( 8 | domain => !reservedKeywords.includes(domain), 9 | ); 10 | if (!domainKeys.length && !schema.import) { 11 | errors.push( 12 | concat([ 13 | 'No domains defined within suit.json.', 14 | 'Try this:', 15 | '{', 16 | ' "getTweets": {}', 17 | '}', 18 | ]), 19 | ); 20 | rm(folder, { silent: true }); 21 | return errors; 22 | } 23 | const domains = domainKeys.map(key => ({ 24 | name: key, 25 | ...schema[key], 26 | })); 27 | domains.forEach(domain => { 28 | if (!domain.initialState || !Object.keys(domain.initialState).length) { 29 | errors.push( 30 | concat([ 31 | `No initialState defined on ${domain.name}`, 32 | `Try this:`.green, 33 | `{`, 34 | ` "${domain.name}": {`, 35 | ` "initialState": {`, 36 | ` "isLoading": true`, 37 | ` }`, 38 | ` }`, 39 | `}`, 40 | ]), 41 | ); 42 | } 43 | if (!domain.actions || !Object.keys(domain.actions).length) { 44 | errors.push( 45 | concat([ 46 | `No actions defined on ${domain.name}`, 47 | `Try this:`.green, 48 | `{`, 49 | ` "${domain.name}": {`, 50 | ` "actions": {`, 51 | ` "${domain.name}FirstAction: {`, 52 | ` "set": {`, 53 | ` "isFirstAction": true`, 54 | ` }`, 55 | ` }`, 56 | ` }`, 57 | ` }`, 58 | `}`, 59 | ]), 60 | ); 61 | } 62 | if (errors.length) return; 63 | const arrayOfActions = Object.keys(domain.actions).map(key => ({ 64 | name: key, 65 | ...domain.actions[key], 66 | })); 67 | 68 | arrayOfActions.forEach(action => { 69 | if ( 70 | (!action.set || !Object.keys(action.set).length) && 71 | !action.customFunction 72 | ) { 73 | errors.push( 74 | concat([ 75 | `${ 76 | action.name 77 | } has no 'set' property defined. That means it won't do anything.`, 78 | `Try this:`.green, 79 | `${action.name}: {`, 80 | ` "set": {`, 81 | ` "isFirstAction": true`, 82 | ` }`, 83 | `}`, 84 | ]), 85 | ); 86 | } 87 | if (action.set) { 88 | Object.keys(action.set).forEach(actionSetKey => { 89 | if (!Object.keys(domain.initialState).includes(actionSetKey)) { 90 | errors.push( 91 | concat([ 92 | `${action.name}`.cyan + 93 | ` is attempting to set ` + 94 | `${actionSetKey}`.cyan + 95 | `. Sadly, ` + 96 | `${actionSetKey}`.cyan + 97 | ` doesn't exist in ` + 98 | `${domain.name}`.cyan + 99 | `'s initial state.`, 100 | `- Did you forget to define it in the initialState?`, 101 | `- Did you do a typo?`, 102 | `- Don't worry - it hapens to the best of us.`, 103 | ]), 104 | ); 105 | } 106 | }); 107 | } 108 | if (action.saga && (action.saga.onFail && action.saga.onSuccess)) { 109 | if (!actionExists(action.saga.onFail, domain)) { 110 | errors.push( 111 | concat([ 112 | `The saga in ` + 113 | `${action.name}`.cyan + 114 | ` is referencing an action, ` + 115 | `${action.saga.onFail}`.cyan + 116 | `, does not exist.`, 117 | `- Your choices are:`, 118 | ...arrayOfActions.map(({ name }) => `- ${name}`), 119 | ]), 120 | ); 121 | } 122 | if (!actionExists(action.saga.onSuccess, domain)) { 123 | errors.push( 124 | concat([ 125 | `The saga in ` + 126 | `${action.name}`.cyan + 127 | ` is referencing an action, ` + 128 | `${action.saga.onSuccess}`.cyan + 129 | `, does not exist.`, 130 | `- Your choices are:`, 131 | ...arrayOfActions.map(({ name }) => `- ${name}`), 132 | ]), 133 | ); 134 | } 135 | } 136 | }); 137 | }); 138 | return errors; 139 | }; 140 | 141 | const actionExists = (action, domain) => 142 | typeof domain.actions[action] !== 'undefined'; 143 | -------------------------------------------------------------------------------- /tools/checkIfBadBuffer.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('./utils'); 2 | 3 | module.exports = ({ reducer, index }) => { 4 | const errors = []; 5 | const noCombineReducers = reducer.indexOf('combineReducers({') === -1; 6 | if (noCombineReducers) { 7 | errors.push( 8 | concat([ 9 | `No 'combineReducers({' in './reducer.js'`, 10 | `- Consider a refactor to combine the reducers, such as:`, 11 | ``, 12 | `import { combineReducers } from 'redux';`, 13 | ``, 14 | `const getTweetsReducer = (state, action) => { ... }`, 15 | ``, 16 | `export default combineReducers({`, 17 | ` getTweets: getTweetsReducer,`, 18 | `});`, 19 | ]), 20 | ); 21 | } 22 | 23 | const noCreateStructuredSelector = 24 | index.indexOf('createStructuredSelector({') === -1; 25 | if (noCreateStructuredSelector) { 26 | errors.push( 27 | concat([ 28 | `No 'createStructuredSelector({' in './index.js'`, 29 | `- Consider a refactor in mapStateToProps to use createStructuredSelector.`, 30 | `import { createStructuredSelector } from 'reselect';`, 31 | `import { makeSelectGetTweetsIsLoading } from './selectors';`, 32 | ``, 33 | `const mapStateToProps = createStructuredSelector({`, 34 | ` getTweetsIsLoading: makeSelectGetTweetsIsLoading(),`, 35 | `});`, 36 | ]), 37 | ); 38 | } 39 | 40 | const noMapDispatchToProps = 41 | index.indexOf(`mapDispatchToProps(dispatch) {`) === -1; 42 | if (noMapDispatchToProps) { 43 | errors.push( 44 | concat([ 45 | `No 'createStructuredSelector({' in './index.js'`, 46 | `- Consider a refactor to use mapDispatchToProps.`, 47 | ``, 48 | `function mapDispatchToProps(dispatch) {`, 49 | ` return {`, 50 | ` submitGetTweets: (payload) => dispatch(getTweetsStarted(payload)),`, 51 | ` };`, 52 | `}`, 53 | ]), 54 | ); 55 | } 56 | 57 | return errors; 58 | }; 59 | -------------------------------------------------------------------------------- /tools/checkIfDomainAlreadyPresent.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Cases = require('./cases'); 4 | const { cleanFile, parseCamelCaseToArray } = require('./utils'); 5 | 6 | module.exports = (folder, cases, actions) => { 7 | const arrayOfActionCases = Object.keys(actions).map(key => 8 | new Cases(parseCamelCaseToArray(key)).all(), 9 | ); 10 | const errors = []; 11 | 12 | const constantFile = cleanFile( 13 | fs.readFileSync(path.resolve(`${folder}/constants.js`)).toString(), 14 | ); 15 | 16 | const hasDuplicateConstants = arrayOfActionCases.filter( 17 | ({ constant }) => constantFile.indexOf(constant) !== -1, 18 | ); 19 | 20 | const actionsFile = cleanFile( 21 | fs.readFileSync(path.resolve(`${folder}/actions.js`)).toString(), 22 | ); 23 | 24 | const hasDuplicateActions = arrayOfActionCases.filter( 25 | ({ camel }) => actionsFile.indexOf(camel) !== -1, 26 | ); 27 | 28 | if (hasDuplicateConstants.length) { 29 | errors.push( 30 | `Duplicate constant: ${ 31 | hasDuplicateConstants[0].constant 32 | } already present in constants.`, 33 | ); 34 | } 35 | if (hasDuplicateActions.length) { 36 | errors.push( 37 | `Duplicate action: ${ 38 | hasDuplicateActions[0].camel 39 | } already present in actions.`, 40 | ); 41 | } 42 | return errors; 43 | }; 44 | -------------------------------------------------------------------------------- /tools/checkIfNoAllSagas.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('./utils'); 2 | 3 | module.exports = buffer => { 4 | const errors = []; 5 | if (buffer.indexOf('export default function*') === -1) { 6 | errors.push( 7 | concat([ 8 | `No defaultSaga pattern present in saga.js`, 9 | `- Refactor to this pattern. This will allow suit-managed sagas`, 10 | ` to slot in alongside other sagas.`, 11 | `| `, 12 | `| export default function* defaultSaga() {`, 13 | `| takeLatest(ACTION_CONSTANT_NAME, sagaName);`, 14 | `| }`, 15 | ]), 16 | ); 17 | } 18 | return errors; 19 | }; 20 | -------------------------------------------------------------------------------- /tools/checkWarningsInSchema.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('./utils'); 2 | const reservedKeywords = require('./constants/reservedKeywords'); 3 | 4 | module.exports = (schema, config) => { 5 | const warnings = []; 6 | const domains = Object.keys(schema) 7 | .filter(domain => !reservedKeywords.includes(domain)) 8 | .map(key => ({ 9 | name: key, 10 | ...schema[key], 11 | })); 12 | 13 | domains.forEach(domain => { 14 | if ( 15 | typeof config.showDescribeWarnings !== 'undefined' && 16 | config.showDescribeWarnings 17 | ) { 18 | if (!domain.describe) { 19 | warnings.push( 20 | concat([ 21 | `No "describe" key defined on ` + 22 | `${domain.name}`.cyan + 23 | `. Write a description so we know what's going on.`, 24 | `| {`, 25 | `| "${domain.name}": {`, 26 | `| "describe": "What do I do? Why do I exist?"`, 27 | `| }`, 28 | `| }`, 29 | ]), 30 | ); 31 | } 32 | } 33 | const arrayOfActions = Object.keys(domain.actions).map(key => ({ 34 | name: key, 35 | ...domain.actions[key], 36 | })); 37 | 38 | arrayOfActions.forEach(action => { 39 | const notUsingPassAsProp = 40 | arrayOfActions.filter( 41 | ({ passAsProp }) => typeof passAsProp !== 'undefined', 42 | ).length === 0; 43 | if ( 44 | typeof config.showDescribeWarnings !== 'undefined' && 45 | config.showDescribeWarnings 46 | ) { 47 | if (!action.describe && (action.passAsProp || notUsingPassAsProp)) { 48 | warnings.push( 49 | concat([ 50 | `No "describe" key defined on ` + 51 | `${action.name}`.cyan + 52 | `. Write a description so we know what the action does.`, 53 | `| {`, 54 | `| "${domain.name}": {`, 55 | `| "actions": {`, 56 | `| "${action.name}": {`, 57 | `| "describe": "What do I do? What payload do I pass? Why do I exist?"`, 58 | `| }`, 59 | `| }`, 60 | `| }`, 61 | `| }`, 62 | ]), 63 | ); 64 | } 65 | } 66 | }); 67 | 68 | const arrayOfPropertiesSet = Object.keys( 69 | arrayOfActions 70 | .map(({ set }) => set) 71 | .reduce((a, b) => ({ ...a, ...b }), {}), 72 | ); 73 | Object.keys(domain.initialState).forEach(field => { 74 | if (!arrayOfPropertiesSet.includes(field)) { 75 | warnings.push( 76 | concat([ 77 | `Unused piece of state in ` + 78 | `${domain.name}`.cyan + 79 | `:` + 80 | ` ${field}`.cyan + 81 | ` is never changed by any action.`, 82 | `- Use it or lose it, buddy.`, 83 | ]), 84 | ); 85 | } 86 | }); 87 | }); 88 | return warnings; 89 | }; 90 | -------------------------------------------------------------------------------- /tools/constants/reservedKeywords.js: -------------------------------------------------------------------------------- 1 | module.exports = ['compose', 'import']; 2 | -------------------------------------------------------------------------------- /tools/parser.js: -------------------------------------------------------------------------------- 1 | const { concat } = require('./utils'); 2 | 3 | const Parser = function(buffer) { 4 | this.buffer = buffer; 5 | this.ticker = 0; 6 | }; 7 | 8 | Parser.prototype.lastImportIndex = function() { 9 | const index = this.buffer.lastIndexOf('import'); 10 | if (index !== -1) { 11 | return index; 12 | } 13 | 14 | throw new Error('Last import index could not be found'); 15 | }; 16 | 17 | Parser.prototype.firstImportIndex = function() { 18 | const index = this.buffer.indexOf('import'); 19 | if (index === -1) { 20 | throw new Error('No imports present in file.'); 21 | } 22 | return index; 23 | }; 24 | 25 | Parser.prototype.getImportIndex = function(filename) { 26 | let index = this.buffer.indexOf(`\n} from '${filename}`); 27 | if (index !== -1) { 28 | return { index, prefix: '\n ', suffix: `,` }; 29 | } 30 | index = this.buffer.indexOf(` } from '${filename}';`); 31 | if (index !== -1) { 32 | return { index, prefix: `, ` }; 33 | } 34 | return { 35 | index: this.lastImportIndex(), 36 | prefix: 'import {\n', 37 | suffix: `} from '${filename}';\n`, 38 | }; 39 | }; 40 | 41 | Parser.prototype.getCombineReducers = function() { 42 | const searchTerm = `combineReducers({`; 43 | const index = this.buffer.indexOf(searchTerm); 44 | if (index !== -1) { 45 | return { index: index + searchTerm.length, wasFound: true, prefix: `\n` }; 46 | } 47 | const exportDefault = this.getExportDefaultIndex(); 48 | return { 49 | index: exportDefault.index, 50 | wasFound: false, 51 | prefix: 52 | exportDefault.suffix + 53 | concat([`/**`, `export default combineReducers({`, ``]), 54 | suffix: concat([``, `});`, `*/`, ``]), 55 | }; 56 | }; 57 | 58 | Parser.prototype.getExportDefaultIndex = function() { 59 | const index = this.buffer.lastIndexOf('export default '); 60 | if (index !== -1) { 61 | return { index, suffix: `\n` }; 62 | } 63 | throw new Error('Could not find export default in file'); 64 | }; 65 | 66 | Parser.prototype.getAllSagasIndex = function() { 67 | const searchTerm = 'export default function* allSagas() {'; 68 | const index = this.buffer.indexOf(searchTerm); 69 | if (index !== -1) { 70 | return { 71 | wasFound: true, 72 | index: index + searchTerm.length, 73 | prefix: `\n`, 74 | }; 75 | } 76 | const exportDefault = this.getExportDefaultIndex(); 77 | return { 78 | index: exportDefault.index, 79 | wasFound: false, 80 | prefix: 81 | exportDefault.suffix + 82 | concat([`/**`, `export default function* allSagas() {`, ``]), 83 | suffix: concat([``, `};`, `*/`, ``]), 84 | }; 85 | }; 86 | 87 | Parser.prototype.getMapStateToPropsIndex = function() { 88 | const mapStateToPropsBeginning = this.buffer.indexOf('mapStateToProps'); 89 | const mapStateToPropsEnd = this.buffer.indexOf( 90 | '});', 91 | mapStateToPropsBeginning, 92 | ); 93 | return { index: mapStateToPropsEnd, suffix: '\n' }; 94 | }; 95 | 96 | Parser.prototype.includes = function(string) { 97 | return this.buffer.indexOf(string) !== -1; 98 | }; 99 | 100 | /** 101 | * Uses the ticker to go from piece of text to piece of text. 102 | */ 103 | Parser.prototype.toNext = function(string) { 104 | const index = this.buffer.indexOf(string, this.ticker); 105 | if (index >= this.ticker) { 106 | this.ticker = index; 107 | return { found: true, index }; 108 | } 109 | return { found: false }; 110 | }; 111 | 112 | /** 113 | * Uses the ticker to go from piece of text to piece of text. 114 | */ 115 | Parser.prototype.toPrev = function(string) { 116 | const index = this.buffer.lastIndexOf(string, this.ticker); 117 | if (index !== -1 && index <= this.ticker) { 118 | this.ticker = index; 119 | return { found: true, index }; 120 | } 121 | return { found: false }; 122 | }; 123 | 124 | Parser.prototype.resetTicker = function() { 125 | this.ticker = 0; 126 | }; 127 | 128 | Parser.prototype.indexOf = function(string) { 129 | return this.buffer.indexOf(string); 130 | }; 131 | 132 | module.exports = Parser; 133 | -------------------------------------------------------------------------------- /tools/printError.js: -------------------------------------------------------------------------------- 1 | module.exports = errors => { 2 | // errors.forEach(error => console.log('\nERROR: '.red + error)); 3 | console.log('\nERROR: '.red + errors[0]); 4 | }; 5 | -------------------------------------------------------------------------------- /tools/printMessages.js: -------------------------------------------------------------------------------- 1 | module.exports = messages => { 2 | messages.forEach(message => console.log(message)); 3 | }; 4 | -------------------------------------------------------------------------------- /tools/printWarning.js: -------------------------------------------------------------------------------- 1 | module.exports = warnings => { 2 | warnings.forEach(error => console.log('\nWARNING: '.yellow + error)); 3 | }; 4 | -------------------------------------------------------------------------------- /tools/replaceInNameOnly.js: -------------------------------------------------------------------------------- 1 | const { indexesOf, transforms } = require('./utils'); 2 | 3 | module.exports = (toRemove, toAdd) => buffer => 4 | transforms(buffer, [ 5 | ...indexesOf('// @suit-name-only-start', buffer) 6 | .map(startIndex => ({ 7 | startIndex, 8 | endIndex: buffer.indexOf('// @suit-name-only-end', startIndex), 9 | })) 10 | .map(sliceObject => ({ 11 | ...sliceObject, 12 | slice: buffer.slice(sliceObject.startIndex, sliceObject.endIndex), 13 | })) 14 | .map(({ slice }) => buf => 15 | buf.slice(0, buf.indexOf(slice)) + 16 | slice.replace(new RegExp(toRemove, 'g'), toAdd) + 17 | buf.slice(buf.indexOf(slice) + slice.length), 18 | ), 19 | ]); 20 | -------------------------------------------------------------------------------- /tools/tests/cases.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const Cases = require('../cases'); 3 | 4 | const cases = new Cases(['Hello', 'World']); 5 | 6 | describe('Cases', () => { 7 | it('Must have certain methods', () => { 8 | expect(typeof cases.capitalize).to.equal('function'); 9 | expect(typeof cases.camel).to.equal('function'); 10 | expect(typeof cases.pascal).to.equal('function'); 11 | expect(typeof cases.constant).to.equal('function'); 12 | expect(typeof cases.display).to.equal('function'); 13 | expect(typeof cases.all).to.equal('function'); 14 | }); 15 | 16 | describe('Capitalize', () => { 17 | it('Should capitalize something', () => { 18 | expect(cases.capitalize('poo')).to.equal('Poo'); 19 | }); 20 | }); 21 | 22 | describe('Camel', () => { 23 | it('Should put something in camel case', () => { 24 | expect(cases.camel()).to.equal('helloWorld'); 25 | }); 26 | }); 27 | 28 | describe('Constant', () => { 29 | it('Should put something in constant case', () => { 30 | expect(cases.constant()).to.equal('HELLO_WORLD'); 31 | }); 32 | }); 33 | 34 | describe('display', () => { 35 | it('Should put something in display case', () => { 36 | expect(cases.display()).to.equal('Hello World'); 37 | }); 38 | }); 39 | 40 | describe('all', () => { 41 | it('Should return an object containing all cases', () => { 42 | const { 43 | display, camel, constant, pascal, 44 | } = cases.all(); 45 | expect(display).to.equal('Hello World'); 46 | expect(camel).to.equal('helloWorld'); 47 | expect(constant).to.equal('HELLO_WORLD'); 48 | expect(pascal).to.equal('HelloWorld'); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tools/tests/ensureImport.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const fs = require('fs'); 3 | const { ensureImport, transforms, fixInlineImports } = require('../utils'); 4 | 5 | const file = fs 6 | .readFileSync('./tools/tests/mocks/withInlineImports.js') 7 | .toString(); 8 | 9 | describe('ensureImport', () => { 10 | it('Must have certain methods', () => { 11 | const result = transforms(file, [ 12 | fixInlineImports, 13 | ensureImport('property', './selectors', { destructure: true }), 14 | ensureImport('otherProperty', './actions', { destructure: true }), 15 | ]); 16 | const expected = `import { 17 | getAssessmentsStarted, 18 | applyFilter, 19 | getRoutesStarted, 20 | otherProperty, // @suit-line 21 | } from './actions'; 22 | 23 | import { 24 | CHANGE_LOCALE, 25 | } from './constants'; 26 | 27 | import CHANGE_LOCALE, { 28 | property, 29 | } from './selectors';` 30 | expect(result).to.contain(expected); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tools/tests/mocks/nameOnly.2.correct.js: -------------------------------------------------------------------------------- 1 | import Api from 'utils/apiClient'; 2 | import appConfig from 'appConfig'; 3 | import { 4 | takeLatest, // @suit-line 5 | call, // @suit-line 6 | put, // @suit-line 7 | } from 'redux-saga/effects'; 8 | import { 9 | DELETE_ASSESSMENT_ELEMENT_STARTED, // @suit-line 10 | GET_ASSESSMENT_ELEMENTS_STARTED, // @suit-line 11 | SOMETHING_STARTED, // @suit-line 12 | } from './constants'; 13 | import { 14 | getAssessmentElementsFailed, 15 | getAssessmentElementsSucceeded, 16 | deleteAssessmentElementFailed, 17 | deleteAssessmentElementSucceeded, 18 | somethingGreat, // @suit-line 19 | somethingSucceeded, // @suit-line 20 | } from './actions'; 21 | 22 | const api = new Api(appConfig.serverUrl); 23 | 24 | /* 25 | * 26 | * AssessmentElementsTableContainer saga 27 | * 28 | */ 29 | 30 | // @suit-name-only-start 31 | export function* something() { 32 | let data = ''; 33 | try { 34 | data = yield call(somethingAjaxCall); 35 | } catch (err) { 36 | console.log(err); // eslint-disable-line 37 | yield put(somethingAmazing()); 38 | } 39 | if (!data) { 40 | yield put(somethingAmazing()); 41 | } else if (!data.error) { 42 | yield put(somethingSucceeded(data.body)); 43 | } else { 44 | yield put(somethingAmazing()); 45 | } 46 | } 47 | // @suit-name-only-end 48 | 49 | export default function* defaultSaga() { 50 | // @suit-start 51 | yield takeLatest(SOMETHING_STARTED, something); 52 | yield takeLatest(GET_ASSESSMENT_ELEMENTS_STARTED, getAssessmentElements); 53 | yield takeLatest(DELETE_ASSESSMENT_ELEMENT_STARTED, deleteAssessmentElement); 54 | // @suit-end 55 | // All sagas go in here 56 | } 57 | -------------------------------------------------------------------------------- /tools/tests/mocks/nameOnly.2.js: -------------------------------------------------------------------------------- 1 | import Api from 'utils/apiClient'; 2 | import appConfig from 'appConfig'; 3 | import { 4 | takeLatest, // @suit-line 5 | call, // @suit-line 6 | put, // @suit-line 7 | } from 'redux-saga/effects'; 8 | import { 9 | DELETE_ASSESSMENT_ELEMENT_STARTED, // @suit-line 10 | GET_ASSESSMENT_ELEMENTS_STARTED, // @suit-line 11 | SOMETHING_STARTED, // @suit-line 12 | } from './constants'; 13 | import { 14 | getAssessmentElementsFailed, 15 | getAssessmentElementsSucceeded, 16 | deleteAssessmentElementFailed, 17 | deleteAssessmentElementSucceeded, 18 | somethingGreat, // @suit-line 19 | somethingSucceeded, // @suit-line 20 | } from './actions'; 21 | 22 | const api = new Api(appConfig.serverUrl); 23 | 24 | /* 25 | * 26 | * AssessmentElementsTableContainer saga 27 | * 28 | */ 29 | 30 | // @suit-name-only-start 31 | export function* something() { 32 | let data = ''; 33 | try { 34 | data = yield call(somethingAjaxCall); 35 | } catch (err) { 36 | console.log(err); // eslint-disable-line 37 | yield put(somethingFailed()); 38 | } 39 | if (!data) { 40 | yield put(somethingFailed()); 41 | } else if (!data.error) { 42 | yield put(somethingSucceeded(data.body)); 43 | } else { 44 | yield put(somethingFailed()); 45 | } 46 | } 47 | // @suit-name-only-end 48 | 49 | export default function* defaultSaga() { 50 | // @suit-start 51 | yield takeLatest(SOMETHING_STARTED, something); 52 | yield takeLatest(GET_ASSESSMENT_ELEMENTS_STARTED, getAssessmentElements); 53 | yield takeLatest(DELETE_ASSESSMENT_ELEMENT_STARTED, deleteAssessmentElement); 54 | // @suit-end 55 | // All sagas go in here 56 | } 57 | -------------------------------------------------------------------------------- /tools/tests/mocks/nameOnly.js: -------------------------------------------------------------------------------- 1 | // @suit-name-only-start 2 | somethingToChange; 3 | // @suit-name-only-end 4 | 5 | somethingToChange; 6 | 7 | // @suit-name-only-start 8 | somethingToChange; 9 | somethingElseToChange; 10 | // @suit-name-only-end 11 | 12 | // @suit-name-only-start 13 | somethingElseToChange; 14 | yetAnotherThingToChange; 15 | // @suit-name-only-end 16 | -------------------------------------------------------------------------------- /tools/tests/mocks/nameOnlyCorrect.js: -------------------------------------------------------------------------------- 1 | // @suit-name-only-start 2 | megaMan; 3 | // @suit-name-only-end 4 | 5 | somethingToChange; 6 | 7 | // @suit-name-only-start 8 | megaMan; 9 | somethingElseToChange; 10 | // @suit-name-only-end 11 | 12 | // @suit-name-only-start 13 | somethingElseToChange; 14 | yetAnotherThingToChange; 15 | // @suit-name-only-end 16 | -------------------------------------------------------------------------------- /tools/tests/mocks/withInlineImports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AdminManageAssessments 4 | * 5 | */ 6 | 7 | import { getAssessmentsStarted, applyFilter, getRoutesStarted } from './actions'; 8 | 9 | import { CHANGE_LOCALE } from './constants'; 10 | 11 | import CHANGE_LOCALE from './selectors'; -------------------------------------------------------------------------------- /tools/tests/parser.test.js: -------------------------------------------------------------------------------- 1 | // const { expect } = require('chai'); 2 | // const Parser = require('../parser'); 3 | 4 | // describe('Parser', () => { 5 | // const parser = new Parser(); 6 | // it('Should have certain methods', () => { 7 | // expect(typeof parser.lastImportIndex).to.equal('function'); 8 | // expect(typeof parser.getImportIndex).to.equal('function'); 9 | // }); 10 | 11 | // describe('lastImportIndex', () => { 12 | // let parser = new Parser('import'); 13 | // expect(parser.lastImportIndex().index).to.equal(0); 14 | // expect(parser.lastImportIndex().suffix).to.equal('\n'); 15 | // parser = new Parser(' import'); 16 | // expect(parser.lastImportIndex().index).to.equal(2); 17 | // }); 18 | 19 | // describe('getImportIndex', () => { 20 | // const parser = new Parser(`\n} from './actions';`); 21 | // expect(parser.getImportIndex('./actions').index).to.equal(0); 22 | // }); 23 | // }); 24 | -------------------------------------------------------------------------------- /tools/tests/replaceInNameOnly.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const replaceInNameOnly = require('../replaceInNameOnly'); 5 | 6 | const file = fs 7 | .readFileSync(path.resolve('./tools/tests/mocks/nameOnly.js')) 8 | .toString(); 9 | const correctFile = fs 10 | .readFileSync(path.resolve('./tools/tests/mocks/nameOnlyCorrect.js')) 11 | .toString(); 12 | 13 | const file2 = fs 14 | .readFileSync(path.resolve('./tools/tests/mocks/nameOnly.2.js')) 15 | .toString(); 16 | const correctFile2 = fs 17 | .readFileSync(path.resolve('./tools/tests/mocks/nameOnly.2.correct.js')) 18 | .toString(); 19 | 20 | describe('fixInlineImports', () => { 21 | it('Must work in a couple of cases', () => { 22 | expect(replaceInNameOnly('somethingToChange', 'megaMan')(file)).to.equal( 23 | correctFile, 24 | ); 25 | expect( 26 | replaceInNameOnly('somethingFailed', 'somethingAmazing')(file2), 27 | ).to.equal(correctFile2); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tools/tests/utils.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { indexesOf } = require('../utils'); 5 | 6 | const file = fs 7 | .readFileSync(path.resolve('./tools/tests/mocks/nameOnly.2.js')) 8 | .toString(); 9 | 10 | describe('indexesOf', () => { 11 | it('Must work', () => { 12 | expect(indexesOf('// @suit-name-only-start', file)[0]).to.equal(675); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | const concat = array => array.filter(line => line !== null).join(`\n`); 2 | 3 | const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1); 4 | 5 | const unCapitalize = string => string.charAt(0).toLowerCase() + string.slice(1); 6 | 7 | const fixFolderName = string => { 8 | if (string.charAt(string.length - 1) !== '/') { 9 | return `${string}/`; 10 | } 11 | return string; 12 | }; 13 | 14 | const transforms = (buffer, funcArray) => { 15 | let newBuffer = buffer; 16 | funcArray.forEach(func => { 17 | newBuffer = func(newBuffer); 18 | }); 19 | return newBuffer; 20 | }; 21 | 22 | const isCapital = string => string === string.toUpperCase(); 23 | 24 | const parseCamelCaseToArray = string => 25 | string.replace(/([A-Z])/g, letter => ` ${letter}`).split(' '); 26 | 27 | const printObject = (object, indent = '') => { 28 | const newObject = JSON.stringify(object, null, 2) 29 | .replace(/"(\w+)"\s*:/g, '$1:') 30 | .replace(/"/g, "'") 31 | .split('\n') 32 | .map(line => { 33 | const lastChar = line[line.length - 1]; 34 | return line.length === 1 || 35 | line.length === 2 || 36 | lastChar === '{' || 37 | lastChar === ',' 38 | ? line 39 | : `${line},`; 40 | }) 41 | .map((line, index) => (index === 0 ? `${line}` : `${indent}${line}`)) 42 | // Sorts out any payloads 43 | .map( 44 | line => 45 | line.includes('payload') && (line.includes(`"`) || line.includes(`'`)) 46 | ? line.replace(/'/g, '').replace(/"/g, '') 47 | : line, 48 | ) 49 | .join(`\n`); 50 | return newObject.slice(0, -2) + newObject.slice(-2); 51 | }; 52 | 53 | const cleanFile = buffer => { 54 | let newBuffer = buffer; 55 | while (newBuffer.indexOf('// @suit-start') !== -1) { 56 | const startIndex = 57 | newBuffer.lastIndexOf('\n', newBuffer.indexOf('// @suit-start')) + 1; 58 | const endIndex = 59 | newBuffer.indexOf('// @suit-end', startIndex) + '// @ suit-end'.length; 60 | newBuffer = newBuffer.slice(0, startIndex) + newBuffer.slice(endIndex); 61 | } 62 | newBuffer = newBuffer 63 | .split('\n') 64 | .filter(line => !line.includes('@suit-line')) 65 | .join('\n'); 66 | 67 | return newBuffer; 68 | }; 69 | 70 | const ensureImport = (property, fileName, { destructure = false }) => b => 71 | transforms(b, [ 72 | buffer => { 73 | /** Checks if already loaded */ 74 | const isImported = 75 | buffer 76 | .slice(0, buffer.lastIndexOf(` from '`)) 77 | .indexOf(`${property}`) !== -1; 78 | if (isImported) { 79 | return buffer; 80 | } 81 | 82 | const hasImportsFromFileName = buffer.indexOf(fileName) !== -1; 83 | 84 | /** If no imports from fileName, add it and the filename */ 85 | if (!hasImportsFromFileName) { 86 | /** We need to know if there are any imports. If not, add it to the top of the file */ 87 | const hasPrevImports = buffer.indexOf('import') !== -1; 88 | let index = 0; 89 | if (hasPrevImports) { 90 | index = buffer.lastIndexOf('import'); 91 | } 92 | if (destructure) { 93 | return ( 94 | buffer.slice(0, index) + 95 | concat([ 96 | `import {`, 97 | ` ${property}, // @suit-line`, 98 | `} from '${fileName}';`, 99 | buffer.slice(index), 100 | ]) 101 | ); 102 | } 103 | return concat([ 104 | buffer.slice(0, index), 105 | `import ${property} from '${fileName}'; // @suit-line`, 106 | buffer.slice(index), 107 | ]); 108 | } 109 | /** 110 | * Now we know that we have imports from the filename, 111 | * it's just whether it's destructured or not 112 | */ 113 | if (destructure) { 114 | const singleDestructureIndex = buffer.indexOf( 115 | `import {} from '${fileName}';`, 116 | ); 117 | if (singleDestructureIndex !== -1) { 118 | return ( 119 | buffer.slice(0, singleDestructureIndex + 8) + 120 | `\n ${property}, // @suit-line\n` + 121 | buffer.slice(singleDestructureIndex + 8) 122 | ); 123 | } 124 | const isOnNewLine = 125 | buffer 126 | .split('\n') 127 | .findIndex(line => line === `} from '${fileName}';`) !== -1; 128 | if (isOnNewLine) { 129 | const index = buffer.indexOf(`\n} from '${fileName}';`); 130 | return ( 131 | buffer.slice(0, index) + 132 | `\n ${property}, // @suit-line` + 133 | buffer.slice(index) 134 | ); 135 | } 136 | const index = buffer.indexOf(` from '${fileName}';`); 137 | return ( 138 | buffer.slice(0, index) + 139 | concat([`, {`, ` ${property}, // @suit-line`, `}`]) + 140 | buffer.slice(index) 141 | ); 142 | } 143 | 144 | /** 145 | * I don't think there's a case for handling 146 | * multiple default inputs from the same filename, 147 | * so I'm not writing it. 148 | */ 149 | return buffer; 150 | }, 151 | correctCommentedOutImport(fileName), 152 | ]); 153 | 154 | const removeWhiteSpace = buffer => { 155 | const lines = buffer.split('\n'); 156 | return lines 157 | .filter((thisLine, index) => !(thisLine === '' && lines[index + 1] === '')) 158 | .join('\n'); 159 | }; 160 | 161 | /** 162 | * If an import required by boilersuit has been commented out, 163 | * this corrects it 164 | */ 165 | const correctCommentedOutImport = fileName => buffer => 166 | transforms(buffer, [ 167 | b => { 168 | const index = buffer 169 | .slice(0, buffer.indexOf(`'${fileName}'`)) 170 | .lastIndexOf('import'); 171 | if (buffer.slice(index - 3, index - 1) === '//') { 172 | return b.slice(0, index - 3) + b.slice(index); 173 | } 174 | return b; 175 | }, 176 | ]); 177 | 178 | const removeSuitDoubling = buffer => 179 | buffer 180 | .replace(new RegExp(concat([`// @suit-end`, `// @suit-start`]), 'g'), '') 181 | .replace( 182 | new RegExp( 183 | concat([`// @suit-name-only-end`, `// @suit-name-only-start`]), 184 | 'g', 185 | ), 186 | '', 187 | ) 188 | .replace( 189 | new RegExp( 190 | concat([` // @suit-name-only-start`, ` // @suit-name-only-end`]), 191 | 'g', 192 | ), 193 | '', 194 | ) 195 | .replace( 196 | new RegExp( 197 | concat([`// @suit-name-only-end`, ``, `// @suit-name-only-start`]), 198 | 'g', 199 | ), 200 | '', 201 | ) 202 | .replace( 203 | new RegExp(concat([`// @suit-end`, ``, `// @suit-start`]), 'g'), 204 | '', 205 | ) 206 | .replace( 207 | new RegExp(concat([`// @suit-end`, ``, ``, `// @suit-start`]), 'g'), 208 | '', 209 | ) 210 | .replace( 211 | new RegExp(concat([`// @suit-end`, ``, ``, ``, `// @suit-start`]), 'g'), 212 | '', 213 | ) 214 | .replace( 215 | new RegExp(concat([` // @suit-end`, ` // @suit-start`, '']), 'g'), 216 | '', 217 | ) 218 | .replace( 219 | new RegExp(concat([` // @suit-end`, ` // @suit-start`, '']), 'g'), 220 | '', 221 | ); 222 | 223 | const correctInlineImports = buffer => 224 | transforms(buffer, [ 225 | ...eachIndexOf(buffer, 'import {') 226 | .map(startIndex => { 227 | const endIndex = buffer.indexOf(';', startIndex) + 1; 228 | return { 229 | length: endIndex - startIndex, 230 | content: buffer.slice(startIndex + 'import {'.length, endIndex), 231 | }; 232 | }) 233 | .filter(({ length }) => length <= 80) 234 | .filter(({ content }) => !content.includes('@suit')) 235 | .map(({ content }) => b => 236 | b.slice(0, b.indexOf(content)) + 237 | content 238 | .replace(/\n/g, '') 239 | .replace(/ {2}/g, ' ') 240 | .replace(/,(?=})/, ' ') + 241 | b.slice(b.indexOf(content) + content.length), 242 | ), 243 | ]); 244 | 245 | const eachIndexOf = (buffer, string) => { 246 | let count = 0; 247 | const indexArray = []; 248 | while (buffer.indexOf(string, count) !== -1) { 249 | const index = buffer.indexOf(string, count); 250 | count = index + 1; 251 | indexArray.push(index); 252 | } 253 | return indexArray; 254 | }; 255 | 256 | const prettify = buffer => 257 | transforms(buffer, [ 258 | removeWhiteSpace, 259 | removeSuitDoubling, 260 | removeWhiteSpace, 261 | correctInlineImports, 262 | ]); 263 | 264 | const fixInlineImports = buffer => { 265 | let newBuffer = buffer; 266 | while (newBuffer.indexOf('import { ') !== -1) { 267 | const startIndex = newBuffer.indexOf('import { ') + 'import {'.length; 268 | const endIndex = newBuffer.indexOf('} from', startIndex); 269 | const content = newBuffer 270 | .slice(startIndex, endIndex) 271 | .replace(/ /g, '') 272 | .split(',') 273 | .map(line => ` ${line.replace('\n', '').replace(',', '')},`) 274 | .join('\n'); 275 | newBuffer = concat([ 276 | newBuffer.slice(0, startIndex), 277 | content, 278 | newBuffer.slice(endIndex), 279 | ]); 280 | } 281 | return newBuffer; 282 | }; 283 | 284 | const actionHasPayload = actions => { 285 | if ( 286 | Object.keys(actions).filter( 287 | key => actions[key].payload && actions[key].customFunction, 288 | ).length 289 | ) { 290 | return true; 291 | } 292 | return ( 293 | Object.values(actions).filter(({ set }) => { 294 | if (!set) return false; 295 | return ( 296 | Object.values(set).filter(val => `${val}`.includes('payload')).length > 297 | 0 298 | ); 299 | }).length > 0 300 | ); 301 | }; 302 | 303 | const getDomainNameFromFolder = folder => 304 | folder 305 | .split('/') 306 | .reverse() 307 | .filter(x => x !== '')[0]; 308 | 309 | const indexesOf = (needle, haystack, modifier = 0) => { 310 | const instances = haystack.match(new RegExp(needle, 'g')); 311 | if (instances === null) return []; 312 | let lastIndex = 0; 313 | return instances.map(() => { 314 | const index = haystack.indexOf(needle, lastIndex) + modifier; 315 | lastIndex = index + 1; 316 | return index; 317 | }); 318 | }; 319 | 320 | const indexOfArray = (buffer, searchTerms = []) => { 321 | let index = 0; 322 | searchTerms.forEach(searchTerm => { 323 | if (index === -1) return; 324 | index = buffer.indexOf(searchTerm, index); 325 | }); 326 | return index; 327 | }; 328 | 329 | module.exports = { 330 | indexOfArray, 331 | concat, 332 | indexesOf, 333 | fixFolderName, 334 | transforms, 335 | isCapital, 336 | parseCamelCaseToArray, 337 | printObject, 338 | cleanFile, 339 | ensureImport, 340 | removeWhiteSpace, 341 | removeSuitDoubling, 342 | prettify, 343 | capitalize, 344 | fixInlineImports, 345 | actionHasPayload, 346 | eachIndexOf, 347 | getDomainNameFromFolder, 348 | unCapitalize, 349 | }; 350 | --------------------------------------------------------------------------------