├── .DS_Store ├── .gitignore ├── README.md ├── cli.ts ├── hookd └── file.jsx ├── index.ts ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── packages ├── hookd-cli │ ├── README.md │ ├── cli.js │ ├── package-lock.json │ └── package.json └── hookd │ ├── README.md │ ├── index.js │ ├── package-lock.json │ └── package.json ├── tests ├── README.md ├── jest-preload.js ├── testHelperFunctions.ts └── unit │ ├── components │ ├── ClassToFunction.jsx │ ├── ConstructorToHooks.jsx │ ├── StateToHooks.jsx │ ├── UCfragment.jsx │ ├── UCmultiple.jsx │ ├── UCstatic.jsx │ ├── UEwCDM.jsx │ ├── UEwCDU.jsx │ └── UEwCWU.jsx │ ├── index.test.ts │ ├── useContext.test.ts │ ├── useEffect.test.ts │ └── useState.test.ts ├── tsconfig.json ├── util ├── .DS_Store ├── constants │ ├── interfaces.ts │ ├── names.ts │ ├── parser.ts │ └── visitors.ts └── helperfunctions.ts └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hookd/8469808f6e82a50706e4935916b85d82c0233a4b/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static 3 | ast.jsx 4 | test 5 | /hookd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hookd 2 | A cli tool for converting React class components to functional components with hooks. 3 | 4 | We have two NPM modules for usage: 5 | 6 | [@reactionaries/hookd](https://github.com/oslabs-beta/Hookd/tree/master/packages/hookd) is our module 7 | 8 | [@reactionaries/hookd-cli](https://github.com/oslabs-beta/Hookd/tree/master/packages/hookd-cli) is our cli tool 9 | 10 | Hookd will create a `/hookd` directory with your newly converted file 11 | ## Babel 12 | Babel will be the main tool for parsing traversal and generating your new code. 13 | ## Resources 14 | ### Babel Packages 15 | - [babel parser](https://babeljs.io/docs/en/babel-parser) 16 | - parse existing code into an ast 17 | - [babel traverse](https://babeljs.io/docs/en/babel-traverse) 18 | - traversal and manipulation of the ast 19 | - [babel types](https://babeljs.io/docs/en/babel-types) 20 | - define and verify the state and creation of nodes 21 | - [babel generator](https://babeljs.io/docs/en/babel-generator) 22 | - creation of final ast code 23 | ### The Babel Handbook 24 | - [jamiebuilds' babel handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) fundamentals for creating babel plugins 25 | ### AST Explorer 26 | - [AST Explorer](https://astexplorer.net/) receives a special thanks 27 | 28 | ## Alpha Release 29 | Hookd is a transpilation and transformation tool for React projects looking to slowly convert their projects into functional components with hooks. 30 | Currently hookd only supports the major 3 hooks: useState, useEffect, useContext. Since the transfer of class component syntax to functional component syntax is not a direct one to one relationship, hookd transforms syntax nodes and tries to make assumptions about the logic of your application and build a new file based off those assumptions. 31 | Due to our early release, the tool should primarily be used as a templating tool to create files that you can later build upon rather than an immediate replacement for all your class components. 32 | 33 | ### useState 34 | - useState makes assumptions about `this.setState(cb)` where cb will `return` an object literal. It does not keep track of _any_ variables and thus should be accounted for during transformation. If the `cb` were to return something else besides an object literal, it will break. 35 | - `this.setState(() => {})` and `this.setState(function(){})` will work fine but `this.setState(() => ())` will break. The code assumes the body of the `ArrowFunctionExpression` will be a `BlockStatement`. 36 | - More syntaxes to account for that we have not thought about 37 | 38 | ### useEffect 39 | useEffect syntax in particular makes assumptions about stateful references within componentDidMount, componentDidUpdate, and componentWillUnmount to build one or multiple useEffect hooks. Additionally hookd will try to find stateful references within the body of any non-life cycle method handlers and look again for those handlers within the life cycle methods. 40 | Hookd will then build up each hook with a callback, return statement, and dependency array depending on the case that it requires. useEffect accomplishes this by building a state dependency tree of all stateful references, the lifecycle method they were called in and whether a setState call was invoked. While these factors should determine most use cases for useEffect they are hardly all encompassing. As this is an alpha release we hope to increase the amount of edge cases it accounts for. 41 | 42 | ### useContext 43 | The useContext logic parses through the AST looking for indicators that Context API functions have been utlized, it then generates useContext cases based upon the values assiciated with those indicators. JSX context.consumer tags will be removed in the next update. 44 | -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | import {parse,traverse,generate} from './util/constants/parser'; 2 | import {ImpDeclVisitor, classDeclarationVisitor} from './util/constants/visitors'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | if (process.argv[2]) { 7 | const file: string = fs.readFileSync(path.resolve((__dirname as string), process.argv[2]), 'utf-8').toString(); 8 | const ast: object = parse(file); 9 | traverse(ast, { 10 | enter(path: any) { 11 | path.traverse(ImpDeclVisitor); 12 | path.traverse(classDeclarationVisitor); 13 | } 14 | }) 15 | const newCode: string = generate(ast).code; 16 | if (!fs.existsSync(path.resolve(__dirname, './hookd'))) fs.mkdirSync(path.resolve(__dirname, 'hookd')); 17 | fs.writeFileSync(path.join(__dirname, `./hookd/${process.argv[2].split(/(\\|\/)/g).pop()}`), newCode as string) 18 | } 19 | else { 20 | throw Error('A path was not specified'); 21 | } -------------------------------------------------------------------------------- /hookd/file.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hookd/8469808f6e82a50706e4935916b85d82c0233a4b/hookd/file.jsx -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { parse, traverse, generate } from './util/constants/parser'; 2 | // if we can modularize visitors more, that'd be swell 3 | import { ImpDeclVisitor, classDeclarationVisitor } from './util/constants/visitors'; 4 | 5 | // the main method to traverse the ast 6 | function hookd(str: string): string { 7 | const ast = parse(str); 8 | traverse(ast, { 9 | enter(path: any) { 10 | path.traverse(ImpDeclVisitor); 11 | path.traverse(classDeclarationVisitor); 12 | } 13 | }); 14 | return generate(ast).code; 15 | } 16 | 17 | module.exports = hookd; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // specifies environment variables and other options 2 | module.exports = { 3 | "verbose": true, 4 | "roots": [ 5 | "/tests" 6 | ], 7 | "transform": { 8 | "^.+\\.tsx?$": "ts-jest" 9 | }, 10 | "setupFilesAfterEnv": ["/tests/jest-preload.js"], 11 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodemonConfig": { 3 | "ignore": [ 4 | "ast.jsx", 5 | "node_modules", 6 | "test/*", 7 | "static" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactionaries/hookd", 3 | "version": "0.0.25", 4 | "description": "a cli tool to convert React class components to hooks", 5 | "scripts": { 6 | "build:ind": "NODE_ENV=production BUILD=index webpack", 7 | "build:cli": "NODE_ENV=production BUILD=cli webpack", 8 | "dev": "NODE_ENV=development nodemon index.ts static/dummyData/App.jsx", 9 | "test": "jest" 10 | }, 11 | "keywords": [], 12 | "author": "Devon Vaccarino, Danny Byrne, Alexander Diep", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.5.5", 16 | "@babel/preset-env": "^7.5.5", 17 | "@babel/preset-react": "^7.0.0", 18 | "@babel/preset-typescript": "^7.3.3", 19 | "@types/jest": "^24.0.18", 20 | "@types/react": "^16.9.2", 21 | "@types/react-dom": "^16.9.0", 22 | "babel-loader": "^8.0.6", 23 | "banner-webpack-plugin": "^0.2.3", 24 | "css-loader": "^3.2.0", 25 | "jest": "^24.9.0", 26 | "nodemon": "^1.19.1", 27 | "sass-loader": "^7.2.0", 28 | "style-loader": "^1.0.0", 29 | "ts-jest": "^24.0.2", 30 | "ts-loader": "^6.0.4", 31 | "ts-node": "^8.3.0", 32 | "typescript": "^3.5.3", 33 | "webpack": "^4.39.3", 34 | "webpack-cli": "^3.3.7", 35 | "webpack-dev-server": "^3.8.0" 36 | }, 37 | "dependencies": { 38 | "@babel/generator": "^7.5.5", 39 | "@babel/parser": "^7.5.5", 40 | "@babel/traverse": "^7.5.5", 41 | "@babel/types": "^7.5.5", 42 | "@types/node": "^12.7.2", 43 | "babel-generator": "^6.26.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/hookd-cli/README.md: -------------------------------------------------------------------------------- 1 | # @reactionaries/hookd-cli 2 | To install and use in `package.json` 3 | - `npm i -D @reactionaries/hookd-cli` into local repository 4 | - make a `package.json` script i.e. 5 | - `"hookd": "hookd "` 6 | 7 | or to use globally 8 | - `npm i -g @reactionaries/hookd-cli` 9 | - and run `hookd` command anywhere 10 | 11 | and the file will be outputted to the created `/hookd` directory folder in the current directory -------------------------------------------------------------------------------- /packages/hookd-cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactionaries/hookd-cli", 3 | "version": "0.0.26", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.5.5", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", 10 | "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", 11 | "requires": { 12 | "@babel/highlight": "^7.0.0" 13 | } 14 | }, 15 | "@babel/generator": { 16 | "version": "7.6.0", 17 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", 18 | "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", 19 | "requires": { 20 | "@babel/types": "^7.6.0", 21 | "jsesc": "^2.5.1", 22 | "lodash": "^4.17.13", 23 | "source-map": "^0.5.0", 24 | "trim-right": "^1.0.1" 25 | } 26 | }, 27 | "@babel/helper-function-name": { 28 | "version": "7.1.0", 29 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", 30 | "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", 31 | "requires": { 32 | "@babel/helper-get-function-arity": "^7.0.0", 33 | "@babel/template": "^7.1.0", 34 | "@babel/types": "^7.0.0" 35 | } 36 | }, 37 | "@babel/helper-get-function-arity": { 38 | "version": "7.0.0", 39 | "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", 40 | "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", 41 | "requires": { 42 | "@babel/types": "^7.0.0" 43 | } 44 | }, 45 | "@babel/helper-split-export-declaration": { 46 | "version": "7.4.4", 47 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", 48 | "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", 49 | "requires": { 50 | "@babel/types": "^7.4.4" 51 | } 52 | }, 53 | "@babel/highlight": { 54 | "version": "7.5.0", 55 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", 56 | "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", 57 | "requires": { 58 | "chalk": "^2.0.0", 59 | "esutils": "^2.0.2", 60 | "js-tokens": "^4.0.0" 61 | } 62 | }, 63 | "@babel/parser": { 64 | "version": "7.5.5", 65 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", 66 | "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==" 67 | }, 68 | "@babel/template": { 69 | "version": "7.6.0", 70 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", 71 | "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", 72 | "requires": { 73 | "@babel/code-frame": "^7.0.0", 74 | "@babel/parser": "^7.6.0", 75 | "@babel/types": "^7.6.0" 76 | }, 77 | "dependencies": { 78 | "@babel/parser": { 79 | "version": "7.6.0", 80 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", 81 | "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" 82 | } 83 | } 84 | }, 85 | "@babel/traverse": { 86 | "version": "7.6.0", 87 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", 88 | "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", 89 | "requires": { 90 | "@babel/code-frame": "^7.5.5", 91 | "@babel/generator": "^7.6.0", 92 | "@babel/helper-function-name": "^7.1.0", 93 | "@babel/helper-split-export-declaration": "^7.4.4", 94 | "@babel/parser": "^7.6.0", 95 | "@babel/types": "^7.6.0", 96 | "debug": "^4.1.0", 97 | "globals": "^11.1.0", 98 | "lodash": "^4.17.13" 99 | }, 100 | "dependencies": { 101 | "@babel/parser": { 102 | "version": "7.6.0", 103 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", 104 | "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" 105 | } 106 | } 107 | }, 108 | "@babel/types": { 109 | "version": "7.6.1", 110 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", 111 | "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", 112 | "requires": { 113 | "esutils": "^2.0.2", 114 | "lodash": "^4.17.13", 115 | "to-fast-properties": "^2.0.0" 116 | } 117 | }, 118 | "@types/node": { 119 | "version": "12.7.5", 120 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz", 121 | "integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==" 122 | }, 123 | "ansi-styles": { 124 | "version": "3.2.1", 125 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 126 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 127 | "requires": { 128 | "color-convert": "^1.9.0" 129 | } 130 | }, 131 | "babel-generator": { 132 | "version": "6.26.1", 133 | "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", 134 | "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", 135 | "requires": { 136 | "babel-messages": "^6.23.0", 137 | "babel-runtime": "^6.26.0", 138 | "babel-types": "^6.26.0", 139 | "detect-indent": "^4.0.0", 140 | "jsesc": "^1.3.0", 141 | "lodash": "^4.17.4", 142 | "source-map": "^0.5.7", 143 | "trim-right": "^1.0.1" 144 | }, 145 | "dependencies": { 146 | "jsesc": { 147 | "version": "1.3.0", 148 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", 149 | "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" 150 | } 151 | } 152 | }, 153 | "babel-messages": { 154 | "version": "6.23.0", 155 | "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", 156 | "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", 157 | "requires": { 158 | "babel-runtime": "^6.22.0" 159 | } 160 | }, 161 | "babel-runtime": { 162 | "version": "6.26.0", 163 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 164 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 165 | "requires": { 166 | "core-js": "^2.4.0", 167 | "regenerator-runtime": "^0.11.0" 168 | } 169 | }, 170 | "babel-types": { 171 | "version": "6.26.0", 172 | "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", 173 | "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", 174 | "requires": { 175 | "babel-runtime": "^6.26.0", 176 | "esutils": "^2.0.2", 177 | "lodash": "^4.17.4", 178 | "to-fast-properties": "^1.0.3" 179 | }, 180 | "dependencies": { 181 | "to-fast-properties": { 182 | "version": "1.0.3", 183 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", 184 | "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" 185 | } 186 | } 187 | }, 188 | "chalk": { 189 | "version": "2.4.2", 190 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 191 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 192 | "requires": { 193 | "ansi-styles": "^3.2.1", 194 | "escape-string-regexp": "^1.0.5", 195 | "supports-color": "^5.3.0" 196 | } 197 | }, 198 | "color-convert": { 199 | "version": "1.9.3", 200 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 201 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 202 | "requires": { 203 | "color-name": "1.1.3" 204 | } 205 | }, 206 | "color-name": { 207 | "version": "1.1.3", 208 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 209 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 210 | }, 211 | "core-js": { 212 | "version": "2.6.9", 213 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", 214 | "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" 215 | }, 216 | "debug": { 217 | "version": "4.1.1", 218 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 219 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 220 | "requires": { 221 | "ms": "^2.1.1" 222 | } 223 | }, 224 | "detect-indent": { 225 | "version": "4.0.0", 226 | "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", 227 | "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", 228 | "requires": { 229 | "repeating": "^2.0.0" 230 | } 231 | }, 232 | "escape-string-regexp": { 233 | "version": "1.0.5", 234 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 235 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 236 | }, 237 | "esutils": { 238 | "version": "2.0.3", 239 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 240 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" 241 | }, 242 | "globals": { 243 | "version": "11.12.0", 244 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 245 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" 246 | }, 247 | "has-flag": { 248 | "version": "3.0.0", 249 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 250 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 251 | }, 252 | "is-finite": { 253 | "version": "1.0.2", 254 | "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", 255 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 256 | "requires": { 257 | "number-is-nan": "^1.0.0" 258 | } 259 | }, 260 | "js-tokens": { 261 | "version": "4.0.0", 262 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 263 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 264 | }, 265 | "jsesc": { 266 | "version": "2.5.2", 267 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", 268 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" 269 | }, 270 | "lodash": { 271 | "version": "4.17.15", 272 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 273 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 274 | }, 275 | "ms": { 276 | "version": "2.1.2", 277 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 278 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 279 | }, 280 | "number-is-nan": { 281 | "version": "1.0.1", 282 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 283 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 284 | }, 285 | "regenerator-runtime": { 286 | "version": "0.11.1", 287 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 288 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 289 | }, 290 | "repeating": { 291 | "version": "2.0.1", 292 | "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", 293 | "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", 294 | "requires": { 295 | "is-finite": "^1.0.0" 296 | } 297 | }, 298 | "source-map": { 299 | "version": "0.5.7", 300 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 301 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 302 | }, 303 | "supports-color": { 304 | "version": "5.5.0", 305 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 306 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 307 | "requires": { 308 | "has-flag": "^3.0.0" 309 | } 310 | }, 311 | "to-fast-properties": { 312 | "version": "2.0.0", 313 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 314 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" 315 | }, 316 | "trim-right": { 317 | "version": "1.0.1", 318 | "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", 319 | "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /packages/hookd-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactionaries/hookd-cli", 3 | "version": "0.0.27", 4 | "description": "a cli tool to convert React class components to hooks", 5 | "keywords": [ 6 | "compiler", 7 | "cli", 8 | "tool", 9 | "alpha", 10 | "React", 11 | "React Hooks" 12 | ], 13 | "author": "Devon Vaccarino, Danny Byrne, Alexander Diep", 14 | "license": "ISC", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/Hookd.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/Hookd/issues" 21 | }, 22 | "bin": { 23 | "hookd": "./cli.js" 24 | }, 25 | "dependencies": { 26 | "@babel/generator": "^7.5.5", 27 | "@babel/parser": "^7.5.5", 28 | "@babel/traverse": "^7.5.5", 29 | "@babel/types": "^7.5.5", 30 | "@types/node": "^12.7.2", 31 | "babel-generator": "^6.26.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/hookd/README.md: -------------------------------------------------------------------------------- 1 | # @reactionaries/hookd 2 | Hookd is a module converting JSX class components to functional components 3 | 4 | To install: 5 | 6 | `npm i @reactionaries/hookd` 7 | 8 | To use: 9 | ``` 10 | const hookd = require('@reactionaries/hookd'); 11 | 12 | // returns a functional component as a string 13 | hookd(someClassComponentAsAString) 14 | ``` -------------------------------------------------------------------------------- /packages/hookd/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactionaries/hookd-cli", 3 | "version": "0.0.25", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.5.5", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", 10 | "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", 11 | "requires": { 12 | "@babel/highlight": "^7.0.0" 13 | } 14 | }, 15 | "@babel/generator": { 16 | "version": "7.6.0", 17 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", 18 | "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", 19 | "requires": { 20 | "@babel/types": "^7.6.0", 21 | "jsesc": "^2.5.1", 22 | "lodash": "^4.17.13", 23 | "source-map": "^0.5.0", 24 | "trim-right": "^1.0.1" 25 | } 26 | }, 27 | "@babel/helper-function-name": { 28 | "version": "7.1.0", 29 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", 30 | "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", 31 | "requires": { 32 | "@babel/helper-get-function-arity": "^7.0.0", 33 | "@babel/template": "^7.1.0", 34 | "@babel/types": "^7.0.0" 35 | } 36 | }, 37 | "@babel/helper-get-function-arity": { 38 | "version": "7.0.0", 39 | "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", 40 | "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", 41 | "requires": { 42 | "@babel/types": "^7.0.0" 43 | } 44 | }, 45 | "@babel/helper-split-export-declaration": { 46 | "version": "7.4.4", 47 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", 48 | "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", 49 | "requires": { 50 | "@babel/types": "^7.4.4" 51 | } 52 | }, 53 | "@babel/highlight": { 54 | "version": "7.5.0", 55 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", 56 | "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", 57 | "requires": { 58 | "chalk": "^2.0.0", 59 | "esutils": "^2.0.2", 60 | "js-tokens": "^4.0.0" 61 | } 62 | }, 63 | "@babel/parser": { 64 | "version": "7.5.5", 65 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", 66 | "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==" 67 | }, 68 | "@babel/template": { 69 | "version": "7.6.0", 70 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", 71 | "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", 72 | "requires": { 73 | "@babel/code-frame": "^7.0.0", 74 | "@babel/parser": "^7.6.0", 75 | "@babel/types": "^7.6.0" 76 | }, 77 | "dependencies": { 78 | "@babel/parser": { 79 | "version": "7.6.0", 80 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", 81 | "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" 82 | } 83 | } 84 | }, 85 | "@babel/traverse": { 86 | "version": "7.6.0", 87 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", 88 | "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", 89 | "requires": { 90 | "@babel/code-frame": "^7.5.5", 91 | "@babel/generator": "^7.6.0", 92 | "@babel/helper-function-name": "^7.1.0", 93 | "@babel/helper-split-export-declaration": "^7.4.4", 94 | "@babel/parser": "^7.6.0", 95 | "@babel/types": "^7.6.0", 96 | "debug": "^4.1.0", 97 | "globals": "^11.1.0", 98 | "lodash": "^4.17.13" 99 | }, 100 | "dependencies": { 101 | "@babel/parser": { 102 | "version": "7.6.0", 103 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", 104 | "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==" 105 | } 106 | } 107 | }, 108 | "@babel/types": { 109 | "version": "7.6.1", 110 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", 111 | "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", 112 | "requires": { 113 | "esutils": "^2.0.2", 114 | "lodash": "^4.17.13", 115 | "to-fast-properties": "^2.0.0" 116 | } 117 | }, 118 | "@types/node": { 119 | "version": "12.7.5", 120 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz", 121 | "integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==" 122 | }, 123 | "ansi-styles": { 124 | "version": "3.2.1", 125 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 126 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 127 | "requires": { 128 | "color-convert": "^1.9.0" 129 | } 130 | }, 131 | "babel-generator": { 132 | "version": "6.26.1", 133 | "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", 134 | "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", 135 | "requires": { 136 | "babel-messages": "^6.23.0", 137 | "babel-runtime": "^6.26.0", 138 | "babel-types": "^6.26.0", 139 | "detect-indent": "^4.0.0", 140 | "jsesc": "^1.3.0", 141 | "lodash": "^4.17.4", 142 | "source-map": "^0.5.7", 143 | "trim-right": "^1.0.1" 144 | }, 145 | "dependencies": { 146 | "jsesc": { 147 | "version": "1.3.0", 148 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", 149 | "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" 150 | } 151 | } 152 | }, 153 | "babel-messages": { 154 | "version": "6.23.0", 155 | "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", 156 | "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", 157 | "requires": { 158 | "babel-runtime": "^6.22.0" 159 | } 160 | }, 161 | "babel-runtime": { 162 | "version": "6.26.0", 163 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 164 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 165 | "requires": { 166 | "core-js": "^2.4.0", 167 | "regenerator-runtime": "^0.11.0" 168 | } 169 | }, 170 | "babel-types": { 171 | "version": "6.26.0", 172 | "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", 173 | "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", 174 | "requires": { 175 | "babel-runtime": "^6.26.0", 176 | "esutils": "^2.0.2", 177 | "lodash": "^4.17.4", 178 | "to-fast-properties": "^1.0.3" 179 | }, 180 | "dependencies": { 181 | "to-fast-properties": { 182 | "version": "1.0.3", 183 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", 184 | "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" 185 | } 186 | } 187 | }, 188 | "chalk": { 189 | "version": "2.4.2", 190 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 191 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 192 | "requires": { 193 | "ansi-styles": "^3.2.1", 194 | "escape-string-regexp": "^1.0.5", 195 | "supports-color": "^5.3.0" 196 | } 197 | }, 198 | "color-convert": { 199 | "version": "1.9.3", 200 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 201 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 202 | "requires": { 203 | "color-name": "1.1.3" 204 | } 205 | }, 206 | "color-name": { 207 | "version": "1.1.3", 208 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 209 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 210 | }, 211 | "core-js": { 212 | "version": "2.6.9", 213 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", 214 | "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" 215 | }, 216 | "debug": { 217 | "version": "4.1.1", 218 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 219 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 220 | "requires": { 221 | "ms": "^2.1.1" 222 | } 223 | }, 224 | "detect-indent": { 225 | "version": "4.0.0", 226 | "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", 227 | "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", 228 | "requires": { 229 | "repeating": "^2.0.0" 230 | } 231 | }, 232 | "escape-string-regexp": { 233 | "version": "1.0.5", 234 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 235 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 236 | }, 237 | "esutils": { 238 | "version": "2.0.3", 239 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 240 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" 241 | }, 242 | "globals": { 243 | "version": "11.12.0", 244 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 245 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" 246 | }, 247 | "has-flag": { 248 | "version": "3.0.0", 249 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 250 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 251 | }, 252 | "is-finite": { 253 | "version": "1.0.2", 254 | "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", 255 | "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", 256 | "requires": { 257 | "number-is-nan": "^1.0.0" 258 | } 259 | }, 260 | "js-tokens": { 261 | "version": "4.0.0", 262 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 263 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 264 | }, 265 | "jsesc": { 266 | "version": "2.5.2", 267 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", 268 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" 269 | }, 270 | "lodash": { 271 | "version": "4.17.15", 272 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 273 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 274 | }, 275 | "ms": { 276 | "version": "2.1.2", 277 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 278 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 279 | }, 280 | "number-is-nan": { 281 | "version": "1.0.1", 282 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 283 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 284 | }, 285 | "regenerator-runtime": { 286 | "version": "0.11.1", 287 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 288 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 289 | }, 290 | "repeating": { 291 | "version": "2.0.1", 292 | "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", 293 | "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", 294 | "requires": { 295 | "is-finite": "^1.0.0" 296 | } 297 | }, 298 | "source-map": { 299 | "version": "0.5.7", 300 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 301 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 302 | }, 303 | "supports-color": { 304 | "version": "5.5.0", 305 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 306 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 307 | "requires": { 308 | "has-flag": "^3.0.0" 309 | } 310 | }, 311 | "to-fast-properties": { 312 | "version": "2.0.0", 313 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 314 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" 315 | }, 316 | "trim-right": { 317 | "version": "1.0.1", 318 | "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", 319 | "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /packages/hookd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactionaries/hookd", 3 | "version": "0.0.27", 4 | "description": "a module to convert React class components to hooks", 5 | "keywords": [ 6 | "compiler", 7 | "cli", 8 | "tool", 9 | "alpha", 10 | "React", 11 | "React Hooks" 12 | ], 13 | "author": "Devon Vaccarino, Danny Byrne, Alexander Diep", 14 | "license": "ISC", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/Hookd.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/Hookd/issues" 21 | }, 22 | "dependencies": { 23 | "@babel/generator": "^7.5.5", 24 | "@babel/parser": "^7.5.5", 25 | "@babel/traverse": "^7.5.5", 26 | "@babel/types": "^7.5.5", 27 | "@types/node": "^12.7.2", 28 | "babel-generator": "^6.26.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing with Hookd 2 | Run `npm t` to test with Jest. 3 | - Presets and configs will be found in `./jest-preload.js` and `../jest.config.js` 4 | ## Modularity of Testing 5 | - Ability to test visitors, but visitors themselves are difficult to modularize as they do depend on each other to work 6 | - Testing helper functions TBD 7 | ## Issues 8 | - Overall, there are ENOUGH edge cases with this app to go around. 9 | - Inside the test `/components` will have commented out edge cases until accounted for, and `xit`s within the `test.ts` files themselves. 10 | - Tests are going to be modularized but the `../util` will be *TBD* 11 | -------------------------------------------------------------------------------- /tests/jest-preload.js: -------------------------------------------------------------------------------- 1 | // got from stack overflow... file can be named anything as long as '/jest.config.js' points to it 2 | // point of this file is to ignore console logs, but keep other console err/warns/etc 3 | // file is setup by '../jest.config.js' 4 | global.console = { 5 | log: jest.fn(), // console.log are ignored in tests 6 | 7 | // Keep native behaviour for other methods, use those to print out things in your own tests, not `console.log` 8 | error: console.error, 9 | warn: console.warn, 10 | info: console.info, 11 | debug: console.debug, 12 | }; -------------------------------------------------------------------------------- /tests/testHelperFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Path, Node } from "../util/constants/interfaces"; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { parse, traverse, generate, t} from '../util/constants/parser' 5 | 6 | /** 7 | * Will parse, traverse, and generate code (a string of transformed code) depending on the visitors you give it 8 | * filePath will be relative to this file's directory 9 | * @param filePath The path of file to parse, traverse and generate code from 10 | * @param arrVisitors The visitors to use as an array 11 | */ 12 | export function ptg(filePath: string, arrVisitors: object[]): string { 13 | // parse 14 | const ast = parse(fs.readFileSync(path.resolve(__dirname, filePath), 'utf-8') as string); 15 | // traverse 16 | traverse(ast, { 17 | enter(path: Path) { 18 | for (let i = 0; i < arrVisitors.length; i++) path.traverse(arrVisitors[i]) 19 | } 20 | }) 21 | // generate 22 | return generate(ast).code; 23 | } -------------------------------------------------------------------------------- /tests/unit/components/ClassToFunction.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Test1 extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = {} 7 | } 8 | render(){ 9 | return ( 10 |
11 | ); 12 | } 13 | } 14 | 15 | class Test2 extends Component { 16 | render(){ 17 | return ( 18 |

19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/unit/components/ConstructorToHooks.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Test1 extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = {} 7 | } 8 | render() { 9 | return
10 | } 11 | } 12 | 13 | class Test2 extends Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | prop: 'properties', 18 | obj: { 19 | this: 'exists' 20 | }, 21 | arr: [ 22 | { 23 | num: 1337, 24 | wow: "I'm cool" 25 | } 26 | ] 27 | } 28 | } 29 | render(){ 30 | return ( 31 |
32 | {this.state.prop}; 33 |
34 | ) 35 | } 36 | } 37 | 38 | /** 39 | * uncomment when class properties have been accounted for 40 | */ 41 | // class Test3 extends React.Component { 42 | // state = { 43 | // short: 'syntax for constructor' 44 | // } 45 | // } -------------------------------------------------------------------------------- /tests/unit/components/StateToHooks.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Test1 extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = { 7 | prop: 'this is ideation week' 8 | } 9 | this.handler = this.handler1.bind(this); 10 | } 11 | 12 | handler(){ 13 | this.setState({ 14 | prop: 'this is another idea' 15 | }) 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | {this.state.prop} 22 |
23 | ) 24 | } 25 | } 26 | 27 | /** 28 | * uncomment if accounting for this.setState callback argument 29 | */ 30 | class Test2 extends Component { 31 | constructor(props) { 32 | super(props) 33 | this.state = { 34 | ohno: 'this is not accounted for' 35 | } 36 | this.handler = this.handler.bind(this); 37 | } 38 | handler(){ 39 | this.setState(() => { 40 | const str = 'because this is another edge case out of many'; 41 | return { ohno: str }; 42 | }) 43 | } 44 | render() { 45 | return ( 46 |
47 | {this.state.ohno} 48 |
49 | ) 50 | } 51 | } 52 | 53 | class Test3 extends Component { 54 | constructor(props) { 55 | super(props) 56 | this.state = { 57 | holy: 'cow', 58 | ohno: 'wow' 59 | } 60 | this.handler = this.handler.bind(this); 61 | } 62 | handler(){ 63 | this.setState((prevProps) => { 64 | const str1 = prevProps.holy + ' because this is another edge case out of many'; 65 | return { ohno: str1 }; 66 | }) 67 | } 68 | render() { 69 | return ( 70 |
71 | {this.state.ohno} 72 |
73 | ) 74 | } 75 | } 76 | // // Use Effect logic is not accounting for this right now but works with use state 77 | // class Test4 extends Component { 78 | // constructor(props) { 79 | // super(props) 80 | // this.state = { 81 | // nice: 'job' 82 | // } 83 | // this.handler = this.handler.bind(this); 84 | // } 85 | // handler(){ 86 | // this.setState((state) => ({ nice: state.nice + 's' })) 87 | // } 88 | // render() { 89 | // return ( 90 | //
91 | // {this.state.nice} 92 | //
93 | // ) 94 | // } 95 | // } -------------------------------------------------------------------------------- /tests/unit/components/UCfragment.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import UserWrapper from './containers/UserWrapper'; 3 | import NameContext from './context'; 4 | 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | user: [{fname: 'fred', lname: 'flintstone'}], 10 | isAuthenticated: false 11 | } 12 | } 13 | 14 | 15 | render() { 16 | return ( 17 | 18 |
19 | {(value) =>
{value}
} 20 |

first name: {this.state.user[0].fName}

21 |
22 | 23 |
24 | ); 25 | } 26 | } 27 | 28 | export default App; -------------------------------------------------------------------------------- /tests/unit/components/UCmultiple.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ThemeContext } from '../contexts/ThemeContext'; 3 | import { AuthContext } from '../contexts/AuthContext'; 4 | 5 | class Navbar extends Component { 6 | render() { 7 | return ( 8 | {(authContext) => ( 9 | {(themeContext) => { 10 | const { isAuthenticated, toggleAuth } = authContext; 11 | const { isLightTheme, light, dark } = themeContext; 12 | const theme = isLightTheme ? light : dark; 13 | return ( 14 | 25 | ) 26 | }} 27 | )} 28 | ); 29 | } 30 | } 31 | 32 | export default Navbar; -------------------------------------------------------------------------------- /tests/unit/components/UCstatic.jsx: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from '../contexts/ThemeContext'; 2 | 3 | class List extends Component { 4 | static contextType = ThemeContext; 5 | render() { 6 | const { isLightTheme, light, dark } = this.context; 7 | } 8 | } 9 | 10 | export default List; -------------------------------------------------------------------------------- /tests/unit/components/UEwCDM.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class UEwCDM1 extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = {} 7 | } 8 | 9 | componentDidMount() { 10 | document.title = this.state.bookName; 11 | } 12 | 13 | render(){ 14 | return ( 15 |
16 | ); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /tests/unit/components/UEwCDU.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class UEwCDU extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = {} 7 | } 8 | 9 | componentDidMount() { 10 | document.title = this.state.bookName; 11 | console.log(this.state.isAuthenticated); 12 | } 13 | 14 | componentDidUpdate() { 15 | console.log(this.state.isAuthenticated); 16 | } 17 | 18 | render(){ 19 | return ( 20 |
21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/unit/components/UEwCWU.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class UEwCWU extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = {} 7 | } 8 | 9 | componentDidMount() { 10 | document.title = this.state.bookName; 11 | } 12 | 13 | componentDidUpdate() { 14 | console.log(this.state.isAuthenticated); 15 | } 16 | 17 | componentWillUnmount() { 18 | clearInterval(this.state.interval); 19 | } 20 | render(){ 21 | return ( 22 |
23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/unit/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Path, Node } from "../../util/constants/interfaces"; 2 | import * as visitors from '../../util/constants/visitors'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { parse, traverse, generate, t} from '../../util/constants/parser' 6 | import { ptg } from '../testHelperFunctions' 7 | 8 | /** 9 | * Commented out are initial tests using shell.js to asynchronously 10 | * manually run 'hookd' with a file and check the syntax of newly created file 11 | * 12 | * May use for end-to-end testing 13 | */ 14 | // const sh = require('shelljs'); 15 | // const cli = require('../index.ts'); 16 | // initial tree parsing 17 | // (async ()=>{ 18 | // await sh.cd(path.resolve(__dirname)) 19 | // if (!fs.existsSync('index.js')) await sh.exec('npm run build') 20 | // // await sh.exec('npm link') 21 | // await sh.exec('hookd ./ClassToFunction.jsx') 22 | // // checks for the changing of class to functional component 23 | // })() 24 | 25 | /** 26 | * - Jest is a small doozy to wrap your head around for a day or two learning curve - 27 | * describe will synchronously deploy, so describe (EYY) what you want to do within them 28 | * 'it' === 'test'. different syntax, same functionality 29 | * 'ptg' rocks. self-made function. 30 | * use shelljs later on to actually test the 'hookd' cli 31 | */ 32 | 33 | describe('turns class components to functional components', () => { 34 | /* 35 | const ast: object = parse(fs.readFileSync(path.resolve(__dirname, './ClassToFunction.jsx'), 'utf-8') as string); 36 | traverse(ast as object, visitors.classDeclarationVisitor as object); 37 | const str: string = generate(ast).code as string; 38 | */ 39 | // above it similar to the 'ptg' one-liner below 40 | const str = ptg('./unit/components/ClassToFunction.jsx', [visitors.classDeclarationVisitor]); 41 | it('Should turn class components to arrow functions', () => { 42 | expect(str).toMatch(/const Test1 \= props \=\>/); 43 | expect(str).toMatch('const Test2 = () =>') 44 | }) 45 | xit('Should ignore classes that are not components', () => { 46 | // should there be a reason there would be a class declaration inside a jsx file? 47 | // find out next time in 'Testing 123s' 48 | expect(str).toBeDefined(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/useContext.test.ts: -------------------------------------------------------------------------------- 1 | import * as visitors from '../../util/constants/visitors'; 2 | import { ptg } from '../testHelperFunctions' 3 | 4 | describe('Test should be running', () => { 5 | it('Should test', () => { 6 | expect(true).toBe(true) 7 | }) 8 | }) 9 | 10 | describe('Handles cases in which multiple Contexts have been passed in', () => { 11 | const str :string = ptg('./unit/components/UCmultiple.jsx', [visitors.ImpDeclVisitor, visitors.classDeclarationVisitor]); 12 | console.log(str); 13 | //we have to fix the multiple context consumer tags that was broken in the last "fix" we did before deploy 14 | xit('') 15 | }) 16 | 17 | describe('Handles cases in which a single context has been passed in statically', () => { 18 | const ucStatic: string = './unit/components/UCstatic.jsx'; 19 | const str: string = ptg(ucStatic, [visitors.ImpSpecVisitor, visitors.ImpDeclVisitor, visitors.memberExpVisitor, visitors.classDeclarationVisitor]); 20 | expect(str).toMatch( 21 | `import { ThemeContext } from '../contexts/ThemeContext'; 22 | 23 | const List = () => { 24 | const { 25 | isLightTheme, 26 | light, 27 | dark 28 | } = useContext(ThemeContext); 29 | }; 30 | export default List;`); 31 | }) 32 | 33 | describe('Handles Context.Consumer tags', () => { 34 | const ucFragment: string = './unit/components/UCfragment.jsx'; 35 | it('replaces .Consumer tags with React.Fragment', () => {s 36 | const str: string = ptg(ucFragment, [visitors.ImpSpecVisitor, visitors.ImpDeclVisitor, visitors.memberExpVisitor, visitors.classDeclarationVisitor]); 37 | expect(str).toMatch(`import React, { useState, useEffect, useContext } from 'react'; 38 | import UserWrapper from './containers/UserWrapper'; 39 | import NameContext from './context'; 40 | 41 | const App = props => { 42 | const importedNameContext = useContext(NameContext); 43 | const [isAuthenticated, setIsAuthenticated] = useState(false); 44 | const [user, setUser] = useState([{ 45 | fname: 'fred', 46 | lname: 'flintstone' 47 | }]); 48 | return 49 |
50 | {importedNameContext} 51 |

first name: {user[0].fName}

52 |
53 | 54 |
; 55 | }; 56 | 57 | export default App;`); 58 | }); 59 | }) 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/unit/useEffect.test.ts: -------------------------------------------------------------------------------- 1 | import * as visitors from '../../util/constants/visitors'; 2 | import {ptg} from '../testHelperFunctions'; 3 | 4 | describe('The first useEffect case should combine logic from componentDidMount into a useEffect hook without a dependency array or return statement', () => { 5 | const pathCDM: string = './unit/components/UEwCDM.jsx'; 6 | const cdmStr: string = ptg(pathCDM, [visitors.classDeclarationVisitor]); 7 | it('Only creates a useEffect hook if we do not setState', () => { 8 | expect(cdmStr).toMatch(` 9 | useEffect(() => { 10 | document.title = bookName; 11 | });`) 12 | }); 13 | it('Has no dependency array when there is no componentDidUpdate', () => { 14 | expect(cdmStr).not.toMatch(` 15 | useEffect(() => { 16 | document.title = bookName; 17 | }, []);`); 18 | }); 19 | const pathCDU: string = './unit/components/UEwCDU.jsx'; 20 | const cduStr: string = ptg(pathCDU, [visitors.classDeclarationVisitor]); 21 | it('Has no dependency array when there is not stateful reference within componentDidUpdate', () => { 22 | expect(cduStr).toMatch(`useEffect(() => { 23 | document.title = bookName; 24 | })`); 25 | expect(cduStr).not.toMatch( 26 | `useEffect(() => { 27 | document.title = bookName; 28 | }, [isAuthenticated])`); 29 | }); 30 | const pathCWU: string = './unit/components/UEwCWU.jsx'; 31 | const cwuStr: string = ptg(pathCWU, [visitors.classDeclarationVisitor]); 32 | it('Has no return statement when there is no stateful reference within componentWillUnmount', () => { 33 | expect(cwuStr).not.toMatch(`useEffect(() => { 34 | document.title = bookName; 35 | return () => { 36 | clearInterval(interval); 37 | } 38 | })`) 39 | }); 40 | it('Makes multiple useEffect hooks for each stateful reference', () => { 41 | expect(cdmStr).toMatch( 42 | `useEffect(() => { 43 | console.log(test2); 44 | });`); 45 | 46 | expect(cdmStr).toMatch( 47 | `useEffect(() => { 48 | console.log(test3); 49 | });`) 50 | }); 51 | }) -------------------------------------------------------------------------------- /tests/unit/useState.test.ts: -------------------------------------------------------------------------------- 1 | import { Path, Node } from "../../util/constants/interfaces"; 2 | import * as visitors from '../../util/constants/visitors'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { parse, traverse, generate, t} from '../../util/constants/parser' 6 | import { ptg } from '../testHelperFunctions' 7 | 8 | describe('Should be passing', () => { 9 | it('Should pass', () => { 10 | expect(true).toBe(true) 11 | }) 12 | }) 13 | 14 | describe('Turns state in a class method to useState equivalents', () => { 15 | /* 16 | const ast = parse(fs.readFileSync(path.resolve(__dirname, './ConstructorToHooks.jsx'), 'utf-8') as string); 17 | traverse(ast as object, { 18 | enter(path: Path) { 19 | path.traverse(visitors.ImpDeclVisitor); 20 | path.traverse(visitors.classDeclarationVisitor); 21 | } 22 | }); 23 | */ 24 | // above is equivalent to below 'ptg' function 25 | const str = ptg('./unit/components/ConstructorToHooks.jsx', [visitors.ImpDeclVisitor, visitors.classDeclarationVisitor]); 26 | 27 | it('Converts state into useState equivalents', () => { 28 | expect(str).toMatch(`const [prop, setProp] = useState('properties')`); 29 | expect(str).toMatch(`const [obj, setObj]`); 30 | }) 31 | xit(`Doesn't define any state for pure components`, ()=>{ 32 | // find a way to check a node for no state declaration 33 | // also fine not to check as well 34 | }) 35 | xit(`Converts class properties 'state' to 'useState'`, () => { 36 | // find a way to convert class property state to hooks 37 | expect(str).toMatch(`const [short, setShort] = useState('syntax for constructor')`) 38 | }) 39 | xit(`Catches initialization of state within mounting or handlers`, () => { 40 | // jesus christ is this necessary 41 | }) 42 | }) 43 | 44 | describe(`Should convert 'this.(set)State' expressions`, () => { 45 | const str = ptg('./unit/components/StateToHooks.jsx', [visitors.ImpDeclVisitor, visitors.classDeclarationVisitor]); 46 | it(`Should change 'this.state.prop' to 'prop'`, () => { 47 | expect(str).toMatch('{prop}'); 48 | }); 49 | it(`Should change 'this.handler' to 'handler'`, () => { 50 | expect(str).toMatch('onClick={handler}') 51 | }); 52 | it(`Should change this.setState({ prop: 'a string'}) to 'setProp('a string')'`, () => { 53 | expect(str).toMatch(`setProp('this is another idea')`); 54 | }) 55 | it(`Should account for 'this.setState(() => {... return {}})'`, () => { 56 | // accounting without arg in anon func 57 | expect(str).toMatch(`const str = 'because this is another edge case out of many';`) 58 | expect(str).toMatch(`setOhno(str)`) 59 | }) 60 | it(`Should account for 'this.setState((prevProps) => {... return {}})`, () => { 61 | // accounting for arg in anon func 62 | expect(str).toMatch(`const str1 = holy + ' because this is another edge case out of many';\n setOhno(str1);`) 63 | }) 64 | xit(`Accounts for this.setState((state) => ({ state }))`, () => { 65 | // UE logic needs ANOTHER edge case 66 | expect(str).toMatch(`setNice(nice + 's')`) 67 | }) 68 | xit('Should account for this.setState(callback)', () => { 69 | // how to account for, find and change locally defined functions locally and appropriately 70 | }) 71 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "jsx": "react" 8 | } 9 | } -------------------------------------------------------------------------------- /util/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Hookd/8469808f6e82a50706e4935916b85d82c0233a4b/util/.DS_Store -------------------------------------------------------------------------------- /util/constants/interfaces.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Node { 3 | type: string; 4 | name: string; 5 | Identifier: any; 6 | body: {body: any[], type: string, properties: Node[]}; 7 | params: any[]; 8 | key: {name: string}; 9 | arguments: any[]; 10 | property: {name: string}; 11 | local: {name: string}; 12 | object: {name: string}; 13 | specifiers: {name: string}; 14 | superClass: {name: string}; 15 | operator:{body: any[]} 16 | program: {program: any}; 17 | declaration: any; 18 | static: boolean; 19 | id: any; 20 | properties: any[]; 21 | value: {name: string}; 22 | init: {name: string}; 23 | callee: any; 24 | argument: Node; 25 | } 26 | export interface Path { 27 | node: Node; 28 | traverse: ({}) => any; 29 | stop: () => void; 30 | skip: () => void; 31 | replaceWithMultiple: ([]:any) => any; 32 | replaceWith: (newNode: any) => any; 33 | findParent: (callback: (path: Path) => any) => any; 34 | isImportSpecifier: () => any; 35 | getMemberExpressionParent: () => any; 36 | isMemberExpression: () => any; 37 | get: (type: string) => any; 38 | parentPath: any; 39 | insertBefore: (node: any) => any; 40 | insertAfter: (node: any) => any; 41 | remove: () => void; 42 | unshiftContainer: (newNode: Node) => void; 43 | isIdentifier:(type: boolean) => false; 44 | 45 | } 46 | export interface stateDep { 47 | [state: string]: stateProps; 48 | } 49 | type stateProps = { 50 | lcms?: lcms[], 51 | handlers?: handlers[] 52 | } 53 | export interface lcms { 54 | name: string; 55 | expressionStatement?: expressionStatement; 56 | // functionDeclaration?: functionDeclaration; 57 | } 58 | export interface expressionStatement { 59 | node: Node; 60 | // check if a function 61 | setsState?: boolean; 62 | }; 63 | // export interface functionDeclaration { 64 | // node: Node; 65 | // setsState?: boolean; 66 | // } 67 | export interface handlers { 68 | name?: string; 69 | node?: any; 70 | stateName?: string; 71 | setsState?: boolean; 72 | } 73 | export interface handlerDepTree { 74 | [name: string] : handlerObj 75 | } 76 | export interface handlerObj { 77 | [state: string]: { 78 | // array of lcms 79 | lcms: { 80 | // node that references the handler 81 | expressionStatement: Node, 82 | // name of the lcm 83 | name: string 84 | }[], 85 | // saves entire node of handler 86 | node: Node; 87 | // does the handler setState with this piece of state in particular 88 | setsState: boolean; 89 | } 90 | } -------------------------------------------------------------------------------- /util/constants/names.ts: -------------------------------------------------------------------------------- 1 | // constants file 2 | export const C: string = 'constructor'; 3 | export const CDM: string = 'componentDidMount'; 4 | export const CDU: string = 'componentDidUpdate'; 5 | export const CWU: string = 'componentWillUnmount'; 6 | export const R: string = 'render'; 7 | 8 | export const UE: string = 'useEffect'; 9 | export const US: string = 'useState'; 10 | export const UC: string = 'useContext'; -------------------------------------------------------------------------------- /util/constants/parser.ts: -------------------------------------------------------------------------------- 1 | const parser: any = require('@babel/parser'); 2 | const traverse: any = require('@babel/traverse').default; 3 | const t: any = require('@babel/types'); 4 | const generate : any = require('@babel/generator').default; 5 | // File to export general parsing, traversing, and generating logic. 6 | function parse(file: string): any { 7 | const ast: any = parser.parse(file, { 8 | sourceType: 'module', 9 | plugins: ['jsx', 'classProperties'] 10 | }) 11 | return ast; 12 | } 13 | export { 14 | parse, 15 | traverse, 16 | t, 17 | generate 18 | } 19 | -------------------------------------------------------------------------------- /util/constants/visitors.ts: -------------------------------------------------------------------------------- 1 | import {t} from './parser'; 2 | import {Path, stateDep, handlerDepTree, Node} from './interfaces'; 3 | import {createFunctionDefinitions, checkKeyIdentifier, parseStateDep, checkIfHandler, makeUseStateNode, setStateToHooks, stateToHooks, thisRemover, buildStateDepTree, buildHandlerDepTree, strRemover} from '../helperfunctions'; 4 | import * as n from './names'; 5 | 6 | const HooksStatements: string[] = ['useState', 'useEffect', 'useContext']; 7 | const ContextStore: string[] = []; 8 | const DeclarationStore: string[] = []; 9 | let isAComponent: boolean = true; 10 | export const ImpSpecVisitor: {ImportSpecifier: (path: Path) => void} = { 11 | // method for traversing through all the ImportSpecifiers 12 | ImportSpecifier(path: Path): void { 13 | if(!HooksStatements.includes(path.node.local.name)){ 14 | DeclarationStore.push(path.node.local.name); 15 | } 16 | // check to see if the property 'imported' is an identifier with the name 'Component' 17 | if (path.get('imported').isIdentifier({name: 'Component'})) { 18 | // replace the current path (importSpecifier) with multiple new importSpcefiers 19 | path.replaceWithMultiple([ 20 | t.importSpecifier(t.identifier(n.US), t.identifier(n.US)), 21 | t.importSpecifier(t.identifier(n.UE), t.identifier(n.UE)), 22 | t.importSpecifier(t.identifier(n.UC), t.identifier(n.UC)), 23 | ]); 24 | } 25 | } 26 | } 27 | 28 | export const ImpDeclVisitor: {ImportDeclaration: (path: Path) => void} = { 29 | ImportDeclaration(path: Path): void { 30 | if (!isAComponent) return path.stop(); 31 | path.traverse(ImpSpecVisitor) 32 | path.traverse({ 33 | ImportDefaultSpecifier (path: Path): void { 34 | if((!HooksStatements.includes(path.node.local.name) && !DeclarationStore.includes(path.node.local.name))){ 35 | DeclarationStore.push(path.node.local.name); 36 | } 37 | } 38 | }) 39 | } 40 | } 41 | 42 | // useState: 43 | export const memberExpVisitor: object = { 44 | MemberExpression(path: Path): void{ 45 | // additional check to determine if the file is a component 46 | if (!isAComponent) return path.stop(); 47 | // check if node is of type setState 48 | if(path.node.property.name === 'setState'){ 49 | const arg0: Path = path.parentPath.get('arguments')[0]; 50 | if (t.isFunction(arg0.node) && (arg0.node.params.length)){ 51 | // should be the arrowFunctionExpression 52 | const setStateParam = arg0.get('params')[0].node 53 | // find the member expression 54 | arg0.traverse({ 55 | MemberExpression(path: Path){ 56 | if(t.isIdentifier(path.node.object, {name: setStateParam.name})){ 57 | strRemover(path as Path, setStateParam.name as string) 58 | } 59 | } 60 | }) 61 | } 62 | setStateToHooks(path.parentPath as Path); 63 | } 64 | // check if the node is just a state reference 65 | else if (path.node.property.name === 'state' && t.isThisExpression(path.node.object)){ 66 | if(path.parentPath) stateToHooks(path.parentPath as Path); 67 | } else { 68 | thisRemover(path as Path); 69 | } 70 | } 71 | } 72 | 73 | export const classDeclarationVisitor: {ClassDeclaration: (path: Path) => void} = { 74 | ClassDeclaration(path: Path): void { 75 | isAComponent = path.node.superClass && (path.get('superClass').isIdentifier({name: 'Component'}) || path.get('superClass').get('property').isIdentifier({name: 'Component'})); 76 | if (!isAComponent) return path.stop(); 77 | // class declaration 78 | let componentName: string = path.get('id').node.name; 79 | // useState 80 | const useStateData: any[] = []; 81 | let possibleProps: string = '()'; 82 | // useContext 83 | let contextToUse: string = ''; 84 | let isStatic: boolean = false; 85 | let isContext: boolean = false; 86 | let objectPattern: any; 87 | let storedBlockStatement: any; 88 | let multipleContexts: boolean = false; 89 | // useEffect 90 | const methodPaths: any[] = []; 91 | // handlers referencing state 92 | const handlerDepTree: handlerDepTree = {} 93 | // dependency tree of state 94 | const stateDependencies: stateDep = {}; 95 | path.traverse({ 96 | ClassProperty(path: Path): void { 97 | // checking for static context 'static contextType = ThemeContext' 98 | if(path.node.static) isStatic = true; 99 | if (!isStatic) path.stop(); 100 | contextToUse = path.node.value.name; 101 | } 102 | }) 103 | path.traverse({ 104 | // need to traverse through handlers first before checking them against references in lcms 105 | ClassMethod(path: Path):void { 106 | const cdm = checkKeyIdentifier(n.CDM, path), 107 | cdu = checkKeyIdentifier(n.CDU, path), 108 | cwu = checkKeyIdentifier(n.CWU, path), 109 | render = checkKeyIdentifier(n.R, path), 110 | constructor = checkKeyIdentifier(n.C, path); 111 | if (!cdm && !cdu && !cwu && !render && !constructor) { 112 | let handlerNode = path.node; 113 | let name: string = path.node.key.name ? path.node.key.name : ''; 114 | let paramNames: any[] = path.node.params; 115 | let body: any[] = path.node.body.body; 116 | methodPaths.push([createFunctionDefinitions(name, paramNames, body), path]); 117 | // check if a handler either setsState or references state 118 | path.traverse({ 119 | ExpressionStatement(path: Path): void { 120 | path.traverse({ 121 | MemberExpression(path: Path): void { 122 | const stateName: string = path.parentPath.node.property ? path.parentPath.node.property.name : null; 123 | if ((t.isIdentifier(path.node.property, {name: 'state'}) || t.isIdentifier(path.node.property, {name: 'props'})) && stateName) { 124 | buildHandlerDepTree(handlerDepTree, name, stateName, false, handlerNode); 125 | } 126 | if(t.isIdentifier(path.node.property, {name: 'setState'})) { 127 | let stateProperties: any[] = []; 128 | if (t.isObjectExpression(path.parentPath.node.arguments[0])) { 129 | stateProperties = stateProperties.concat(path.parentPath.node.arguments[0].properties); 130 | } 131 | else if (t.isArrowFunctionExpression(path.parentPath.node.arguments[0])) { 132 | path.parentPath.node.arguments[0].body.body.forEach((bodyEl: Node) => { 133 | if (t.isReturnStatement(bodyEl) && bodyEl.argument.properties) stateProperties = bodyEl.argument.properties; 134 | }) 135 | } 136 | if (stateProperties.length > 0) { 137 | stateProperties.forEach(property => { 138 | const setStateName: string = property.key.name; 139 | buildHandlerDepTree(handlerDepTree, name, setStateName, true, handlerNode); 140 | }) 141 | } 142 | } 143 | } 144 | }); 145 | } 146 | }); 147 | } 148 | // traverse into variable declarator to check for a member expression with the name 'context' 149 | path.traverse({ 150 | VariableDeclarator(path: Path): void { 151 | path.traverse({ 152 | MemberExpression(path: Path): void { 153 | if(path.node.property.name === "context"){ 154 | // flip this boolean to run other functions dependent on it 155 | isContext = true; 156 | } 157 | } 158 | }) 159 | // 160 | if(isContext){ 161 | if(path.node.id.type === "ObjectPattern"){ 162 | // check for destructured object 163 | // should only run if we've found context 164 | objectPattern = path.node.id; 165 | path.parentPath.remove(); 166 | } 167 | } 168 | } 169 | }) 170 | } 171 | }); 172 | // useContext: 173 | // if we find a static property, that property is the context we're looking for and it is destructured, then create a variable declarator for useContext 174 | if(isStatic && isContext && objectPattern){ 175 | path.traverse({ 176 | ClassProperty(path: Path): void { 177 | if(path.node.static) { 178 | path.replaceWith( 179 | t.variableDeclaration("const", 180 | [t.variableDeclarator(objectPattern, 181 | t.callExpression( 182 | t.identifier('useContext'),[ 183 | t.identifier(`${contextToUse}`) 184 | ] 185 | ) 186 | )] 187 | ) 188 | ) 189 | } 190 | } 191 | }) 192 | } 193 | path.traverse({ 194 | ClassMethod(path: Path): void { 195 | let currMethodName = path.node.key.name; 196 | const cdm = checkKeyIdentifier(n.CDM, path), 197 | cdu = checkKeyIdentifier(n.CDU, path), 198 | cwu = checkKeyIdentifier(n.CWU, path), 199 | render = checkKeyIdentifier(n.R, path), 200 | constructor = checkKeyIdentifier(n.C, path); 201 | // traverse through all expression statements and function declarations within a classMethod to create general stateDepTree 202 | // need to change this logic so that it's encapsulated within the body of each lcm 203 | // otherwise its not going to find the highest level node for the stateful reference 204 | // e.g., this.map.on('load', () => { this.setState({mapLoaded: true})}) 205 | // won't be captured, instead only the setState expressionStatement will be kept 206 | path.traverse({ 207 | ExpressionStatement(path: Path): void { 208 | let expressionStatement: any = path.node; 209 | let functionDeclaration: any; 210 | if (path.parentPath.parentPath.node.type === 'FunctionDeclaration') functionDeclaration = path.parentPath.parentPath.node; 211 | path.traverse({ 212 | MemberExpression(path: Path): void { 213 | const stateName: string = path.parentPath.node.property ? path.parentPath.node.property.name : null; 214 | let isHandler = checkIfHandler(currMethodName); 215 | if (!isHandler) { 216 | // assign expressionstatement node to functiondeclaration node if there is one 217 | if (functionDeclaration) expressionStatement = functionDeclaration; 218 | // check if the current member expression is a state object 219 | if (t.isIdentifier(path.node.property, {name: 'state'}) && stateName) { 220 | buildStateDepTree(currMethodName, expressionStatement, stateDependencies, stateName, false); 221 | } 222 | // check if member expression is a setState object 223 | if(t.isIdentifier(path.node.property, {name: 'setState'})) { 224 | let stateProperties: any[] | undefined; 225 | stateProperties = path.parentPath.node.arguments[0].properties; 226 | if (!stateProperties) { 227 | path.parentPath.node.arguments[0].body.body.forEach((bodyEl: Node) => { 228 | if (t.isReturnStatement(bodyEl)) stateProperties = bodyEl.argument.properties; 229 | }) 230 | } 231 | stateProperties.forEach(property => { 232 | const setStateName: string = property.key.name; 233 | buildStateDepTree(currMethodName, expressionStatement, stateDependencies, setStateName, true); 234 | }) 235 | } 236 | } 237 | 238 | } 239 | }) 240 | } 241 | }) 242 | // look specifically for the constructor method, where all the state is held 243 | let stateArr: any[]; 244 | if(constructor){ 245 | possibleProps = path.get('params').length === 0 ? possibleProps: path.get('params')[0].node.name; 246 | let assignmentExpressionExists = false; 247 | path.traverse({ 248 | AssignmentExpression(path: Path): void{ 249 | assignmentExpressionExists = true; 250 | if (t.isExpression(path.node, {operator: '='})) { 251 | if(t.isIdentifier(path.get('left').node.property, {name: 'state'})) { 252 | // in an Assignment Expression, there will be a left and a right 253 | // left will be what is the label/key and right will be the value(s) of what's being assigned 254 | // we keep the value(s) in a variable. it will be an array. 255 | stateArr = path.get('right').node.properties; 256 | } 257 | } 258 | } 259 | }) 260 | // in the case that there is no assignment expression for state 261 | if (!assignmentExpressionExists) path.remove(); 262 | else useStateData.push(path, stateArr); 263 | } 264 | if (cdm || cdu || cwu) { 265 | //traverse through lcm and check if a handler that uses state is referenced. If it is then build the handlerDepTree using the node that references that handler 266 | path.traverse({ 267 | ExpressionStatement(path: Path): void { 268 | // expressionStatement acts as a catch all for node paths. The variable can be reassigned to a node OTHER than an expression statement. 269 | let expressionStatement: Node = path.node; 270 | let functionDeclaration: Node; 271 | if (path.parentPath.parentPath.node.type === 'FunctionDeclaration') functionDeclaration = path.parentPath.parentPath.node; 272 | path.traverse({ 273 | MemberExpression(path: Path): void { 274 | // if the node path is wrapped by a function declaration reassign the expressionStatement to the functionDec 275 | if (functionDeclaration) expressionStatement = functionDeclaration; 276 | Object.keys(handlerDepTree).forEach(handlerName => { 277 | if (handlerName === path.node.property.name) { 278 | const stateNames = Object.keys(handlerDepTree[handlerName]); 279 | // loop through the stateNames array and build the handler dependency tree for the first pass 280 | stateNames.forEach(statename => { 281 | const setsState = handlerDepTree[handlerName][statename].setsState; 282 | const node = handlerDepTree[handlerName][statename].node; 283 | buildHandlerDepTree(handlerDepTree, handlerName, statename, setsState, node, currMethodName, expressionStatement) 284 | }) 285 | } 286 | }) 287 | } 288 | }) 289 | } 290 | }) 291 | path.remove(); 292 | } 293 | if(render) path.replaceWithMultiple(path.node.body.body); 294 | } 295 | }) 296 | methodPaths.forEach(arr => { 297 | arr[1].replaceWith(arr[0]) 298 | }) 299 | // need to change position of useEffect so it's after state declarations 300 | parseStateDep(stateDependencies, handlerDepTree).forEach(UE => { 301 | path.get('body').unshiftContainer('body', UE); 302 | }) 303 | // prepends 'const [state, setState] = useState(initVal)' outside of the constructor function 304 | // makeUseStateNode(path as any, state as any); 305 | makeUseStateNode(useStateData[0] as any, useStateData[1] as any).forEach(stateNode => path.get('body').unshiftContainer('body', stateNode)) 306 | path.traverse({ 307 | JSXElement(path: Path): void { 308 | path.traverse({ 309 | JSXMemberExpression(path: Path): void { 310 | // useContext: 311 | //if right side of expression is "consumer", grab the value on the left side of the dot to constuct the useContext statement 312 | if(path.node.property.name.toLowerCase() === 'consumer' && DeclarationStore.includes(path.node.object.name)){ 313 | // if DeclarationStore includes left side expession 314 | // contextStore checks import statement context name against what is included as the context wrapper 315 | if(!ContextStore.includes(path.node.object.name)){ 316 | // if they match, push the name into the ContextStore. 317 | ContextStore.push(path.node.object.name); 318 | } 319 | } 320 | // check for multiple context consumers 321 | if(ContextStore.length > 1)multipleContexts = true; 322 | else contextToUse = ContextStore[0]; 323 | // if there's a consumer context replace that node with a Fragment 324 | if(path.node.property.name.toLowerCase() === 'consumer' && path.node.object.name === contextToUse){ 325 | path.replaceWith( 326 | t.jSXMemberExpression(t.jSXIdentifier('React'), t.jSXIdentifier('Fragment')) 327 | ) 328 | } 329 | } 330 | }) 331 | if(!multipleContexts && contextToUse !== ''){ 332 | path.traverse({ 333 | JSXExpressionContainer(path: Path): void { 334 | let importedContext: string = 'imported' + contextToUse; 335 | path.traverse({ 336 | ArrowFunctionExpression(path: Path): void{ 337 | path.replaceWith( 338 | t.ExpressionStatement( 339 | t.identifier(`${importedContext}`) 340 | ) 341 | ) 342 | } 343 | }) 344 | } 345 | }) 346 | } 347 | if(multipleContexts){ 348 | ContextStore.forEach((e) => { 349 | path.traverse({ 350 | JSXExpressionContainer(path: Path): void { 351 | path.traverse({ 352 | ArrowFunctionExpression(path: Path): void { 353 | path.traverse({ 354 | VariableDeclarator(path: Path): void { 355 | if(path.node.init.name !== undefined) { 356 | if(path.node.init.name.toUpperCase() === e.toUpperCase()){ 357 | objectPattern = path.node.id; 358 | path.replaceWith( 359 | t.variableDeclarator(objectPattern, 360 | t.callExpression( 361 | t.identifier('useContext'),[ 362 | t.identifier(`${e}`) 363 | ] 364 | ) 365 | ) 366 | ) 367 | path.stop(); 368 | } 369 | } 370 | } 371 | }) 372 | //grab block statement to swap with JSX opening element 373 | if(path.node.body.type === "BlockStatement")storedBlockStatement = path.node.body.body; 374 | } 375 | }) 376 | } 377 | })//end of forEach 378 | }) 379 | } 380 | } 381 | }) 382 | path.traverse(memberExpVisitor); 383 | // if there is no static type context or multiple contexts then we set context name to the one we found in the import statement 384 | if(!isStatic && !multipleContexts && contextToUse){ 385 | path.get('body').unshiftContainer('body', 386 | t.variableDeclaration("const", 387 | [t.variableDeclarator( 388 | t.identifier('imported'+`${contextToUse}`), 389 | t.callExpression(t.identifier("useContext"), 390 | [t.identifier(`${contextToUse}`)] 391 | ) 392 | )] 393 | ) 394 | ) 395 | } 396 | // looks for multiple context statements and gets new entire stored body 397 | storedBlockStatement = path.node.body.body; 398 | path.replaceWith( 399 | t.variableDeclaration("const", 400 | [t.variableDeclarator( 401 | t.identifier(`${componentName}`), 402 | t.arrowFunctionExpression([t.identifier(possibleProps)], t.blockStatement(storedBlockStatement)) 403 | ) 404 | ]) 405 | ) 406 | } 407 | } -------------------------------------------------------------------------------- /util/helperfunctions.ts: -------------------------------------------------------------------------------- 1 | import {t} from './constants/parser'; 2 | import {Path, stateDep, handlers, lcms, handlerObj, handlerDepTree, Node} from './constants/interfaces'; 3 | import * as n from './constants/names'; 4 | 5 | function depArr (stateToCheck?: any []): any { 6 | if (!stateToCheck) return null; 7 | if (stateToCheck.length === 0) return t.arrayExpression(); 8 | return t.arrayExpression(stateToCheck.map(state => { 9 | if (Array.isArray(state)) return state.map(innerStateArr => t.Identifier(innerStateArr)); 10 | return t.Identifier(state) 11 | })); 12 | } 13 | function createReturnStatement (returnFunction: any): any { 14 | // define 'return' 15 | return [t.returnStatement( 16 | // define '()=> {}' 17 | t.arrowFunctionExpression( 18 | // params- 'none' 19 | [], 20 | // blockstatement [t.expressionStatement(t.thisExpression())] 21 | t.blockStatement(returnFunction) 22 | ) 23 | )] 24 | } 25 | /** 26 | * 27 | * @param body the main body array for the entire useEffect hook, contains all the nodes to be used in the first argument callback 28 | * @param opts an object that contains the type of lcm, an array of nodes for the return function, and the dependency array (if any) 29 | * 30 | * Does the main work to create the useEffect hook, relies on createReturnStatement and depArr functions to create the corresponding new nodes. 31 | */ 32 | export function createUseEffect (body: any[], opts?: {lcm?: string, returnFunction?: any[], stateToCheck?: any []} ): string { 33 | // determine what second argument (if any) should be passed into createSecondArg 34 | const secondArg: any[] = 35 | opts.lcm === 'componentDidMount' 36 | // if componentDidMount call createSecondArg without an argument to return an Array Expression without any value 37 | ? depArr() 38 | // if componenDidUpdate call createSecondArg with the stateToCheck 39 | : depArr(opts.stateToCheck) 40 | // returnStatement is of type returnStatement 41 | const returnStatement: any[] = opts.returnFunction.length > 0 ? createReturnStatement(opts.returnFunction) : []; 42 | // create the expressionstatement 43 | if (!secondArg) return t.ExpressionStatement( 44 | // use the identifier useEffect 45 | t.callExpression(t.identifier('useEffect'), 46 | //arrow function argument 47 | [t.arrowFunctionExpression([], t.blockStatement(body.concat(returnStatement)))] 48 | ) 49 | ); 50 | return t.ExpressionStatement( 51 | // use the identifier useEffect 52 | t.callExpression(t.identifier('useEffect'), 53 | //arrow function argument 54 | [t.arrowFunctionExpression([], t.blockStatement(body.concat(returnStatement))), 55 | secondArg //put optional argument for empty array 56 | ]) 57 | ); 58 | } 59 | // helper function to create function definitions 60 | export function createFunctionDefinitions(name: string, paramNames: any[], body: any[]) { 61 | // const params: any[] = paramNames.map(param => t.identifier(param)); 62 | return t.functionDeclaration(t.identifier(name), 63 | paramNames, 64 | t.blockStatement(body) 65 | ) 66 | } 67 | // checks for key identifiers for method names 68 | export function checkKeyIdentifier(name: string, path: Path): any { 69 | return path.get('key').isIdentifier({name}) 70 | } 71 | // checks if the current method is a non-lcm handler 72 | export function checkIfHandler(methodName: string) { 73 | if (methodName !== n.CDM && methodName !== n.CDU && methodName !== n.CWU && methodName !== n.C && methodName !== n.R) return true; 74 | return false; 75 | } 76 | 77 | 78 | /** 79 | * @param stateDep is the stateDepTree of all state references within lcms. See interfaces.ts, stateDep 80 | * @param handlerDepTree is the handlerDepTree that refences all non-lcm methods that are referenced within lcm methods. See interfaces.ts, handlerDepTree 81 | * Does the major work to parse through the created stateDepTree/handlerDepTree to format the depTree into their corresponding UE hooks. 82 | * This is based on several things but boils down to the type of lcm the expressionstatement was called in and whether it setsState. 83 | * 84 | */ 85 | export function parseStateDep(stateDep: stateDep, handlerDepTree? : handlerDepTree) { 86 | // array of useEffect hooks 87 | let useEffectArr: any[] = []; 88 | // need to account for two cases for handlerDepTree 89 | // case 1: stateful references are more than 1 90 | // loop through the lcms array and build the UE opts from lcm expressionstatement, setsState property, and lcm name 91 | // (all state properties should have the same reference to the expression statements) 92 | // this will be its own UE hook 93 | // case 2: stateful references === 1 94 | // handler should be grouped with the rest of the stateArr 95 | if (handlerDepTree) { 96 | // check data and reformat it to use in rest of code 97 | const handlerUseEffect = Object.entries(handlerDepTree); 98 | handlerUseEffect.forEach(handlerArr => { 99 | const handlerName: string = handlerArr[0]; 100 | const stateArr = Object.entries(handlerArr[1]); 101 | // if the handler doesn't have any lcm references there's no need to put it in UE 102 | if (stateArr[0][1].lcms.length === 0) delete handlerDepTree[handlerName]; 103 | else { 104 | let mappedLcmsObj: any; 105 | // set key name for stateDepTree 106 | let stateDepTreeKey: string; 107 | // if there is more than one stateful reference within a handler, logic pertaining to that handler needs to be grouped together 108 | stateArr.forEach(statePair => { 109 | const lcmsObj = statePair[1]; 110 | mappedLcmsObj = lcmsObj.lcms.map(lcm => { 111 | return {expressionStatement: {node: lcm.expressionStatement, setsState: lcmsObj.setsState}, name: lcm.name}; 112 | }); 113 | }); 114 | if (stateArr.length > 1) { 115 | // key becomes the entire array of state that is being tracked through the handler 116 | const stateNameKeys: string[] = Object.keys(handlerArr[1]); 117 | stateDepTreeKey = JSON.stringify(stateNameKeys); 118 | stateDep[stateDepTreeKey] = {lcms: mappedLcmsObj}; 119 | } 120 | else { 121 | stateDepTreeKey = stateArr[0][0]; 122 | stateDep[stateDepTreeKey].lcms = stateDep[stateDepTreeKey].lcms.concat(mappedLcmsObj); 123 | } 124 | } 125 | }) 126 | } 127 | const stateArr = Object.keys(stateDep); 128 | // for each state that is being tracked through the lcms we must check all lcm properties 129 | stateArr.forEach(state => { 130 | // body to pass into useEffect 131 | let body: any[] = []; 132 | let opts: {lcm?: string, returnFunction?: any[], stateToCheck?: any[]} = { 133 | lcm: '', 134 | returnFunction: [], 135 | stateToCheck: [] 136 | }; 137 | // catches duplicates within nodes- doesn't work currently because the nodes are all unique and can't be used as key pairs to check against. Need some other criteria to cache so we don't create duplicates. 138 | // const nodeCache: any = {}; 139 | if(stateDep[state].lcms) { 140 | stateDep[state].lcms.forEach(lcm => { 141 | const setsState: boolean = lcm.expressionStatement.setsState 142 | const node: Node = lcm.expressionStatement.node; 143 | // const nodeKey = JSON.stringify(node); 144 | const CDU: boolean = lcm.name === n.CDU; 145 | const CWU: boolean = lcm.name === n.CWU; 146 | // if (!nodeCache[nodeKey]){ 147 | if (!CDU && !setsState && !CWU) { 148 | opts.stateToCheck = null; 149 | body.push(node); 150 | } 151 | if (!CDU && setsState && !CWU) { 152 | opts.stateToCheck = []; 153 | body.push(node); 154 | } 155 | if (CDU && setsState && !CWU) { 156 | if(!opts.stateToCheck) opts.stateToCheck = []; 157 | // parse the state in the case where we pass in a stringified array for handlers 158 | const newState = state.charAt(0) === '[' ? JSON.parse(state) : state; 159 | opts.stateToCheck.push(newState); 160 | body.push(node); 161 | } 162 | if (CWU) { 163 | opts.returnFunction.push(node); 164 | } 165 | // nodeCache[nodeKey] = true; 166 | // } 167 | }) 168 | useEffectArr.push(createUseEffect(body, opts)); 169 | } 170 | }) 171 | return useEffectArr; 172 | } 173 | /** 174 | * @param currMethodName the current lcm name 175 | * @param expressionStatement the node associated with the state's reference in the lcm 176 | * @param stateDependencies the entire State Dependency Tree object that will be mutated 177 | * @param stateName the name of the state that is referenced in the lcm 178 | * @param setsState a boolean based on whether the referenced expressionStatement node setsState 179 | */ 180 | export function buildStateDepTree(currMethodName: string, expressionStatement: any, stateDependencies: stateDep, stateName: string, setsState: boolean) { 181 | const lcmsObj: lcms = { 182 | name: currMethodName, 183 | expressionStatement: { 184 | node: expressionStatement, 185 | setsState 186 | } 187 | }; 188 | if(stateDependencies.hasOwnProperty(stateName)) { 189 | stateDependencies[stateName].lcms.push(lcmsObj) 190 | } 191 | // if the state property is not defined yet, we need to initialize it 192 | else { 193 | stateDependencies[stateName] = {lcms: [lcmsObj]}; 194 | } 195 | } 196 | /** 197 | * @param handlerDepTree the entire handler dependency tree that will be mutated and built 198 | * @param handlerName the name of the handler function- any function not a lcm 199 | * @param stateDependencies the entire State Dependency Tree object that will be mutated 200 | * @param stateName the name of the state that is referenced in the lcm and handler 201 | * @param setsState a boolean based on whether the referenced handler setsState 202 | * @param node the node of the entire handler function 203 | * @param currMethodName the name of the lcm that has a reference to the handler 204 | * @param expressionStatement the node within the lcm that references the handler 205 | */ 206 | export function buildHandlerDepTree(handlerDepTree: handlerDepTree, handlerName: string,stateName: string, setsState: boolean, node: Node, currMethodName?: string, expressionStatement?: Node ) { 207 | // requires two passes, one to set the handlerObj for the initial pass when we discover a handler function 208 | // the other to add the lcmsObj when we find this handler referenced in a lcm 209 | const handlerObj: handlerObj = { 210 | [stateName]: { 211 | lcms: [], 212 | node, 213 | setsState 214 | } 215 | }; 216 | 217 | // check if this is the second pass to add the lcmsObj 218 | if (currMethodName && expressionStatement) { 219 | const lcmsObj = { 220 | expressionStatement, 221 | name: currMethodName 222 | } 223 | if(handlerDepTree.hasOwnProperty(handlerName)) { 224 | handlerDepTree[handlerName][stateName].lcms.push(lcmsObj); 225 | } 226 | else { 227 | handlerDepTree[handlerName] = handlerObj; 228 | } 229 | } 230 | // if we're on our first pass we need to add our handlerObj 231 | else { 232 | // if we haven't seen this handler before then add the entire handlerObj; 233 | if (!handlerDepTree.hasOwnProperty(handlerName)) { 234 | handlerDepTree[handlerName] = handlerObj; 235 | } 236 | else { 237 | // if our handlerDepTree already has seen a state only update the setsState property with a truthy value 238 | if (handlerDepTree[handlerName].hasOwnProperty(stateName) && setsState) { 239 | handlerDepTree[handlerName][stateName].setsState = setsState; 240 | } 241 | // if it's a new state ref then add a new object to the handlerName 242 | else { 243 | handlerDepTree[handlerName][stateName] = { 244 | lcms: [], 245 | node, 246 | setsState 247 | }; 248 | } 249 | } 250 | } 251 | } 252 | 253 | 254 | /** 255 | * this will uppercase the first letter in the string 256 | * @param string the string to affect 257 | * @returns a string 258 | * taken from a website do not steal 259 | */ 260 | const upFirstChar = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); 261 | /** 262 | * this func will create 'const [state, setState] = useState(initState);' from 'rightObjProps' and insert from 'path' 263 | * @param path the path to append siblings to before deletion 264 | * @param rightExpr the props array from ObjectExpression which contains the state 265 | */ 266 | export function makeUseStateNode(path: Path, rightObjProps: any[]): Node[] { 267 | if (rightObjProps === undefined) return []; 268 | const states = []; 269 | // the rightObjProps will be an array 270 | for (let i = 0; i < rightObjProps.length; i++){ 271 | // declare the node itself to make it easier to work with 272 | const objExp = rightObjProps[i]; 273 | // an ObjectExpression will contain a key with a Node type 'Identifier' 274 | const key = objExp.key; 275 | // the actual name of the state as a string and not a Node type 276 | const keyName = key.name; 277 | // an ObjectExpression will contain a value with a Node type of any Expression (another nested Object or any value) 278 | const val = objExp.value; 279 | // declare an array pattern for the '[state, setState]' 280 | const arrPatt = t.arrayPattern([t.identifier(keyName), t.identifier('set' + upFirstChar(keyName))]); 281 | // declares 'useState(initVal)' 282 | const callExp = t.callExpression(t.identifier('useState'), [val]); 283 | // creates '[state, setState] = useState(initVal);' 284 | const varDecl = t.variableDeclarator(arrPatt, callExp); 285 | // adds 'const [state, setState] = useState(initState);' as a sibling 286 | states.push(t.variableDeclaration('const', [varDecl])); 287 | // path.insertBefore(t.variableDeclaration('const', [varDecl])) 288 | } 289 | path.remove(); 290 | return states; 291 | } 292 | 293 | /** 294 | * replaces this.setState({ state: newState }) and this.setState(anon => {return { callback }}) with setState(newState) 295 | * ALERT -- place function within member expression visitor 296 | * @param parentPath exactly what it says 297 | */ 298 | export function setStateToHooks(parentPath: any): void { 299 | // this will be an array of arguments to make setState Call Arguments with 300 | const states: Node[] = []; 301 | // props of the object in setState to iterate through 302 | let args: Node[] | undefined = parentPath.node.arguments[0].properties; 303 | // escape hatch for updater version of setState if the return statement doesn't contain any properties. 304 | if (!args) return; 305 | // node of arg[0] of setState 306 | const arg0: Node = parentPath.node.arguments[0]; 307 | // if arg[0] is a func and has a parameter 308 | // node of arg[1] of setState 309 | const arg1: Node | undefined = parentPath.node.arguments[1]; 310 | // if arg[0] is not an object, assume it's an anonymous cb function 311 | if (t.isFunction(arg0) && t.isBlock(arg0.body)) { 312 | const expressions = arg0.body.body; 313 | for (let i = 0; i < expressions.length; i++) { 314 | if (t.isReturnStatement(expressions[i])) args = expressions[i].argument.properties; 315 | else parentPath.insertBefore(expressions[i]) 316 | } 317 | } 318 | else if (t.isFunction(arg0) && t.isObjectExpression(arg0.body)) { 319 | args = arg0.body.properties; 320 | } 321 | // HAVE TO ACCOUNT IF AN IDENTIFIER IS ARG[0] 322 | for (let i = 0; i < args.length; i++){ 323 | const keyName = args[i].key.name; 324 | const call = t.identifier('set' + upFirstChar(keyName)) 325 | const arg = args[i].value; 326 | const callStatement = t.callExpression(call, [arg]) 327 | const expStatement = t.expressionStatement(callStatement) 328 | states.push(expStatement) 329 | } 330 | if (t.isFunction(arg1)) parentPath.insertAfter(arg1.body) 331 | parentPath.replaceWithMultiple(states) 332 | } 333 | /** 334 | * turns 'this.state.example' expressions to 'example' 335 | * @param parentPath path.parentPath. this. what it says. 336 | */ 337 | export function stateToHooks (parentPath: any): void { 338 | if (t.isMemberExpression(parentPath.parentPath.node)) parentPath.parentPath.node.object = parentPath.node.property; 339 | else parentPath.replaceWith(parentPath.node.property); 340 | } 341 | /** 342 | * will DECIMATE all other this statements no matter what. Used within MemberExpression Visitor 343 | * WARNING: will literally destroy any and all 'this' statements 344 | * @param path pass in the path of MemberExpression where it will look for anything that has to do with 'this' 345 | */ 346 | export function thisRemover(path: any): void { 347 | if (t.isThisExpression(path.node.object)){ 348 | if (t.isMemberExpression(path.node)) path.node.object = path.node.property; 349 | if (t.isCallExpression(path.node)) path.node.callee = path.node.property; 350 | else path.replaceWith(path.node.property); 351 | } 352 | } 353 | 354 | /** 355 | * alternative of thisRemover; will remove specified string's member expression 356 | * @param path pass in the path of MemberExpression where it will look for anything that has to do with 'str' 357 | * @param str the str to remove in syntax tree 358 | */ 359 | export function strRemover(path: Path, str: string) { 360 | if (t.isIdentifier(path.node.object, { name: str })){ 361 | if (t.isMemberExpression(path.node)) path.node.object = path.node.property; 362 | if (t.isCallExpression(path.node)) path.node.callee = path.node.property; 363 | else path.replaceWith(path.node.property); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | // building a knock-off webpack-merge 5 | // https://www.npmjs.com/package/webpack-merge 6 | const mainExport = { 7 | target: 'node', 8 | mode: process.env.NODE_ENV, 9 | resolve: { 10 | modules: [path.join(__dirname, './node_modules')], 11 | extensions: ['.ts', '.tsx', '.js', '.jsx'] 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?/, 17 | exclude: /node_modules/, 18 | use: [ 19 | {loader: 'ts-loader'} 20 | ] 21 | }, 22 | { 23 | enforce: 'pre', 24 | test: /\.jsx?/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['@babel/preset-env', '@babel/preset-react'] 30 | }, 31 | } 32 | }, 33 | ] 34 | }, 35 | } 36 | 37 | // options for makign cli 38 | const cli = { 39 | entry: { 40 | cli: './cli.ts' 41 | }, 42 | output: { 43 | path: path.resolve(__dirname, 'packages/hookd-cli'), 44 | filename: '[name].js', 45 | library: '', 46 | libraryTarget: 'umd', 47 | globalObject: 'this' 48 | }, 49 | plugins: [ 50 | new webpack.BannerPlugin({ banner: "#!/usr/bin/env node", raw: true}) 51 | ], 52 | node:{ 53 | __dirname: true, 54 | } 55 | } 56 | 57 | // options for making the main module 58 | const index = { 59 | entry: { 60 | index:'./index.ts', 61 | }, 62 | output: { 63 | path: path.resolve(__dirname, 'packages/hookd'), 64 | filename: '[name].js', 65 | library: '', 66 | libraryTarget: 'umd', 67 | globalObject: 'this' 68 | }, 69 | } 70 | 71 | // conditionals to assign proper variables to build env 72 | if (process.env.BUILD === 'cli') Object.assign(mainExport, cli); 73 | else if (process.env.BUILD === 'index') Object.assign(mainExport, index); 74 | 75 | // exports mutated mainExport 76 | module.exports = mainExport; --------------------------------------------------------------------------------