├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── cmd.js ├── demo.gif ├── esquery ├── esquery.js └── parser.js ├── esrecurse └── esrecurse.js ├── estraverse └── estraverse.js ├── examples ├── counter-example │ ├── index.html │ ├── index.js │ ├── src │ │ ├── App.jsx │ │ └── Counter.js │ └── webpack.config.js ├── recipe │ ├── app │ │ ├── components │ │ │ ├── CardAdd.jsx │ │ │ ├── CardBack.jsx │ │ │ ├── CardFront.jsx │ │ │ ├── EditableCard.jsx │ │ │ ├── Menu.jsx │ │ │ ├── MenuItem.jsx │ │ │ └── RecipeBox.jsx │ │ └── index.jsx │ ├── index.html │ └── webpack.config.js ├── todoApp │ ├── app │ │ ├── components │ │ │ ├── List.jsx │ │ │ ├── ListItem.jsx │ │ │ ├── TODO.jsx │ │ │ └── TodoForm.jsx │ │ └── index.jsx │ └── webpack.config.js └── todoApp2 │ ├── app │ ├── components │ │ ├── List.jsx │ │ ├── ListItem.jsx │ │ ├── TODO.jsx │ │ └── TodoForm.jsx │ └── index.jsx │ ├── index.html │ └── webpack.config.js ├── index.html ├── package.json ├── react ├── actions │ └── index.js ├── assets │ ├── icon.svg │ ├── logo.png │ └── logo.svg ├── components │ ├── App.jsx │ ├── Graph.jsx │ ├── Node.jsx │ └── NodeUp.jsx ├── hooks.jsx ├── index.jsx ├── reducers │ └── monocleApp.js └── store │ └── monocleStore.js ├── server ├── server.js └── start.js ├── src ├── astGenerator.js ├── constants.js ├── d3DataBuilder.js ├── d3Tree │ ├── app.js │ ├── hooks.js │ └── renderHtml.js ├── htmlParser.js ├── previewParser.js ├── reactInterceptor.js ├── reactParser.js ├── stringRegexHelper.js └── test │ ├── astGeneratorTest.js │ ├── d3DataBuilderTest.js │ ├── fixtures │ ├── bundleFileFixture.js │ ├── commonReactComponentFixtures.js │ ├── d3TreeFixtures.js │ ├── dummyTree.js │ ├── es5ReactComponentFixtures.js │ ├── es6ReactComponentFixtures.js │ ├── es6StatelessFunctionalComponentFixtures.js │ ├── modifyInitialStateOutputFixture.js │ ├── modifySetStateStringsInputFixture.js │ ├── modifySetStateStringsOutputFixture.js │ ├── reactParserOutputFixtures.js │ └── test_components │ │ ├── BIG.jsx │ │ ├── BigPoppa.jsx │ │ ├── Biggie.jsx │ │ ├── Foo_es5.jsx │ │ ├── Foo_es6.jsx │ │ ├── Notorious.jsx │ │ ├── ReactDOMRender.jsx │ │ └── app.jsx │ ├── iterTest.js │ ├── previewParserTest.js │ ├── reactInterceptorTest.js │ ├── reactParserTest.js │ └── test.js ├── webpack.config.js └── webpack.production.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | src/d3Tree/* 2 | src/test/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "installedESLint": true, 4 | "plugins" : [ 5 | "react" 6 | ], 7 | "rules": { 8 | "strict": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | react-monocle.html 3 | .DS_Store 4 | npm-debug.log 5 | bundle.js 6 | bundle.js.map 7 | data.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 6 7 | - 4 8 | 9 | cache: 10 | directories: 11 | - node_modules 12 | 13 | install: 14 | - npm i -g npm@latest 15 | - npm install 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 team-gryff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-monocle 2 | [![Build Status](https://travis-ci.org/team-gryff/react-monocle.svg?branch=master)](https://travis-ci.org/team-gryff/react-monocle) [![npm version](https://badge.fury.io/js/react-monocle.svg)](https://badge.fury.io/js/react-monocle) 3 | 4 | 5 | 6 |
7 | **React Monocle** is a developer tool for generating visual representations of your React app's component hierarchy. 8 | 9 |
10 | 11 | 12 | ## How It Works 13 | React Monocle parses through your React source files to generate a visual tree graph representing your React component hierarchy. 14 | The tree is then displayed along with a live copy of your application. 15 | This is done by using your un-minified bundle file to inject wrapper functions around setState calls in order to have our tree display real-time feedback. 16 | The rendered tree is synced up to the state(s) of your component using Redux, and as the state of your live app changes, the monocle tree graph will also provide visual feedback of data flow and state changes through the React components. 17 | 18 | ## Setup 19 | **IMPORTANT** The way we use your bundle file requires so that the bundle is not mangled (ie not minified). 20 | 21 | 1. ```npm install -g react-monocle``` 22 | 2. Navigate to the directory which contains your html file. 23 | 3. Type in ```monocle -h``` in order to find what options suit your needs the best. The only required option is specifying the bundle. 24 | 25 | 26 | ## Options 27 | 28 | | Option | Command | Description | 29 | -------------|-------------------|------------------------------------------------------------------------------------------------------------------------------| 30 | | **Bundle** | **--bundle (-b)** | **Required** Path to React bundle file. | 31 | | HTML | --html (-c) | HTML page which has your bundle and CSS files. Specify if you want CSS displayed and/or you are relying on external scripts. | 32 | | Entry | --entry (-e) | App entry point. Defaults to JSX file where ReactDOM.render is found. | 33 | | Directory | --directory (-d) | directory of React files. Defaults to where Monocle was called. | 34 | 35 | ## Contributing/Testing 36 | 37 | * Please feel free to fork and submit pull requests! 38 | * After installing, you can run tests via ```npm run unit-tests``` or ```npm run test``` to run ESLint simultaneously 39 | * Tests can be found in src/test and are currently broken out into: 40 | 1. astGeneratorTest.js 41 | 2. d3DataBuilderTest.js 42 | 3. iterTest.js 43 | 4. previewParserTest.js 44 | 5. reactParserTest.js 45 | 6. test.js (to compile and run all tests at once) 46 | * Please add new tests to relevant files specified above, or create new test files as needed. 47 | 48 | ## Coming Soon 49 | 50 | * Demo 51 | * Quick description of what goes on under the hood. 52 | * Improving how we search for components 53 | 54 | ## In the Pipeline 55 | 56 | * Support for React-Router 57 | * Support for Redux 58 | 59 | 60 | 61 | ## Our Team 62 | * Michael-Bryant Choa - [github.com/mbchoa](https://github.com/mbchoa) 63 | * Jenna Davis - [github.com/jdavis218](https://github.com/jdavis218) 64 | * Jerry Mao - [github.com/jerrymao15](https://github.com/jerrymao15) 65 | 66 | ## License 67 | MIT 68 | 69 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | // add babel hook to compile following requires on the fly into es5 compliant code 5 | const babelRegister = require('babel-register'); 6 | babelRegister(); 7 | 8 | const program = require('commander'); 9 | const glob = require('glob'); 10 | const astGenerator = require('../src/astGenerator'); 11 | const assign = require('lodash.assign'); 12 | const d3DataBuilder = require('../src/d3DataBuilder'); 13 | const renderHtml = require('../src/d3Tree/renderHtml'); 14 | const previewParser = require('../src/previewParser'); 15 | const htmlParser = require('../src/htmlParser'); 16 | const start = Date.now(); 17 | 18 | // specifying one required parameter 19 | program 20 | .option('-b, --bundle ', '**Required** Path to React bundle file.') 21 | .option('-c --html ', 'HTML page which has your bundle and CSS files. Specify if you want CSS displayed and/or you are relying on external scripts.') 22 | .option('-e, --entry ', 'App entry point. Defaults to JSX file where ReactDOM.render is found.') 23 | .option('-d, --directory ', 'directory of React files. Defaults to where Monocle was called.') 24 | .parse(process.argv); 25 | 26 | program.name = 'monocle'; 27 | 28 | (function () { 29 | 30 | let bundle = null; 31 | let entry = null; 32 | let htmlElements; 33 | let directory = process.cwd(); 34 | if (program.html) htmlElements = htmlParser(program.html, program.bundle); 35 | if (!program.bundle) { 36 | throw new Error(' --bundle | -b required '); 37 | } else if (program.html) { 38 | bundle = `${process.cwd()}/${htmlElements.bundle}`; 39 | } else { 40 | bundle = `${process.cwd()}/${program.bundle}`; 41 | } 42 | if (program.entry) entry = `${process.cwd()}/${program.entry}`; 43 | if (program.directory) directory = `${process.cwd()}/${program.directory}`; 44 | 45 | let modifiedBundle = previewParser.modifySetStateStrings(bundle); 46 | modifiedBundle = previewParser.modifyInitialState(modifiedBundle); 47 | const divsArr = previewParser.getDivs(modifiedBundle); 48 | 49 | // globs to match any jsx or js file in directory 50 | glob(`**/*.{js,jsx}`, { cwd: directory, nosort: true, ignore: ['node_modules/**', 'react/**'] }, (err, files) => { // TODO: CHANGE BACK 51 | if (files.length === 0) throw new Error('No files found (try specifying file path and extension)'); 52 | if (directory !== process.cwd()) files = files.map(ele => `${process.cwd()}/${program.directory}/${ele}`); 53 | 54 | // converting file paths to abstract syntax trees (output is an array with {ComponentName: AST} objects) 55 | const astz = files.map(ele => astGenerator(ele)); 56 | let componentObject = assign.apply(null, astz); // combining into one file 57 | if (entry) componentObject = assign(componentObject, astGenerator(entry)); 58 | const formatedD3Object = d3DataBuilder(componentObject); // building the tree 59 | 60 | // render react-monocle.html output with completed React component tree 61 | renderHtml( 62 | formatedD3Object, 63 | modifiedBundle, 64 | start, 65 | divsArr, 66 | htmlElements ? htmlElements.scripts : [], 67 | htmlElements ? htmlElements.css : [] 68 | ); 69 | }); 70 | })(); 71 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/team-gryff/react-monocle/b90f26e2e6440d7e8f2e596237ec1ed598827453/demo.gif -------------------------------------------------------------------------------- /esquery/esquery.js: -------------------------------------------------------------------------------- 1 | /* vim: set sw=4 sts=4 : */ 2 | (function () { 3 | 4 | var estraverse = require('../estraverse/estraverse'); 5 | var parser = require('./parser'); 6 | 7 | var isArray = Array.isArray || function isArray(array) { 8 | return {}.toString.call(array) === '[object Array]'; 9 | }; 10 | 11 | function esqueryModule() { 12 | 13 | /** 14 | * Get the value of a property which may be multiple levels down in the object. 15 | */ 16 | function getPath(obj, key) { 17 | var i, keys = key.split("."); 18 | for (i = 0; i < keys.length; i++) { 19 | if (obj == null) { return obj; } 20 | obj = obj[keys[i]]; 21 | } 22 | return obj; 23 | } 24 | 25 | /** 26 | * Determine whether `node` can be reached by following `path`, starting at `ancestor`. 27 | */ 28 | function inPath(node, ancestor, path) { 29 | var field, remainingPath, i; 30 | if (path.length === 0) { return node === ancestor; } 31 | if (ancestor == null) { return false; } 32 | field = ancestor[path[0]]; 33 | remainingPath = path.slice(1); 34 | if (isArray(field)) { 35 | for (i = 0, l = field.length; i < l; ++i) { 36 | if (inPath(node, field[i], remainingPath)) { return true; } 37 | } 38 | return false; 39 | } else { 40 | return inPath(node, field, remainingPath); 41 | } 42 | } 43 | 44 | /** 45 | * Given a `node` and its ancestors, determine if `node` is matched by `selector`. 46 | */ 47 | function matches(node, selector, ancestry) { 48 | var path, ancestor, i, l, p; 49 | if (!selector) { return true; } 50 | if (!node) { return false; } 51 | if (!ancestry) { ancestry = []; } 52 | 53 | switch(selector.type) { 54 | case 'wildcard': 55 | return true; 56 | 57 | case 'identifier': 58 | return selector.value.toLowerCase() === node.type.toLowerCase(); 59 | 60 | case 'field': 61 | path = selector.name.split('.'); 62 | ancestor = ancestry[path.length - 1]; 63 | return inPath(node, ancestor, path); 64 | 65 | case 'matches': 66 | for (i = 0, l = selector.selectors.length; i < l; ++i) { 67 | if (matches(node, selector.selectors[i], ancestry)) { return true; } 68 | } 69 | return false; 70 | 71 | case 'compound': 72 | for (i = 0, l = selector.selectors.length; i < l; ++i) { 73 | if (!matches(node, selector.selectors[i], ancestry)) { return false; } 74 | } 75 | return true; 76 | 77 | case 'not': 78 | for (i = 0, l = selector.selectors.length; i < l; ++i) { 79 | if (matches(node, selector.selectors[i], ancestry)) { return false; } 80 | } 81 | return true; 82 | 83 | case 'child': 84 | if (matches(node, selector.right, ancestry)) { 85 | return matches(ancestry[0], selector.left, ancestry.slice(1)); 86 | } 87 | return false; 88 | 89 | case 'descendant': 90 | if (matches(node, selector.right, ancestry)) { 91 | for (i = 0, l = ancestry.length; i < l; ++i) { 92 | if (matches(ancestry[i], selector.left, ancestry.slice(i + 1))) { 93 | return true; 94 | } 95 | } 96 | } 97 | return false; 98 | 99 | case 'attribute': 100 | p = getPath(node, selector.name); 101 | switch (selector.operator) { 102 | case null: 103 | case void 0: 104 | return p != null; 105 | case '=': 106 | switch (selector.value.type) { 107 | case 'regexp': return selector.value.value.test(p); 108 | case 'literal': return '' + selector.value.value === '' + p; 109 | case 'type': return selector.value.value === typeof p; 110 | } 111 | case '!=': 112 | switch (selector.value.type) { 113 | case 'regexp': return !selector.value.value.test(p); 114 | case 'literal': return '' + selector.value.value !== '' + p; 115 | case 'type': return selector.value.value !== typeof p; 116 | } 117 | case '<=': return p <= selector.value.value; 118 | case '<': return p < selector.value.value; 119 | case '>': return p > selector.value.value; 120 | case '>=': return p >= selector.value.value; 121 | } 122 | 123 | case 'sibling': 124 | return matches(node, selector.right, ancestry) && 125 | sibling(node, selector.left, ancestry) || 126 | matches(node, selector.left, ancestry) && 127 | sibling(node, selector.right, ancestry); 128 | 129 | case 'adjacent': 130 | return matches(node, selector.right, ancestry) && 131 | adjacent(node, selector.left, ancestry) || 132 | matches(node, selector.left, ancestry) && 133 | adjacent(node, selector.right, ancestry); 134 | 135 | case 'nth-child': 136 | return matches(node, selector.right, ancestry) && 137 | nthChild(node, ancestry, function (length) { 138 | return selector.index.value - 1; 139 | }); 140 | 141 | case 'nth-last-child': 142 | return matches(node, selector.right, ancestry) && 143 | nthChild(node, ancestry, function (length) { 144 | return length - selector.index.value; 145 | }); 146 | 147 | case 'class': 148 | if(!node.type) return false; 149 | switch(selector.name.toLowerCase()){ 150 | case 'statement': 151 | if(node.type.slice(-9) === 'Statement') return true; 152 | // fallthrough: interface Declaration <: Statement { } 153 | case 'declaration': 154 | return node.type.slice(-11) === 'Declaration'; 155 | case 'pattern': 156 | if(node.type.slice(-7) === 'Pattern') return true; 157 | // fallthrough: interface Expression <: Node, Pattern { } 158 | case 'expression': 159 | return node.type.slice(-10) === 'Expression' || 160 | node.type === 'Literal' || 161 | node.type === 'Identifier'; 162 | case 'function': 163 | return node.type.slice(0, 8) === 'Function' || 164 | node.type === 'ArrowFunctionExpression'; 165 | } 166 | throw new Error('Unknown class name: ' + selector.name); 167 | } 168 | 169 | throw new Error('Unknown selector type: ' + selector.type); 170 | } 171 | 172 | /* 173 | * Determines if the given node has a sibling that matches the given selector. 174 | */ 175 | function sibling(node, selector, ancestry) { 176 | var parent = ancestry[0], listProp, keys, i, l, k, m; 177 | if (!parent) { return false; } 178 | keys = estraverse.VisitorKeys[parent.type]; 179 | for (i = 0, l = keys.length; i < l; ++i) { 180 | listProp = parent[keys[i]]; 181 | if (isArray(listProp)) { 182 | for (k = 0, m = listProp.length; k < m; ++k) { 183 | if (listProp[k] !== node && matches(listProp[k], selector, ancestry)) { 184 | return true; 185 | } 186 | } 187 | } 188 | } 189 | return false; 190 | } 191 | 192 | /* 193 | * Determines if the given node has an asjacent sibling that matches the given selector. 194 | */ 195 | function adjacent(node, selector, ancestry) { 196 | var parent = ancestry[0], listProp, keys, i, l, idx; 197 | if (!parent) { return false; } 198 | keys = estraverse.VisitorKeys[parent.type]; 199 | for (i = 0, l = keys.length; i < l; ++i) { 200 | listProp = parent[keys[i]]; 201 | if (isArray(listProp)) { 202 | idx = listProp.indexOf(node); 203 | if (idx < 0) { continue; } 204 | if (idx > 0 && matches(listProp[idx - 1], selector, ancestry)) { 205 | return true; 206 | } 207 | if (idx < listProp.length - 1 && matches(listProp[idx + 1], selector, ancestry)) { 208 | return true; 209 | } 210 | } 211 | } 212 | return false; 213 | } 214 | 215 | /* 216 | * Determines if the given node is the nth child, determined by idxFn, which is given the containing list's length. 217 | */ 218 | function nthChild(node, ancestry, idxFn) { 219 | var parent = ancestry[0], listProp, keys, i, l, idx; 220 | if (!parent) { return false; } 221 | keys = estraverse.VisitorKeys[parent.type]; 222 | for (i = 0, l = keys.length; i < l; ++i) { 223 | listProp = parent[keys[i]]; 224 | if (isArray(listProp)) { 225 | idx = listProp.indexOf(node); 226 | if (idx >= 0 && idx === idxFn(listProp.length)) { return true; } 227 | } 228 | } 229 | return false; 230 | } 231 | 232 | /* 233 | * For each selector node marked as a subject, find the portion of the selector that the subject must match. 234 | */ 235 | function subjects(selector, ancestor) { 236 | var results, p; 237 | if (selector == null || typeof selector != 'object') { return []; } 238 | if (ancestor == null) { ancestor = selector; } 239 | results = selector.subject ? [ancestor] : []; 240 | for(p in selector) { 241 | if(!{}.hasOwnProperty.call(selector, p)) { continue; } 242 | [].push.apply(results, subjects(selector[p], p === 'left' ? selector[p] : ancestor)); 243 | } 244 | return results; 245 | } 246 | 247 | /** 248 | * From a JS AST and a selector AST, collect all JS AST nodes that match the selector. 249 | */ 250 | function match(ast, selector) { 251 | var ancestry = [], results = [], altSubjects, i, l, k, m; 252 | if (!selector) { return results; } 253 | altSubjects = subjects(selector); 254 | estraverse.traverse(ast, { 255 | enter: function (node, parent) { 256 | if (parent != null) { ancestry.unshift(parent); } 257 | if (matches(node, selector, ancestry)) { 258 | if (altSubjects.length) { 259 | for (i = 0, l = altSubjects.length; i < l; ++i) { 260 | if (matches(node, altSubjects[i], ancestry)) { results.push(node); } 261 | for (k = 0, m = ancestry.length; k < m; ++k) { 262 | if (matches(ancestry[k], altSubjects[i], ancestry.slice(k + 1))) { 263 | results.push(ancestry[k]); 264 | } 265 | } 266 | } 267 | } else {+ 268 | results.push(node); 269 | } 270 | } 271 | }, 272 | leave: function () { ancestry.shift(); } 273 | }); 274 | return results; 275 | } 276 | 277 | /** 278 | * Parse a selector string and return its AST. 279 | */ 280 | function parse(selector) { 281 | return parser.parse(selector); 282 | } 283 | 284 | /** 285 | * Query the code AST using the selector string. 286 | */ 287 | function query(ast, selector) { 288 | return match(ast, parse(selector)); 289 | } 290 | 291 | query.parse = parse; 292 | query.match = match; 293 | query.matches = matches; 294 | return query.query = query; 295 | } 296 | 297 | 298 | if (typeof define === "function" && define.amd) { 299 | define(esqueryModule); 300 | } else if (typeof module !== 'undefined' && module.exports) { 301 | module.exports = esqueryModule(); 302 | } else { 303 | this.esquery = esqueryModule(); 304 | } 305 | 306 | })(); 307 | -------------------------------------------------------------------------------- /esrecurse/esrecurse.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2014 Yusuke Suzuki 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 22 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | */ 24 | (function () { 25 | 'use strict'; 26 | 27 | var assign, 28 | estraverse, 29 | isArray, 30 | objectKeys; 31 | 32 | assign = require('lodash.assign'); 33 | estraverse = require('../estraverse/estraverse'); 34 | 35 | isArray = Array.isArray || function isArray(array) { 36 | return Object.prototype.toString.call(array) === '[object Array]'; 37 | }; 38 | 39 | objectKeys = Object.keys || function (o) { 40 | var keys = [], key; 41 | for (key in o) { 42 | keys.push(key); 43 | } 44 | return keys; 45 | }; 46 | 47 | function isNode(node) { 48 | if (node == null) { 49 | return false; 50 | } 51 | return typeof node === 'object' && typeof node.type === 'string'; 52 | } 53 | 54 | function isProperty(nodeType, key) { 55 | return (nodeType === estraverse.Syntax.ObjectExpression || nodeType === estraverse.Syntax.ObjectPattern) && key === 'properties'; 56 | } 57 | 58 | function Visitor(visitor, options) { 59 | options = options || {}; 60 | 61 | this.__visitor = visitor || this; 62 | this.__childVisitorKeys = options.childVisitorKeys 63 | ? assign({}, estraverse.VisitorKeys, options.childVisitorKeys) 64 | : estraverse.VisitorKeys; 65 | if (options.fallback === 'iteration') { 66 | this.__fallback = objectKeys; 67 | } else if (typeof options.fallback === 'function') { 68 | this.__fallback = options.fallback; 69 | } 70 | } 71 | 72 | /* Default method for visiting children. 73 | * When you need to call default visiting operation inside custom visiting 74 | * operation, you can use it with `this.visitChildren(node)`. 75 | */ 76 | Visitor.prototype.visitChildren = function (node) { 77 | var type, children, i, iz, j, jz, child; 78 | 79 | if (node == null) { 80 | return; 81 | } 82 | 83 | type = node.type || estraverse.Syntax.Property; 84 | 85 | children = this.__childVisitorKeys[type]; 86 | if (!children) { 87 | if (this.__fallback) { 88 | children = this.__fallback(node); 89 | } else { 90 | throw new Error('Unknown node type ' + type + '.'); 91 | } 92 | } 93 | 94 | for (i = 0, iz = children.length; i < iz; ++i) { 95 | child = node[children[i]]; 96 | if (child) { 97 | if (isArray(child)) { 98 | for (j = 0, jz = child.length; j < jz; ++j) { 99 | if (child[j]) { 100 | if (isNode(child[j]) || isProperty(type, children[i])) { 101 | this.visit(child[j]); 102 | } 103 | } 104 | } 105 | } else if (isNode(child)) { 106 | this.visit(child); 107 | } 108 | } 109 | } 110 | }; 111 | 112 | /* Dispatching node. */ 113 | Visitor.prototype.visit = function (node) { 114 | var type; 115 | 116 | if (node == null) { 117 | return; 118 | } 119 | 120 | type = node.type || estraverse.Syntax.Property; 121 | if (this.__visitor[type]) { 122 | this.__visitor[type].call(this, node); 123 | return; 124 | } 125 | this.visitChildren(node); 126 | }; 127 | 128 | exports.Visitor = Visitor; 129 | exports.visit = function (node, visitor, options) { 130 | var v = new Visitor(visitor, options); 131 | v.visit(node); 132 | }; 133 | }()); 134 | /* vim: set sw=4 ts=4 et tw=80 : */ 135 | -------------------------------------------------------------------------------- /examples/counter-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Counter Example 4 | 9 | 10 | 11 | 12 |

Counter Example

13 |
14 | 15 | -------------------------------------------------------------------------------- /examples/counter-example/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | // const node = document.createElement('div'); 6 | // document.body.appendChild(node); 7 | render(, document.getElementById('root')); -------------------------------------------------------------------------------- /examples/counter-example/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Counter from './Counter'; 3 | 4 | export default class App extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | counters: [], 9 | } 10 | this.handleAddCounter = this.handleAddCounter.bind(this); 11 | } 12 | 13 | handleAddCounter() { 14 | const counters = this.state.counters.slice(); 15 | counters.push(0); 16 | this.setState({ counters }); 17 | } 18 | 19 | handleDecrement(i) { 20 | const counters = this.state.counters.slice(); 21 | counters[i]--; 22 | this.setState({ counters }); 23 | } 24 | 25 | handleIncrement(i) { 26 | const counters = this.state.counters.slice(); 27 | counters[i]++; 28 | this.setState({ counters }); 29 | } 30 | 31 | render() { 32 | const renderArr = this.state.counters.map((count, i) => { 33 | return 40 | }); 41 | return (
42 | 46 | { renderArr } 47 |
); 48 | } 49 | } -------------------------------------------------------------------------------- /examples/counter-example/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Counter = ({ 4 | header, 5 | count, 6 | onDecrement, 7 | onIncrement, 8 | }) => ( 9 |
10 |

Counter {header}

11 |

Count: {count}

12 | 13 | 14 |
15 | ); 16 | 17 | Counter.propTypes = { 18 | header: React.PropTypes.number.isRequired, 19 | count: React.PropTypes.number.isRequired 20 | }; 21 | 22 | export default Counter; -------------------------------------------------------------------------------- /examples/counter-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | output: { 6 | path: __dirname, 7 | filename: 'bundle.js', 8 | }, 9 | resolve: { 10 | extensions: ['', '.js', '.jsx'], 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.jsx?$/, 16 | loaders: ['babel-loader'], 17 | exclude: /node_modules/, 18 | }, 19 | ], 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 24 | }), 25 | new webpack.optimize.UglifyJsPlugin({ 26 | mangle: false, 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /examples/recipe/app/components/CardAdd.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var CardAdd = React.createClass({ 4 | render: function(){ 5 | return
6 |

Add a Recipe

7 |
8 | 16 | 25 | 32 |
33 | Save 34 | Cancel 35 |
36 |
37 |
38 | }, 39 | closeHandler: function(){ 40 | 41 | this.props.closeClickHandler(); 42 | } 43 | }); 44 | 45 | export default CardAdd; -------------------------------------------------------------------------------- /examples/recipe/app/components/CardBack.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var CardBack = React.createClass({ 4 | getInitialState: function(){ 5 | return { 6 | title: this.props.data.title, 7 | ingredients: this.props.data.ingredients, 8 | imageUrl: this.props.data.imageUrl 9 | } 10 | }, 11 | render: function(){ 12 | return
13 |

Edit Recipe

14 |
15 | 23 | 30 | 37 |
38 | Save 39 | Undo 40 |
41 |
42 |
43 | }, 44 | //change this so that the next 45 | handleTitleChange: function(e){ 46 | this.setState({title: e.target.value}) 47 | }, 48 | handleIngredientsChange: function(e){ 49 | this.setState({ingredients: e.target.value.split(',')}) 50 | }, 51 | handleImageUrlChange: function(e){ 52 | this.setState({imageUrl: e.target.value}) 53 | }, 54 | handleSave: function(){ 55 | this.props.saveTitle(this.state.title); 56 | this.props.saveIngredients(this.state.ingredients); 57 | this.props.saveImageUrl(this.state.imageUrl); 58 | this.props.toggleEditMode(); 59 | }, 60 | handleUndo: function(){ 61 | this.setState({ 62 | title: this.props.data.title, 63 | ingredients: this.props.data.ingredients 64 | }); 65 | } 66 | }); 67 | 68 | export default CardBack; -------------------------------------------------------------------------------- /examples/recipe/app/components/CardFront.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var CardFront = React.createClass({ 4 | render: function(){ 5 | 6 | var cardImageStyle = { 7 | background: 'url(' + this.props.data.imageUrl + ') center center no-repeat', 8 | backgroundSize: 'cover' 9 | }; 10 | 11 | var createIngredients = function(item, i){ 12 | return
  • 13 | {item} 14 |
  • 15 | }; 16 | return
    17 |
    18 |
    19 |
    20 |

    {this.props.data.title}

    21 |
      22 | {this.props.data.ingredients.map(createIngredients)} 23 |
    24 |
    25 |
    26 | Edit 27 | Delete 28 |
    29 |
    30 |
    31 | } 32 | }); 33 | 34 | export default CardFront; -------------------------------------------------------------------------------- /examples/recipe/app/components/EditableCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardFront from './CardFront'; 3 | import CardBack from './CardBack'; 4 | 5 | var EditableCard = React.createClass({ 6 | render: function(){ 7 | return
    8 | {(this.props.totalCards < 2) ? null :
    } 9 |
    10 | 15 | 20 |
    21 | {(this.props.totalCards < 3) ? null :
    } 22 |
    23 | } 24 | 25 | }); 26 | 27 | export default EditableCard; -------------------------------------------------------------------------------- /examples/recipe/app/components/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from './MenuItem'; 3 | 4 | var Menu = React.createClass({ 5 | render: function(){ 6 | var disabledStyle = { 7 | opacity: "0.5", 8 | cursor: "default" 9 | }; 10 | 11 | var createMenuItems = function(item,i){ 12 | return 21 | }.bind(this); 22 | 23 | return
    25 |
      26 | {this.props.data.map(createMenuItems)} 27 |
    • 32 | Add 33 |
    • 34 |
    35 | 36 |
    37 | }, 38 | handleClick: function(){ 39 | this.props.addClickHandler(); 40 | } 41 | }); 42 | 43 | export default Menu; -------------------------------------------------------------------------------- /examples/recipe/app/components/MenuItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var MenuItem = React.createClass({ 4 | render: function(){ 5 | 6 | var disabledStyle = { 7 | opacity: "0.5", 8 | cursor: "default" 9 | }; 10 | 11 | var isActiveItem = (this.props.itemIdx === this.props.activeCardIdx) && this.props.isEditMode; 12 | var isDisabled = this.props.isAddMode || this.props.isEditMode; 13 | return (
  • 18 | {this.props.data.title} 19 |
  • ) 20 | }, 21 | handleClick: function(){ 22 | var isDisabled = this.props.isAddMode || this.props.isEditMode; 23 | if (!isDisabled){ 24 | this.props.clickHandler(this.props.itemIdx) 25 | } 26 | } 27 | }); 28 | 29 | export default MenuItem; -------------------------------------------------------------------------------- /examples/recipe/app/components/RecipeBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EditableCard from './EditableCard'; 3 | import CardAdd from './CardAdd'; 4 | import Menu from './Menu'; 5 | 6 | var helpers = { 7 | //localStorage test helper from https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API 8 | storageAvailable: function(type) { 9 | try { 10 | var storage = window[type], 11 | x = '__storage_test__'; 12 | storage.setItem(x, x); 13 | storage.removeItem(x); 14 | return true; 15 | } 16 | catch(e) { 17 | return false; 18 | } 19 | } 20 | }; 21 | 22 | 23 | var testData = [ 24 | { 25 | title: 'Banana Bread', 26 | ingredients: ['140g softened butter','140g caster sugar','2 large eggs', '140g self-raising flour','1tsp baking powder', '2 very ripe banana', '50g icing powder'], 27 | imageUrl: 'http://cdn.wholelifestylenutrition.com/wp-content/uploads/Banana-Nut-Bread.jpg' 28 | }, 29 | { 30 | title: 'Chilli Con Carne', 31 | ingredients: ['1tbsp olive oil','1 onion', '2 garlic cloves', '250g beef mince', '500ml beef stock', '1tsp chilli flakes', '400g tin tomatoes', '2 tins red kidney beans', '200g long grain rice'], 32 | imageUrl: 'http://www.bbcgoodfood.com/sites/default/files/recipe_images/recipe-image-legacy-id--1001451_6.jpg' 33 | }, 34 | { 35 | title: 'Sausage & Bean Caserole', 36 | ingredients: ['12 pork sausages', '6 rashers streaky bacon', '2 onions', '400g can tomatoes', '300ml chicken stock', '400g mixed beans'], 37 | imageUrl: 'http://www.bbcgoodfood.com/sites/default/files/recipe_images/recipe-image-legacy-id--901576_11.jpg' 38 | }, 39 | { 40 | title: 'Gravy', 41 | ingredients: ['meat juices', '2 tbsp liquid meat fat', '1 pint stock', '2tsp gravy browning'], 42 | imageUrl: 'http://40.media.tumblr.com/086f28f810a04d33cd6a685eabdb164d/tumblr_inline_nmjcqrBk7j1tr4e0j_1280.jpg' 43 | } 44 | ]; 45 | 46 | 47 | var RecipeBox = React.createClass({ 48 | getInitialState: function(){ 49 | return { 50 | editMode: false, 51 | addMode: false, 52 | activeCardIdx: 0, 53 | data: testData, 54 | nextItem: { 55 | title: '', 56 | ingredients: [], 57 | imageUrl: '' 58 | } 59 | }; 60 | }, 61 | componentWillMount: function(){ 62 | var storageAvailable = helpers.storageAvailable; 63 | if (storageAvailable('localStorage')) { 64 | if (!localStorage.thepetedRecipes){ 65 | localStorage.thepetedRecipes = JSON.stringify(this.state.data); 66 | } else { 67 | var obj = JSON.parse(localStorage.thepetedRecipes) 68 | this.setState({data: obj}); 69 | } 70 | } 71 | else { 72 | return; 73 | } 74 | }, 75 | render: function(){ 76 | var filterByActiveIndex = function(data,i){ 77 | return i === this.state.activeCardIdx 78 | }.bind(this); 79 | 80 | var makeEditableCard = function(card,i){ 81 | return 93 | }.bind(this); 94 | 95 | var disabledStyle = { 96 | opacity: "0.3", 97 | cursor: "default" 98 | }; 99 | 100 | return (
    101 |
    102 |
    103 |
    104 | 108 | 109 |
    110 |
    111 | {(this.state.data.length 112 | ? !this.state.addMode && 113 | this.state.data.filter(filterByActiveIndex).map(makeEditableCard) 114 | : No recipes!)} 115 | 124 |
    125 |
    126 | 130 | 131 |
    132 |
    133 |
    134 | 135 | 136 | 144 |
    ) 145 | }, 146 | changeActiveCard: function(idx){ 147 | this.setState({ 148 | activeCardIdx: idx, 149 | addMode: false 150 | }) 151 | }, 152 | cycleLeft: function(){ 153 | var modifier; 154 | if (!this.state.addMode && !this.state.editMode) { 155 | if (this.state.activeCardIdx === 0) { 156 | modifier = this.state.data.length - 1; 157 | } else { 158 | modifier = -1; 159 | } 160 | this.setState({ 161 | activeCardIdx: this.state.activeCardIdx + modifier 162 | }) 163 | } 164 | }, 165 | cycleRight: function(){ 166 | var modifier; 167 | if (!this.state.addMode && !this.state.editMode) { 168 | if (this.state.activeCardIdx === this.state.data.length - 1) { 169 | modifier = 0; 170 | } else { 171 | modifier = this.state.activeCardIdx + 1; 172 | } 173 | this.setState({ 174 | activeCardIdx: modifier 175 | }) 176 | } 177 | }, 178 | updateRecipeIngredients: function(ingredients){ 179 | var idx = this.state.activeCardIdx; 180 | 181 | var obj = this.state.data; 182 | obj[idx].ingredients = ingredients; 183 | 184 | this.setState({ 185 | data: obj 186 | },this.updateLocalStorage); 187 | }, 188 | updateImageUrl: function(imageUrl){ 189 | var idx = this.state.activeCardIdx; 190 | 191 | var obj = this.state.data; 192 | obj[idx].imageUrl = imageUrl; 193 | 194 | this.setState({ 195 | data: obj 196 | },this.updateLocalStorage); 197 | }, 198 | updateRecipeTitle: function(title){ 199 | var idx = this.state.activeCardIdx; 200 | 201 | var obj = this.state.data; 202 | obj[idx].title = title; 203 | 204 | this.setState({ 205 | data: obj 206 | },this.updateLocalStorage); 207 | }, 208 | updateNextTitle: function(e){ 209 | var obj = this.state.nextItem; 210 | obj.title = e.target.value; 211 | this.setState({ 212 | nextItem: obj 213 | }) 214 | }, 215 | updateNextIngredients: function(e){ 216 | var obj = this.state.nextItem; 217 | obj.ingredients = e.target.value.split(','); 218 | this.setState({ 219 | nextItem: obj 220 | }) 221 | }, 222 | updateNextImageUrl: function(e){ 223 | var obj = this.state.nextItem; 224 | obj.imageUrl = e.target.value; 225 | this.setState({ 226 | nextItem: obj 227 | }) 228 | }, 229 | addRecipe: function(){ 230 | var placeholder = 'http://www.jamesbeard.org/sites/default/files/styles/recipe_335x285/public/default_images/recipe_placeholder.jpg?itok=10fpzziS'; 231 | var recipes = this.state.data; 232 | var nextRecipe = { 233 | title: this.state.nextItem.title || 'Untitled', 234 | ingredients: 235 | this.state.nextItem.ingredients.length 236 | ? this.state.nextItem.ingredients 237 | : ['No ingredients yet!'], 238 | imageUrl: this.state.nextItem.imageUrl || placeholder 239 | }; 240 | 241 | recipes.push(nextRecipe); 242 | this.setState({ 243 | addMode: false, 244 | data: recipes, 245 | nextItem: { 246 | title: '', 247 | ingredients: [], 248 | imageUrl: '' 249 | }, 250 | activeCardIdx: this.state.data.length - 1 251 | },this.updateLocalStorage); 252 | }, 253 | updateLocalStorage: function(){ 254 | var storageAvailable = helpers.storageAvailable; 255 | if (storageAvailable('localStorage')) { 256 | localStorage.thepetedRecipes = JSON.stringify(this.state.data); 257 | } 258 | else { 259 | return; 260 | } 261 | }, 262 | removeRecipe: function(){ 263 | var cpArray = this.state.data.slice(0); 264 | cpArray.splice(this.state.activeCardIdx, 1); 265 | this.setState({ 266 | data: cpArray, 267 | activeCardIdx: Math.max(this.state.activeCardIdx - 1, 0) 268 | },this.updateLocalStorage); 269 | }, 270 | toggleAddMode: function(title){ 271 | this.setState({ 272 | addMode: !this.state.addMode, 273 | nextItem: { 274 | title: '', 275 | ingredients: [], 276 | imageUrl: '' 277 | } 278 | }) 279 | }, 280 | toggleEditMode: function(){ 281 | this.setState({ 282 | editMode: !this.state.editMode 283 | }) 284 | } 285 | }); 286 | 287 | export default RecipeBox; 288 | 289 | -------------------------------------------------------------------------------- /examples/recipe/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import RecipeBox from './components/RecipeBox'; 4 | 5 | var App = React.createClass({ 6 | render: function() { 7 | return (
    8 |

    Recipe Book

    9 | 10 |
    ) 11 | } 12 | }); 13 | 14 | render(, document.getElementById('container')); -------------------------------------------------------------------------------- /examples/recipe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 282 | 283 | 284 |
    285 | 286 | 287 | -------------------------------------------------------------------------------- /examples/recipe/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './app/index.jsx', 6 | output: { 7 | path: 'public', 8 | filename: 'bundle.js', 9 | }, 10 | resolve: { 11 | extensions: ['', '.js', '.jsx'], 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.jsx?$/, 17 | loaders: ['babel-loader'], 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ NODE_ENV: 'production' }), 24 | new webpack.optimize.DedupePlugin(), 25 | new webpack.optimize.UglifyJsPlugin({ 26 | mangle: false, 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /examples/todoApp/app/components/List.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from './ListItem' 3 | 4 | export default class List extends React.Component { 5 | 6 | render() { 7 | return ( 8 |
      9 | {this.props.items.map((itemText, i) => 10 | 14 | )} 15 |
    16 | ); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/todoApp/app/components/ListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ListItem extends React.Component { 4 | 5 | render() { 6 | return
  • 7 | {this.props.text} 8 |
  • 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /examples/todoApp/app/components/TODO.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoForm from './TodoForm'; 3 | import List from './List.jsx'; 4 | 5 | 6 | export default class TODO extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = {items: [ 11 | 'Build splash page', 12 | 'Deploy app', 13 | ]}; 14 | } 15 | 16 | render() { 17 | return ( 18 |
    19 | 22 | 25 |
    26 | ); 27 | } 28 | 29 | addItem(item) { 30 | this.setState({items: this.state.items.concat([item])}); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /examples/todoApp/app/components/TodoForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TodoForm extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = {item: ''}; 8 | } 9 | 10 | render() { 11 | return ( 12 |
    13 | 14 | 15 | 16 |
    17 | ); 18 | } 19 | 20 | handleSubmit(e) { 21 | e.preventDefault(); 22 | this.props.onFormSubmit(this.state.item); 23 | this.setState({item: ''}); 24 | React.findDomNode(this.refs.item).focus(); 25 | } 26 | 27 | onChange(e) { 28 | this.setState({ 29 | item: e.target.value 30 | }); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /examples/todoApp/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import TODO from './components/TODO.jsx'; 4 | 5 | 6 | class App extends React.Component { 7 | 8 | render () { 9 | return ( 10 |
    11 |

    My Todo's

    12 | 13 |
    14 | ) 15 | } 16 | } 17 | 18 | render(, document.getElementById('app')); 19 | -------------------------------------------------------------------------------- /examples/todoApp/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './app/index.jsx', 5 | output: { 6 | path: 'public', 7 | filename: 'bundle.js', 8 | }, 9 | resolve: { 10 | extensions: ['', '.js', '.jsx'], 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.jsx?$/, 16 | loaders: ['babel-loader'], 17 | exclude: /node_modules/, 18 | }, 19 | ], 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 24 | }), 25 | new webpack.optimize.DedupePlugin(), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | mangle: false, 28 | }), 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /examples/todoApp2/app/components/List.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from './ListItem' 3 | 4 | export default class List extends React.Component { 5 | 6 | 7 | render() { 8 | const itemsArr = this.props.items.map((item, i) => { 9 | return 15 | }); 16 | return ( 17 |
    18 | {itemsArr} 19 |
    20 | ); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /examples/todoApp2/app/components/ListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | const ListItem = ({ 5 | id, 6 | text, 7 | onDelete, 8 | }) => ( 9 |
    10 | {text} 11 | 12 |
    13 | ) 14 | 15 | ListItem.propTypes = { 16 | id: React.PropTypes.number.isRequired, 17 | } 18 | 19 | export default ListItem; 20 | -------------------------------------------------------------------------------- /examples/todoApp2/app/components/TODO.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoForm from './TodoForm'; 3 | import List from './List.jsx'; 4 | 5 | 6 | export default class TODO extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | items: [ 12 | 'Make splash page', 13 | 'Run unit tests', 14 | 'Deploy app', 15 | ] 16 | }; 17 | this.addItem = this.addItem.bind(this); 18 | this.handleDelete = this.handleDelete.bind(this) 19 | } 20 | 21 | render() { 22 | return ( 23 |
    24 | 28 | 31 |
    32 | ); 33 | } 34 | 35 | addItem(item) { 36 | const items = this.state.items.slice(); 37 | items.push(item); 38 | this.setState({ items }); 39 | } 40 | 41 | handleDelete(i) { 42 | const items = this.state.items.slice(); 43 | items.splice(i, 1); 44 | console.log(i); 45 | this.setState({ items }); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /examples/todoApp2/app/components/TodoForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TodoForm extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = {item: ''}; 8 | } 9 | 10 | render() { 11 | return ( 12 |
    13 | 14 | 15 | 16 |
    17 | ); 18 | } 19 | 20 | handleSubmit(e) { 21 | e.preventDefault(); 22 | this.props.onFormSubmit(this.state.item); 23 | this.setState({item: ''}); 24 | } 25 | 26 | onChange(e) { 27 | this.setState({ 28 | item: e.target.value 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /examples/todoApp2/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import TODO from './components/TODO.jsx'; 4 | 5 | 6 | class App extends React.Component { 7 | 8 | render () { 9 | return ( 10 |
    11 |

    My Todo's

    12 | 13 |
    14 | ) 15 | } 16 | } 17 | 18 | render(, document.getElementById('app')); 19 | -------------------------------------------------------------------------------- /examples/todoApp2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Counter Example 4 | 9 | 10 | 11 | 12 |

    Counter Example

    13 |
    14 | 15 | -------------------------------------------------------------------------------- /examples/todoApp2/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './app/index.jsx', 6 | output: { 7 | path: 'public', 8 | filename: 'bundle.js', 9 | }, 10 | resolve: { 11 | extensions: ['', '.js', '.jsx'], 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.jsx?$/, 17 | loaders: ['babel-loader'], 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ NODE_ENV: 'production' }), 24 | new webpack.optimize.DedupePlugin(), 25 | new webpack.optimize.UglifyJsPlugin({ 26 | mangle: false, 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 34 | 35 | 36 |
    37 |
    38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-monocle", 3 | "version": "0.1.6", 4 | "description": "A developer tool to visualize a React application's component hierarchy.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --progress --colors --config webpack.production.config.js --watch", 8 | "lint": "eslint src", 9 | "unit-tests": "mocha --compilers js:babel-core/register ./src/test/test.js", 10 | "start": "nodemon server/server.js", 11 | "test": "npm run lint || true && npm run unit-tests", 12 | "bundle-d3tree": "webpack --watch -d ./src/d3Tree/index.js ./src/d3Tree/bundle.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/team-gryff/react-monocle.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "visualization", 21 | "d3", 22 | "developer", 23 | "tool" 24 | ], 25 | "author": "Michael-Bryant Choa , Jenna Davis , Jerry Mao ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/team-gryff/react-monocle/issues" 29 | }, 30 | "homepage": "https://github.com/team-gryff/react-monocle#readme", 31 | "bin": { 32 | "monocle": "./bin/cmd.js" 33 | }, 34 | "babel": { 35 | "presets": [ 36 | "airbnb", 37 | "react", 38 | "es2015", 39 | "stage-1" 40 | ], 41 | "plugins": [ 42 | "transform-runtime" 43 | ] 44 | }, 45 | "dependencies": { 46 | "acorn": "^3.2.0", 47 | "acorn-bfs": "^0.1.0", 48 | "acorn-jsx": "^3.0.1", 49 | "app-root-path": "^1.2.1", 50 | "commander": "^2.9.0", 51 | "escodegen": "^1.8.0", 52 | "glob": "^7.0.5", 53 | "lodash.assign": "^4.0.9", 54 | "lodash.clonedeep": "^4.3.2", 55 | "strip-comments": "^0.4.4" 56 | }, 57 | "devDependencies": { 58 | "babel-cli": "^6.9.0", 59 | "babel-core": "^6.9.0", 60 | "babel-eslint": "^6.0.4", 61 | "babel-loader": "^6.2.4", 62 | "babel-plugin-transform-runtime": "^6.9.0", 63 | "babel-polyfill": "^6.9.0", 64 | "babel-preset-airbnb": "^2.0.0", 65 | "babel-preset-es2015": "^6.9.0", 66 | "babel-preset-react": "^6.5.0", 67 | "babel-preset-stage-1": "^6.5.0", 68 | "babel-register": "^6.9.0", 69 | "babel-runtime": "^6.9.2", 70 | "chai": "^3.5.0", 71 | "d3": "^4.1.1", 72 | "eslint": "^2.9.0", 73 | "eslint-config-airbnb": "^9.0.1", 74 | "eslint-plugin-import": "^1.10.0", 75 | "eslint-plugin-jsx-a11y": "^1.5.3", 76 | "eslint-plugin-react": "^5.2.2", 77 | "express": "^4.13.4", 78 | "jsdom": "^9.2.1", 79 | "lodash.isequal": "^4.2.0", 80 | "mocha": "^2.5.3", 81 | "nodemon": "^1.9.2", 82 | "react": "^15.1.0", 83 | "react-addons-test-utils": "^15.1.0", 84 | "react-dom": "^15.1.0", 85 | "react-hot-loader": "^1.3.0", 86 | "react-popover": "^0.4.4", 87 | "rebass": "^0.3.0", 88 | "redux": "^3.5.2", 89 | "svg-inline-loader": "^0.6.1", 90 | "uglify-loader": "^1.3.0", 91 | "webpack": "^1.13.1", 92 | "webpack-dev-middleware": "^1.6.1", 93 | "webpack-dev-server": "^1.14.1", 94 | "webpack-hot-middleware": "^2.10.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /react/actions/index.js: -------------------------------------------------------------------------------- 1 | const updateState = (name, state) => ({ 2 | type: 'UPDATE_STATE', 3 | name, 4 | state, 5 | }); 6 | 7 | const sendInitialState = (name, obj) => ({ 8 | type: 'INITIALIZE_STATE', 9 | name, 10 | obj, 11 | }); 12 | 13 | module.exports = { 14 | updateState, 15 | sendInitialState, 16 | }; 17 | -------------------------------------------------------------------------------- /react/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 17 | 21 | 23 | 24 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /react/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/team-gryff/react-monocle/b90f26e2e6440d7e8f2e596237ec1ed598827453/react/assets/logo.png -------------------------------------------------------------------------------- /react/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 14 | 26 | 40 | 51 | 56 | 58 | 60 | 72 | 83 | 84 | 86 | 98 | 99 | 100 | 104 | 109 | 113 | 115 | 116 | 119 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /react/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Graph from './Graph.jsx'; 3 | import logo from '../assets/logo.svg'; 4 | const cloneDeep = require('lodash.clonedeep'); 5 | 6 | // const formatted = require('../../formattedDataNoState'); 7 | 8 | 9 | 10 | 11 | class App extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = {}; 15 | this.treebuilder = this.treebuilder.bind(this); 16 | } 17 | 18 | componentWillMount() { 19 | return this.setState(this.props.store); 20 | } 21 | 22 | 23 | componentWillReceiveProps(nextProps) { 24 | const result = {}; 25 | for (const key in nextProps.store) { 26 | if (this.props.store.hasOwnProperty(key)) { 27 | result[key] = Object.assign({}, this.state[key], nextProps.store[key]); 28 | } else { 29 | result[key] = nextProps.store[key]; 30 | } 31 | } 32 | this.setState(Object.assign({}, this.state, result)); 33 | } 34 | 35 | treebuilder(state) { 36 | const result = cloneDeep(formatted[formatted.monocleENTRY]); 37 | const bfs = this.bfs; 38 | 39 | function treeRecurse(node, root, newState) { 40 | if (!node.children) throw new Error('Invalid Node! Something went wrong with the parsing (no children array)'); 41 | if (newState[node.name]) node.state = cloneDeep(newState[node.name]); 42 | 43 | if (node.children.length === 0) return; // base case 44 | const tempChildren = []; 45 | 46 | // iterating through children 47 | for (let j = 0; j < node.children.length; j++) { 48 | const child = cloneDeep(node.children[j]); 49 | 50 | // maybe check if it is object already 51 | if (formatted.hasOwnProperty(child.name)) child.children = cloneDeep(formatted[child.name].children); // adding children of child 52 | 53 | 54 | if (!Array.isArray(child.props)) { 55 | tempChildren.push(child); 56 | continue; 57 | } 58 | 59 | 60 | // if child is not made through an iterator 61 | if (!child.iterated) { 62 | const propsObj = {}; 63 | 64 | // iterating through props to parse 65 | child.props.forEach(ele => { 66 | // if it has a prop or state find source and parse 67 | if (typeof ele.value === 'string' && ele.value.search(/(^props.|^state.)/) !== -1) { 68 | propsObj[ele.name] = eval(`node.${ele.value}`); 69 | } else propsObj[ele.name] = ele.value; // else just return the value 70 | }); 71 | child.props = propsObj; 72 | tempChildren.push(child); 73 | 74 | // if it is made through an iterator 75 | } else { 76 | switch (child.iterated) { 77 | case 'forIn': 78 | for (const key in eval(`node.${child.source}`)) { 79 | const forInChild = cloneDeep(child); 80 | const propsObj = {}; 81 | 82 | forInChild.props.forEach(ele => { 83 | if (typeof ele.value === 'string' && ele.value.search(/(^props.|^state.)/) !== -1) { 84 | propsObj[ele.name] = eval(`node.${ele.value}`); 85 | } else if (ele.value.includes(key) && ele.value.search(/\wkey\w/) === -1) propsObj[ele.name] = ele.value.replace('key', key); 86 | else propsObj[ele.name] = ele.value; 87 | }); 88 | 89 | forInChild.props = propsObj; 90 | tempChildren.push(forInChild); 91 | } 92 | break; 93 | 94 | case 'forLoop': 95 | for (var i = 0; i < eval(`node.${child.source}.length`); i++) { 96 | const num = i; 97 | const forLoopChild = cloneDeep(child); 98 | const propsObj = {}; 99 | 100 | forLoopChild.props.forEach(ele => { 101 | if (typeof ele.value === 'string' && ele.value.search(/(^props.|^state.)/) !== -1) { 102 | propsObj[ele.name] = eval(`node.${ele.value}`); 103 | } else if (ele.value.includes('i') && ele.value.search(/\wi\w/) === -1) propsObj[ele.name] = ele.value.replace('i', i); 104 | else propsObj[ele.name] = ele.value; 105 | }); 106 | 107 | forLoopChild.props = propsObj; 108 | forLoopChild.num = num; 109 | tempChildren.push(forLoopChild); 110 | } 111 | break; 112 | 113 | case 'higherOrder': 114 | for (var i = 0; i < eval(`node.${child.source}.length`); i++) { 115 | const num = i; 116 | const forLoopChild = cloneDeep(child); 117 | const propsObj = {}; 118 | 119 | forLoopChild.props.forEach(ele => { 120 | if (typeof ele.value === 'string' && ele.value.search(/(^props.|^state.)/) !== -1) { 121 | propsObj[ele.name] = eval(`node.${ele.value}`); 122 | } else if (ele.value.includes('i') && ele.value.search(/\wi\w/) === -1) propsObj[ele.name] = ele.value.replace('i', i); 123 | else propsObj[ele.name] = ele.value; 124 | }); 125 | 126 | forLoopChild.props = propsObj; 127 | forLoopChild.num = num; 128 | tempChildren.push(forLoopChild); 129 | } 130 | break; 131 | 132 | default: 133 | throw new Error('unrecognized iterator'); 134 | } 135 | } 136 | } 137 | node.children = tempChildren; 138 | node.children.forEach(ele => { 139 | treeRecurse(ele, root, state); 140 | }); 141 | } 142 | 143 | treeRecurse(result, result, state); 144 | return result; 145 | } 146 | 147 | render() { 148 | const builtObj = this.treebuilder(this.state); 149 | const logoStyle = { 150 | width: '200px', 151 | height: '100px', 152 | zIndex: '1', 153 | position: 'absolute', 154 | }; 155 | return ( 156 |
    157 |
    158 | 159 |
    160 | ); 161 | } 162 | } 163 | 164 | App.propTypes = { 165 | store: React.PropTypes.object.isRequired, 166 | }; 167 | 168 | App.defaultProps = { 169 | store: {}, 170 | }; 171 | 172 | 173 | export default App; 174 | -------------------------------------------------------------------------------- /react/components/Graph.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tree, hierarchy, select, path } from 'd3'; 3 | import Node from './Node.jsx'; 4 | const cloneDeep = require('lodash.clonedeep'); 5 | const isEqual = require('lodash.isequal'); 6 | 7 | 8 | class Graph extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | width: 1000, 13 | height: 900, 14 | initialHeight: 900, 15 | nodeW: 121.35, // golden ratio 16 | nodeH: 75, 17 | nodes: [], 18 | }; 19 | this.highlight = this.highlight.bind(this); 20 | this.lowlight = this.lowlight.bind(this); 21 | this.resizeGraph = this.resizeGraph.bind(this); 22 | this.highlightRecursion = this.highlightRecursion.bind(this); 23 | this.nodeRender = this.nodeRender.bind(this); 24 | this.linkRender = this.linkRender.bind(this); 25 | this.resizeGraph = this.resizeGraph.bind(this); 26 | this.propUpdate = this.propUpdate.bind(this); 27 | this.propDoneUpdate = this.propDoneUpdate.bind(this); 28 | } 29 | 30 | /** 31 | * Clone tree data, pass to tree() to create nodes objects, setting state to render nodes, 32 | */ 33 | componentWillMount() { 34 | const nodes = tree().size([window.innerWidth * 0.6, this.state.height])(hierarchy(this.props.treeData)); 35 | return this.nodeRender(nodes); // react render 36 | } 37 | 38 | /** 39 | * add resize event listener to resize graph, render links 40 | */ 41 | componentDidMount() { 42 | window.addEventListener('resize', this.resizeGraph); 43 | this.linkRender(this.state.d3nodes, this.state.d3nodes, 1); // d3 dom injection 44 | } 45 | 46 | componentWillReceiveProps(nextProps) { 47 | const nodes = tree().size([window.innerWidth * 0.6, this.state.height])(hierarchy(nextProps.treeData)); 48 | this.nodeRender(nodes); // react render 49 | } 50 | 51 | componentDidUpdate(prevProps, prevState) { 52 | if (prevState.width !== this.state.width || (this.state.nodes && prevState.nodes && prevState.nodes.length !== this.state.nodes.length)) { 53 | this.linkRender(this.state.d3nodes, prevState.d3nodes, 500); 54 | } 55 | return true; 56 | } 57 | 58 | componentWillUnmount() { 59 | select(document.getElementById('graphz')) 60 | .selectAll('path.link').remove(); 61 | window.removeEventListener('resize', this.resizeGraph); 62 | } 63 | 64 | propUpdate(...i) { 65 | select(document.getElementById('graphz')) 66 | .selectAll('path.link').filter(ele => { 67 | if (i.indexOf(ele.target.id) !== -1) return ele; 68 | return false; 69 | }) 70 | .classed('propchange', true); 71 | } 72 | 73 | propDoneUpdate(...i) { 74 | select(document.getElementById('graphz')) 75 | .selectAll('path.link').filter(ele => { 76 | if (i.indexOf(ele.target.id) !== -1) return ele; 77 | return false; 78 | }) 79 | .classed('propchange', false); 80 | } 81 | 82 | 83 | highlightRecursion(d) { 84 | // finds out which links to highlight 85 | // recurses up to where there is no parent (top most node) 86 | if (!d.parent) return d; 87 | select(document.getElementById('graphz')) 88 | .selectAll('path.link').filter(ele => { 89 | if (ele.source.id === d.parent.id && ele.target.id === d.id) return ele; 90 | return false; 91 | }) 92 | .classed('highlight', true); 93 | return this.highlightRecursion(d.parent); 94 | } 95 | 96 | highlight(i) { 97 | // highlight links on hover over 98 | this.state.d3nodes.each(ele => { 99 | if (ele.id && ele.id === i) this.highlightRecursion(ele); 100 | }); 101 | return true; 102 | } 103 | 104 | lowlight() { 105 | // unhighlight on cursor exit 106 | return select(document.getElementById('graphz')) 107 | .selectAll('path.link') 108 | .classed('highlight', false) 109 | .classed('propchange', false); 110 | } 111 | 112 | nodeRender(nodes) { 113 | let i = 0; 114 | const renderArr = []; 115 | 116 | nodes.each(d => { 117 | d.y = d.depth * this.state.initialHeight / 7; 118 | d.id = d.id || ++i; 119 | // using the information provided by d3 120 | // to render Node components 121 | renderArr.push(); 136 | }); 137 | 138 | // setting state so information can be used in render + other methods 139 | this.setState({ 140 | nodes: renderArr, 141 | d3nodes: nodes, 142 | width: window.innerWidth * 0.6, 143 | }); 144 | return nodes; 145 | } 146 | 147 | 148 | linkRender(nodes, prev, duration) { 149 | const graphz = document.getElementById('graphz'); 150 | const links = nodes.links(); 151 | 152 | const prevPositions = {}; 153 | prev.each(d => { 154 | prevPositions[d.id] = { 155 | x: d.x, 156 | y: d.y, 157 | }; 158 | }); 159 | 160 | // const prev = {}; 161 | // prev.each(d => { 162 | // const source = d.source.id; 163 | // if (!prev.hasOwnProperty(source)) prev[source] = 0; 164 | // prev[source]++; 165 | // }) 166 | // const current = {}; 167 | 168 | select(document.getElementById('graphz')) 169 | .selectAll('path.link').remove(); 170 | 171 | select(document.getElementById('graphz')) 172 | .selectAll('path.link').data(links, d => { return d.target.id; }) 173 | .enter() 174 | .insert('svg:path', 'foreignObject') 175 | .attr('class', 'link') 176 | .attr('d', (node) => { 177 | const pathing = path(); 178 | if (prevPositions.hasOwnProperty(node.source.id) && prevPositions.hasOwnProperty(node.target.id)) { 179 | 180 | const oldX = prevPositions[node.source.id].x; 181 | const oldY = prevPositions[node.source.id].y; 182 | const newX = prevPositions[node.target.id].x; 183 | const newY = prevPositions[node.target.id].y; 184 | pathing.moveTo(oldX + this.state.nodeW / 2, oldY); 185 | pathing.bezierCurveTo(oldX + this.state.nodeW / 2, (oldY + newY) / 2, newX + this.state.nodeW / 2, (oldY + newY) / 2, newX + this.state.nodeW / 2, newY); 186 | return pathing; 187 | } 188 | this.propUpdate(node.source.id, node.target.id); 189 | setTimeout(() => this.propDoneUpdate(node.source.id, node.target.id), 1700); 190 | pathing.moveTo(node.source.x + this.state.nodeW / 2, node.source.y + this.state.nodeH / 2); 191 | pathing.lineTo(node.source.x + this.state.nodeW / 2 + 1, node.source.y + 1); 192 | return pathing; 193 | }) 194 | .transition() 195 | .duration(duration) 196 | .attr('d', (node) => { 197 | // creating a cubic bezier curve for the link 198 | // equiv to d3.svg.diagonal before 4.0 199 | const oldX = node.source.x; 200 | const oldY = node.source.y; 201 | const newX = node.target.x; 202 | const newY = node.target.y; 203 | const pathing = path(); 204 | pathing.moveTo(oldX + this.state.nodeW / 2, oldY); 205 | pathing.bezierCurveTo(oldX + this.state.nodeW / 2, (oldY + newY) / 2, newX + this.state.nodeW / 2, (oldY + newY) / 2, newX + this.state.nodeW / 2, newY); 206 | return pathing; 207 | }); 208 | 209 | if (this.state.height > this.state.currentHeight) this.setState({ height: graphz.getBBox().y + graphz.getBBox().height + 100 }); 210 | } 211 | 212 | resizeGraph() { 213 | // makes sure graph is the right size after rendering the graph 214 | const root = cloneDeep(this.props.treeData); 215 | const nodes = tree().size([this.state.width, this.state.initialHeight])(hierarchy(root)); 216 | this.nodeRender(nodes); 217 | } 218 | 219 | render() { 220 | const divStyle = { 221 | backgroundColor: '#2D2D2D', 222 | paddingTop: '20px', 223 | transform: 'translate(0px, -20px)', 224 | }; 225 | const gStyle = { 226 | transform: 'translate(0px,40px)', 227 | fill: '#969696', 228 | }; 229 | const svgStyle = { 230 | transform: `translate(-${this.state.nodeW / 2}px, 0px)`, 231 | }; 232 | return ( 233 |
    234 | 235 | 236 | {this.state.nodes} 237 | 238 | 239 |
    240 | ); 241 | } 242 | } 243 | 244 | Graph.propTypes = { 245 | treeData: React.PropTypes.object.isRequired, 246 | }; 247 | 248 | Graph.defaultProps = { 249 | treeData: { 250 | name: '', 251 | children: [], 252 | props: [], 253 | methods: [], 254 | state: [], 255 | }, 256 | }; 257 | 258 | export default Graph; 259 | -------------------------------------------------------------------------------- /react/components/Node.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Panel, Block, Heading, Text, Close } from 'rebass'; 3 | import Popover from 'react-popover'; 4 | import NodeUp from './NodeUp.jsx'; 5 | import isEqual from 'lodash.isequal'; 6 | 7 | 8 | class Node extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | isOpen: false, 13 | updating: false, 14 | }; 15 | this.toggle = this.toggle.bind(this); 16 | } 17 | 18 | componentWillReceiveProps(nextProps) { 19 | if (!isEqual(this.props.props, nextProps.props) || !isEqual(this.props.state, nextProps.state)) { 20 | if (!isEqual(this.props.props, nextProps.props)) this.props.propUpdate(); 21 | this.setState({ updating: true }, () => { 22 | setTimeout(() => this.setState({ updating: false }), 1500); 23 | setTimeout(() => this.props.propDoneUpdate(), 1700); 24 | }); 25 | } 26 | } 27 | 28 | toggle() { 29 | // simple toggle function for a popover 30 | const bool = !this.state.isOpen; 31 | return this.setState({ isOpen: bool }); 32 | } 33 | 34 | render() { 35 | // setting background color depending on state 36 | let bgColor = '#FAFAFA'; 37 | let updating; 38 | let propsFontSize = '12px'; 39 | if (Object.keys(this.props.props).length < 7) propsFontSize = '11px'; 40 | else if (Object.keys(this.props.props).length < 10) propsFontSize = '10px'; 41 | else if (Object.keys(this.props.props).length < 13) propsFontSize = '9px'; 42 | else if (Object.keys(this.props.props).length < 16) propsFontSize = '8px'; 43 | if (Object.keys(this.props.state).length !== 0) bgColor = '#99E3E8'; 44 | 45 | if (this.state.updating) updating = '0 0 3em #2979FF'; 46 | else updating = '0 0 0em #90A4AE'; 47 | 48 | // inline styling, as well as using translate coordinates found by d3 49 | const style = { 50 | transform: `translate(${this.props.xtranslate}px,${this.props.ytranslate}px)`, 51 | transition: 'box-shadow 1s ease, transform 0.5s ease', 52 | width: this.props.width, 53 | height: this.props.height, 54 | cursor: 'pointer', 55 | backgroundColor: bgColor, 56 | borderRadius: '5px', 57 | boxShadow: updating, 58 | // fontSize: '10px', 59 | textDecoration: 'none', 60 | textOverflow: 'ellipsis', 61 | }; 62 | const popStyle = { 63 | overflow: 'auto', 64 | maxHeight: '400px', 65 | maxWidth: '350px', 66 | borderRadius: '10px', 67 | }; 68 | 69 | const blockStyle = { 70 | display: 'block', 71 | width: '100%', 72 | overflow: 'hidden', 73 | textOverflow: 'ellipsis', 74 | whiteSpace: 'nowrap', 75 | padding: '0px', 76 | margin: '0px', 77 | }; 78 | 79 | const propsArr = []; 80 | for (const key in this.props.props) { 81 | propsArr.push(key); 82 | } 83 | const propNum = Object.keys(this.props.props).length; 84 | 85 | return ( 86 | 87 | } 98 | > 99 | 106 | 115 |

    {this.props.name}

    116 |
    117 | {propNum} props 118 | 119 |
    120 | ); 121 | } 122 | } 123 | // 124 | // {propsArr.join(' | ')}
    125 | //
    126 | Node.propTypes = { 127 | xtranslate: React.PropTypes.number, 128 | ytranslate: React.PropTypes.number, 129 | name: React.PropTypes.string, 130 | props: React.PropTypes.object, 131 | state: React.PropTypes.object, 132 | methods: React.PropTypes.array, 133 | width: React.PropTypes.number, 134 | height: React.PropTypes.number, 135 | highlight: React.PropTypes.func, 136 | lowlight: React.PropTypes.func, 137 | propUpdate: React.PropTypes.func, 138 | propDoneUpdate: React.PropTypes.func, 139 | }; 140 | 141 | Node.defaultProps = { 142 | xtranslate: 0, 143 | ytranslate: 0, 144 | name: 'Component', 145 | props: {}, 146 | state: {}, 147 | methods: [], 148 | width: 150, 149 | height: 100, 150 | }; 151 | 152 | 153 | export default Node; 154 | -------------------------------------------------------------------------------- /react/components/NodeUp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, Heading, Space, Close } from 'rebass'; 3 | 4 | 5 | 6 | function NodeUp(props) { 7 | // booleans to determine what needs to be dipslayed 8 | const propsBool = Object.keys(props.props).length > 0; 9 | const stateBool = Object.keys(props.state).length > 0; 10 | const methodsBool = props.methods.length > 0; 11 | // headings for the respective tables 12 | const propsHeadings = ['prop', 'value']; 13 | const stateHeadings = ['state', 'value']; 14 | const style = { 15 | overflow: 'visible', 16 | backgroundColor: '#FAFAFA', 17 | fontFamily: '"Roboto", sans-serif', 18 | padding: '1em 1em 1em 1em', 19 | border: '1px solid #78909C', 20 | borderRadius: '10px', 21 | }; 22 | 23 | const tableStyle = { 24 | overflowX: 'visible', 25 | }; 26 | 27 | // formatting the data for table usage 28 | const stateData = []; 29 | for (const key in props.state) { 30 | stateData.push([key, `${JSON.stringify(props.state[key], null, 2)}`]); 31 | } 32 | const propsData = []; 33 | for (const key in props.props) { 34 | propsData.push([key, `${JSON.stringify(props.props[key], null, 2)}`]); 35 | } 36 | const methodsData = props.methods.map((ele, i) => { 37 | return (
  • {ele}
  • ); 38 | }) 39 | return ( 40 |
    41 | 42 | {props.name} 43 | 44 | { 45 | stateBool ? (
    STATE 46 | ) 51 | : null 52 | } 53 | {propsBool ? (
    PROPS 54 |
    ) 59 | : null 60 | } 61 | {methodsBool ? (
    METHODS 62 |
      63 | {methodsData} 64 |
    65 |
    ) 66 | : null 67 | } 68 | { 69 | (!propsBool && !stateBool && !methodsBool) ? Nothing to see here! 70 | : null 71 | } 72 | 73 | ); 74 | } 75 | 76 | NodeUp.propTypes = { 77 | name: React.PropTypes.string, 78 | props: React.PropTypes.object, 79 | state: React.PropTypes.object, 80 | methods: React.PropTypes.array, 81 | }; 82 | 83 | NodeUp.defaultProps = { 84 | name: 'Component', 85 | props: {}, 86 | state: {}, 87 | methods: [], 88 | }; 89 | 90 | 91 | export default NodeUp; 92 | -------------------------------------------------------------------------------- /react/hooks.jsx: -------------------------------------------------------------------------------- 1 | // initialize wrapper function so it's accessible on the window or global object during run-time 2 | window.wrapper = require('../src/reactInterceptor.js').reactInterceptor; 3 | window.grabInitialState = require('../src/reactInterceptor.js').grabInitialState; 4 | -------------------------------------------------------------------------------- /react/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App.jsx'; 4 | import monocleStore from './store/monocleStore'; 5 | 6 | function render () { 7 | ReactDOM.render( 8 | , 9 | document.getElementById('monocle-content') 10 | ); 11 | } 12 | 13 | // render first time; 14 | render(); 15 | 16 | // run render function whenever monocleStore updates 17 | monocleStore.subscribe(render); 18 | 19 | -------------------------------------------------------------------------------- /react/reducers/monocleApp.js: -------------------------------------------------------------------------------- 1 | const initialState = {}; 2 | const monocleApp = function(state, action) { 3 | if (state === undefined) state = initialState; 4 | switch(action.type) { 5 | case 'UPDATE_STATE': 6 | const updatedState = Object.assign({}, state); 7 | if (state[action.name]) { 8 | updatedState[action.name] = Object.assign({}, action.state); 9 | } else { 10 | const currentComponentState = Object.assign({}, updatedState[action.name]); 11 | updatedState[action.name] = Object.assign(currentComponentState, action.state); 12 | } 13 | return updatedState; 14 | case 'INITIALIZE_STATE': 15 | const getState = Object.assign({}, state); 16 | getState[action.name] = Object.assign({}, action.obj); 17 | return getState; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | module.exports = monocleApp; -------------------------------------------------------------------------------- /react/store/monocleStore.js: -------------------------------------------------------------------------------- 1 | const createStore = require('redux').createStore; 2 | const monocleApp = require('../reducers/monocleApp'); 3 | 4 | module.exports = (function() { 5 | if (!window.monocleStore) 6 | window.monocleStore = createStore(monocleApp); 7 | return window.monocleStore; 8 | })(); 9 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const path = require('path'); 6 | const request = require('request'); 7 | const server = app.listen(3000, () => {console.log('listening on 3000....');}); 8 | 9 | 10 | app.use(express.static(path.join(__dirname, './../'))); 11 | 12 | 13 | const webpack = require('webpack'); 14 | const WebpackDevServer = require('webpack-dev-server'); 15 | const config = require('../webpack.config'); 16 | 17 | app.get('/app.js', (req, res) => { 18 | if (process.env.PRODUCTION) { 19 | res.sendFile(__dirname + '/client/app.js'); 20 | } else { 21 | res.redirect('//localhost:9090/client/app.js'); 22 | } 23 | }); 24 | 25 | // Serve aggregate stylesheet depending on environment 26 | app.get('/style.css', (req, res) => { 27 | if (process.env.PRODUCTION) { 28 | res.sendFile(__dirname + '/client/style.css'); 29 | } else { 30 | res.redirect('//localhost:9090/client/style.css'); 31 | } 32 | }); 33 | 34 | // Serve index page 35 | app.get('*', (req, res) => { 36 | res.sendFile(path.join(__dirname, '../', 'index.html')) 37 | // res.sendFile(__dirname + '/index.html'); 38 | }); 39 | 40 | new WebpackDevServer(webpack(config), { 41 | publicPath: config.output.publicPath, 42 | hot: true, 43 | noInfo: true, 44 | historyApiFallback: true 45 | }).listen(9090, 'localhost', (err, result) => { 46 | if (err) { 47 | console.log(err); 48 | } 49 | console.log('webpack i think'); 50 | }); -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | // Server-side entrypoint that registers Babel's require() hook 2 | const babelRegister = require('babel-register'); 3 | babelRegister(); 4 | 5 | require('./server'); -------------------------------------------------------------------------------- /src/astGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const acorn = require('acorn-jsx/inject')(require('acorn')); 4 | const esquery = require('../esquery/esquery'); 5 | 6 | /** 7 | * Takes in directory name and generates an AST based on the File. 8 | * If it is a React Component an object is returned with the corresponding key/value pair 9 | * ComponentName: AST 10 | * @param {directory} file directory 11 | * @returns {Object} Object with Component name and AST 12 | */ 13 | 14 | function astGenerator(directory) { 15 | // using directory of component to turn into string for acorn 16 | const stringed = fs.readFileSync(directory, { encoding: 'utf-8' }); 17 | const result = {}; 18 | let name; 19 | let splicing; 20 | let found; 21 | let sfc; 22 | const sfcNames = []; 23 | 24 | // ast generation 25 | const ast = acorn.parse(stringed, { 26 | sourceType: 'module', 27 | plugins: { jsx: true }, 28 | }); 29 | 30 | function nameFinder(node) { 31 | if ((node.superClass.type === 'MemberExpression' && 32 | node.superClass.object.name === 'React' && 33 | node.superClass.property.name === 'Component') 34 | || (node.superClass.type === 'Identifier' && 35 | node.superClass.name === 'Component')) return node.id.name; 36 | throw new Error(`Unable to find component name at specified directory: ${directory}`); 37 | } 38 | 39 | ast.body.forEach((node, i) => { 40 | if (node.type === 'ClassDeclaration' && node.superClass) { 41 | name = nameFinder(node); 42 | found = true; 43 | } else if (node.type === 'ExportDefaultDeclaration') { 44 | if (node.declaration.type === 'ClassDeclaration' && 45 | node.declaration.superClass) { 46 | name = nameFinder(node.declaration); 47 | found = true; 48 | } else if (node.declaration.name || node.declaration.id.name) { 49 | sfc = node.declaration.name || node.declaration.id.name; 50 | } 51 | } else if (node.type === 'VariableDeclaration' && node.declarations[0].init) { 52 | if (node.declarations[0].init.callee && 53 | node.declarations[0].init.callee.object && 54 | node.declarations[0].init.callee.object.name === 'React' && 55 | node.declarations[0].init.callee.property.name === 'createClass') { 56 | name = node.declarations[0].id.name; 57 | found = true; 58 | } else if (node.declarations[0].init.type === 'FunctionExpression' || 59 | node.declarations[0].init.type === 'ArrowFunctionExpression') { 60 | if (esquery(node, 'JSXElement').length < 0) { 61 | sfcNames.push(node.declarations[0].id.name); 62 | } 63 | } 64 | } else if (node.type === 'ExpressionStatement') { 65 | if (node.expression.callee) { 66 | if ((node.expression.callee.type === 'MemberExpression' && 67 | node.expression.callee.object.name === 'ReactDOM' && 68 | node.expression.callee.property.name === 'render') || 69 | (node.expression.callee.type === 'Identifier' && 70 | node.expression.callee.name === 'render')) { 71 | name = node.expression.arguments[0].openingElement.name.name; 72 | result.ENTRY = name; 73 | splicing = i; 74 | } 75 | } else if (node.expression.left && node.expression.left.object.name === 'module') { 76 | sfc = node.expression.right.name; 77 | } 78 | } else if (node.type === 'FunctionDeclaration') { 79 | if (esquery(node, 'JSXElement').length > 0) { 80 | sfcNames.push(node.id.name); 81 | } 82 | } 83 | }); 84 | 85 | 86 | if (splicing) ast.body.splice(splicing, 1); 87 | if (found) result[name] = ast; 88 | if (sfc && sfcNames.indexOf(sfc) !== -1) result[sfc] = ast; 89 | return result; 90 | } 91 | 92 | 93 | module.exports = astGenerator; 94 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | htmlElements: [ 3 | 'a', 'article', 'audio', 'b', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 4 | 'cite', 'code', 'col', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'font', 'footer', 'form', 5 | 'foreignObject', 'frame', 'g', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'hr', 6 | 'i', 'iframe', 'img', 'input', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'menu', 7 | 'noscript', 'object', 'ol', 'option', 'p', 'path', 'param', 'pre', 'progress', 'q', 'rb', 'rt', 8 | 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 9 | 'strong', 'style', 'sub', 'summary', 'svg', 'table', 'tbody', 'td', 'th', 'thead', 'title', 10 | 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr', 11 | ], 12 | reactMethods: [ 13 | 'render', 'getInitialState', 'getDefaultProps', 'propTypes', 'mixins', 'statics', 14 | 'displayName', 'componentWillMount', 'componentDidMount', 'componentWillReceiveProps', 15 | 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate', 16 | 'componentWillUnmount', 'constructor', 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /src/d3DataBuilder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const reactParser = require('./reactParser'); 3 | const fs = require('fs'); 4 | 5 | /** 6 | * Takes in a formatted object and returns the tree object that D3 will need 7 | * @param {obj} Object 8 | * @return {Object} Tree object for D3 9 | */ 10 | function d3DataBuilder(obj) { 11 | if (!obj.ENTRY) throw new Error('Entry component not found'); 12 | const formatted = {}; 13 | 14 | // parsing AST into formatted objects based on ES5/ES6 syntax 15 | Object.entries(obj).forEach(entry => { 16 | if (entry[0] === 'ENTRY') return; 17 | const componentChecker = reactParser.componentChecker(entry[1]); 18 | switch (componentChecker) { 19 | case 'ES6': 20 | formatted[entry[0]] = reactParser.getES6ReactComponents(entry[1]); 21 | break; 22 | case 'ES5': 23 | formatted[entry[0]] = reactParser.getES5ReactComponents(entry[1]); 24 | break; 25 | default: 26 | formatted[entry[0]] = reactParser.getStatelessFunctionalComponents(entry[1], entry[0]); 27 | break; 28 | } 29 | }); 30 | 31 | Object.values(formatted).forEach(node => { 32 | node.children.forEach(ele => { 33 | if (Array.isArray(ele.props)) { 34 | ele.props.forEach((propped, i) => { 35 | if (typeof propped.value === 'object' && propped.value.name && propped.value.children) { 36 | formatted[propped.parent].children.push(propped.value); 37 | ele.props.splice(i, 1); 38 | } 39 | }); 40 | } 41 | }); 42 | }); 43 | 44 | formatted.monocleENTRY = obj.ENTRY; 45 | fs.writeFileSync('data.js', JSON.stringify(formatted, null, 2)); 46 | 47 | return formatted; 48 | } 49 | 50 | 51 | module.exports = d3DataBuilder; 52 | -------------------------------------------------------------------------------- /src/d3Tree/hooks.js: -------------------------------------------------------------------------------- 1 | !function(t){function n(r){if(e[r])return e[r].exports;var o=e[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var e={};return n.m=t,n.c=e,n.p="",n(0)}({0:function(t,n,e){try{(function(){"use strict";window.wrapper=e(379).reactInterceptor,window.grabInitialState=e(379).grabInitialState}).call(this)}finally{}},165:function(t,n,e){t.exports={"default":e(166),__esModule:!0}},166:function(t,n,e){e(167),t.exports=e(170).Object.assign},167:function(t,n,e){var r=e(168);r(r.S+r.F,"Object",{assign:e(183)})},168:function(t,n,e){var r=e(169),o=e(170),i=e(171),u=e(173),c="prototype",f=function(t,n,e){var a,s,l,p=t&f.F,d=t&f.G,y=t&f.S,v=t&f.P,h=t&f.B,b=t&f.W,w=d?o:o[n]||(o[n]={}),x=w[c],m=d?r:y?r[n]:(r[n]||{})[c];d&&(e=n);for(a in e)s=!p&&m&&void 0!==m[a],s&&a in w||(l=s?m[a]:e[a],w[a]=d&&"function"!=typeof m[a]?e[a]:h&&s?i(l,r):b&&m[a]==l?function(t){var n=function(n,e,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(n);case 2:return new t(n,e)}return new t(n,e,r)}return t.apply(this,arguments)};return n[c]=t[c],n}(l):v&&"function"==typeof l?i(Function.call,l):l,v&&((w.virtual||(w.virtual={}))[a]=l,t&f.R&&x&&!x[a]&&u(x,a,l)))};f.F=1,f.G=2,f.S=4,f.P=8,f.B=16,f.W=32,f.U=64,f.R=128,t.exports=f},169:function(t,n){var e=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},170:function(t,n){var e=t.exports={version:"2.4.0"};"number"==typeof __e&&(__e=e)},171:function(t,n,e){var r=e(172);t.exports=function(t,n,e){if(r(t),void 0===n)return t;switch(e){case 1:return function(e){return t.call(n,e)};case 2:return function(e,r){return t.call(n,e,r)};case 3:return function(e,r,o){return t.call(n,e,r,o)}}return function(){return t.apply(n,arguments)}}},172:function(t,n){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},173:function(t,n,e){var r=e(174),o=e(182);t.exports=e(178)?function(t,n,e){return r.f(t,n,o(1,e))}:function(t,n,e){return t[n]=e,t}},174:function(t,n,e){var r=e(175),o=e(177),i=e(181),u=Object.defineProperty;n.f=e(178)?Object.defineProperty:function(t,n,e){if(r(t),n=i(n,!0),r(e),o)try{return u(t,n,e)}catch(c){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(t[n]=e.value),t}},175:function(t,n,e){var r=e(176);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},176:function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},177:function(t,n,e){t.exports=!e(178)&&!e(179)(function(){return 7!=Object.defineProperty(e(180)("div"),"a",{get:function(){return 7}}).a})},178:function(t,n,e){t.exports=!e(179)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},179:function(t,n){t.exports=function(t){try{return!!t()}catch(n){return!0}}},180:function(t,n,e){var r=e(176),o=e(169).document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},181:function(t,n,e){var r=e(176);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},182:function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},183:function(t,n,e){"use strict";var r=e(184),o=e(199),i=e(200),u=e(201),c=e(188),f=Object.assign;t.exports=!f||e(179)(function(){var t={},n={},e=Symbol(),r="abcdefghijklmnopqrst";return t[e]=7,r.split("").forEach(function(t){n[t]=t}),7!=f({},t)[e]||Object.keys(f({},n)).join("")!=r})?function(t,n){for(var e=u(t),f=arguments.length,a=1,s=o.f,l=i.f;f>a;)for(var p,d=c(arguments[a++]),y=s?r(d).concat(s(d)):r(d),v=y.length,h=0;v>h;)l.call(d,p=y[h++])&&(e[p]=d[p]);return e}:f},184:function(t,n,e){var r=e(185),o=e(198);t.exports=Object.keys||function(t){return r(t,o)}},185:function(t,n,e){var r=e(186),o=e(187),i=e(191)(!1),u=e(195)("IE_PROTO");t.exports=function(t,n){var e,c=o(t),f=0,a=[];for(e in c)e!=u&&r(c,e)&&a.push(e);for(;n.length>f;)r(c,e=n[f++])&&(~i(a,e)||a.push(e));return a}},186:function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},187:function(t,n,e){var r=e(188),o=e(190);t.exports=function(t){return r(o(t))}},188:function(t,n,e){var r=e(189);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},189:function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},190:function(t,n){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},191:function(t,n,e){var r=e(187),o=e(192),i=e(194);t.exports=function(t){return function(n,e,u){var c,f=r(n),a=o(f.length),s=i(u,a);if(t&&e!=e){for(;a>s;)if(c=f[s++],c!=c)return!0}else for(;a>s;s++)if((t||s in f)&&f[s]===e)return t||s||0;return!t&&-1}}},192:function(t,n,e){var r=e(193),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},193:function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},194:function(t,n,e){var r=e(193),o=Math.max,i=Math.min;t.exports=function(t,n){return t=r(t),t<0?o(t+n,0):i(t,n)}},195:function(t,n,e){var r=e(196)("keys"),o=e(197);t.exports=function(t){return r[t]||(r[t]=o(t))}},196:function(t,n,e){var r=e(169),o="__core-js_shared__",i=r[o]||(r[o]={});t.exports=function(t){return i[t]||(i[t]={})}},197:function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++e+r).toString(36))}},198:function(t,n){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},199:function(t,n){n.f=Object.getOwnPropertySymbols},200:function(t,n){n.f={}.propertyIsEnumerable},201:function(t,n,e){var r=e(190);t.exports=function(t){return Object(r(t))}},364:function(t,n,e){try{(function(){"use strict";var n=e(365).createStore,r=e(378);t.exports=function(){return window.monocleStore||(window.monocleStore=n(r)),window.monocleStore}()}).call(this)}finally{}},365:function(t,n,e){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}n.__esModule=!0,n.compose=n.applyMiddleware=n.bindActionCreators=n.combineReducers=n.createStore=void 0;var o=e(366),i=r(o),u=e(373),c=r(u),f=e(375),a=r(f),s=e(376),l=r(s),p=e(377),d=r(p),y=e(374);r(y);n.createStore=i["default"],n.combineReducers=c["default"],n.bindActionCreators=a["default"],n.applyMiddleware=l["default"],n.compose=d["default"]},366:function(t,n,e){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,n,e){function r(){b===h&&(b=h.slice())}function i(){return v}function c(t){if("function"!=typeof t)throw new Error("Expected listener to be a function.");var n=!0;return r(),b.push(t),function(){if(n){n=!1,r();var e=b.indexOf(t);b.splice(e,1)}}}function s(t){if(!(0,u["default"])(t))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if("undefined"==typeof t.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(w)throw new Error("Reducers may not dispatch actions.");try{w=!0,v=y(v,t)}finally{w=!1}for(var n=h=b,e=0;e { 12 | inlineScripts += ele; 13 | }) 14 | 15 | css.forEach(ele => { 16 | cssStyles += ele; 17 | }); 18 | 19 | const treeLogicPath = '/src/d3Tree/app.js', 20 | hookLogicPath = '/src/d3Tree/hooks.js', 21 | endHtmlPath = '/src/d3Tree/end.html', 22 | treeHtml = htmlTop(cssStyles), 23 | treeLogic = fs.readFileSync(appRoot + treeLogicPath, { encoding: 'utf-8' }), 24 | hookLogic = fs.readFileSync(appRoot + hookLogicPath, { encoding: 'utf-8' }), 25 | endHtml = htmlBottom(inlineScripts), 26 | insert = `${treeHtml} 27 | ${arr.join('') || '
    '} 28 | 29 | 35 | ${endHtml}`; 36 | fs.writeFile(`${process.cwd()}/react-monocle.html`, insert, err => { 37 | if (err) throw new Error(err); 38 | exec(`open ${process.cwd()}/react-monocle.html`, (error, stdout, stderr) => { 39 | if (error !== null) throw new Error(error); 40 | if (stderr !== '') throw new Error(stderr); 41 | else console.log(`Parsed in ${Date.now() - start}ms. 42 | Look in ${process.cwd()} to find react-monocle.html.`); 43 | }); 44 | }); 45 | } 46 | 47 | function htmlTop(css) { 48 | return ` 49 | 50 | 51 | 52 | React Monocle 53 | 54 | 55 | ${css} 56 | 117 | 118 | 119 | 120 | 121 |
    122 |
    ` 123 | } 124 | 125 | function htmlBottom(scripts) { 126 | return ` 127 | ${scripts} 128 | 129 | 130 | ` 131 | } 132 | 133 | 134 | 135 | module.exports = renderHtml; 136 | -------------------------------------------------------------------------------- /src/htmlParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | 4 | 5 | function htmlParser(path, bundle) { 6 | const stringed = fs.readFileSync(path, { encoding: 'utf-8' }); 7 | 8 | const result = findJavaScript(stringed, bundle, path.replace(/\/.*?\.html/, '').replace(/\/?.*?\.html/, '')); 9 | result.css = findCSS(stringed, path.replace(/\/.*?\.html/, '').replace(/\/?.*?\.html/, '')); 10 | 11 | return result; 12 | } 13 | 14 | function findCSS(str, relPath) { 15 | if (relPath !== '') relPath = relPath + '/'; 16 | const styleTags = str.match(/`; 36 | } 37 | }) 38 | .concat(styleTags); 39 | } 40 | 41 | function findJavaScript(str, bundle, relPath) { 42 | if (relPath !== '') relPath = relPath + '/'; 43 | const result = { 44 | bundle: '', 45 | scripts: [], 46 | }; 47 | const scriptz = str.match(//g); 48 | if (!scriptz) throw new Error('Was not able to find script tags in HTML.'); 49 | scriptz.forEach(ele => { 50 | if (!ele) return; 51 | if (ele.includes(bundle)) { 52 | result.bundle = relPath + ele.match(/src(\s?)=(\s?)(\\?)('|").*?(\\?)('|")/g)[0] 53 | .match(/(\\?)('|").*?(\\?)('|")/g)[0] 54 | .replace(/\\/g, '') 55 | .replace(/'/g, '') 56 | .replace(/"/g, '') 57 | .trim(); 58 | if (result.bundle[0] === '/') result.bundle = result.bundle.slice(1); 59 | } else if (ele.search(/src(\s?)=(\s?)(\\?)('|").*?(\\?)('|")/ === -1) || ele.search(/http/) !== -1) result.scripts.push(ele); 60 | }); 61 | return result; 62 | } 63 | 64 | 65 | module.exports = htmlParser; 66 | -------------------------------------------------------------------------------- /src/previewParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const regexLastIndexOf = require('./stringRegexHelper').regexLastIndexOf; 4 | const strip = require('strip-comments'); 5 | 6 | const WEBPACKES5 = /(var )?\w+\s*?=\s*(\(\d+, )?_react(\d+\["\w+"\])?.createClass/; 7 | const WEBPACKES6 = /(var )?\w+\s*?=\s*?function\s*?\(_(React\$)?Component\)/; 8 | const GULP = /var \w+ = React.createClass\({/; 9 | const ROLLUP = /var \w+ = \(function \(superclass\) {/; 10 | 11 | function getComponentName(bundle, startingIndex) { 12 | const bundleSearchIndicesMap = {}; 13 | // get index of component declaration 14 | bundleSearchIndicesMap[regexLastIndexOf(bundle, WEBPACKES5, startingIndex)] = 'WEBPACKES5'; 15 | // let's try ES6... 16 | bundleSearchIndicesMap[regexLastIndexOf(bundle, WEBPACKES6, startingIndex)] = 'WEBPACKES6'; 17 | // let's try GULP 18 | bundleSearchIndicesMap[regexLastIndexOf(bundle, GULP, startingIndex)] = 'GULP'; 19 | // let's try Rollup ex: var Slick = (function (superclass) { 20 | bundleSearchIndicesMap[regexLastIndexOf(bundle, ROLLUP, startingIndex)] = 'ROLLUP'; 21 | const targetIndex = Object.keys(bundleSearchIndicesMap) 22 | .filter(index => index >= 0) 23 | .reduce((prev, curr) => { 24 | return Math.abs(curr - startingIndex) < Math.abs(prev - startingIndex) 25 | ? curr 26 | : prev; 27 | }); 28 | 29 | let componentMatch; 30 | switch (bundleSearchIndicesMap[targetIndex]) { 31 | case 'WEBPACKES5': 32 | componentMatch = bundle.slice(targetIndex) 33 | .match(/(var )?\w+\s*?=\s*(\(\d+, )?_react(\d+\["\w+"\])?.createClass/); 34 | break; 35 | case 'WEBPACKES6': 36 | componentMatch = bundle.slice(targetIndex) 37 | .match(/(var )?\w+\s*?=\s*?function\s*?\(_(React\$)?Component\)/); 38 | break; 39 | case 'GULP': 40 | componentMatch = bundle.slice(targetIndex) 41 | .match(/var \w+ = React.createClass\({/); 42 | break; 43 | case 'ROLLUP': 44 | componentMatch = bundle.slice(targetIndex) 45 | .match(/var \w+ = \(function \(superclass\) {/); 46 | break; 47 | default: 48 | throw new Error('Unable to find component from bundle file'); 49 | } 50 | 51 | // need to normalize component name (remove declarator ex. var, const) 52 | return componentMatch[0] 53 | .replace(/var |const /, '') 54 | .replace(/ /g, '') 55 | .split('=')[0]; 56 | } 57 | 58 | function modifySetStateStrings(bundleFilePath) { 59 | let bundle; 60 | try { 61 | bundle = fs.readFileSync(bundleFilePath, { encoding: 'utf-8' }); 62 | } catch (error) { 63 | throw new Error('Invalid bundle file path specified.' + 64 | ' Please enter a valid path to your app\'s bundle file'); 65 | } 66 | 67 | if (bundle.length === 0) { 68 | throw new Error('Bundle string is empty, provide valid bundle string input'); 69 | } 70 | 71 | console.log('Starting to strip comments from bundle file...'); 72 | const start = Date.now(); 73 | let modifiedBundle = strip(bundle.slice()); 74 | console.log(`Took ${(Date.now() - start) / 1000} seconds to strip comments input bundle file`); 75 | let index = modifiedBundle.indexOf('this.setState', 0); 76 | while (index !== -1) { 77 | const openBraceIdx = modifiedBundle.indexOf('{', index); 78 | let currentIdx = openBraceIdx + 1; 79 | const parensStack = ['{']; 80 | while (parensStack.length !== 0) { 81 | if (modifiedBundle[currentIdx] === '{') parensStack.push(modifiedBundle[currentIdx]); 82 | if (modifiedBundle[currentIdx] === '}') parensStack.pop(); 83 | currentIdx++; 84 | } 85 | const stateStr = modifiedBundle.slice(openBraceIdx, currentIdx); 86 | const functionStartIdx = currentIdx; 87 | parensStack.push('('); 88 | while (parensStack.length !== 0) { 89 | if (modifiedBundle[currentIdx] === '(') parensStack.push(modifiedBundle[currentIdx]); 90 | if (modifiedBundle[currentIdx] === ')') parensStack.pop(); 91 | currentIdx++; 92 | } 93 | currentIdx--; 94 | const callbackStr = modifiedBundle.slice(functionStartIdx, currentIdx); 95 | const injection = `wrapper('${getComponentName(modifiedBundle, 96 | index)}',this)(${stateStr}${callbackStr})`; 97 | modifiedBundle = modifiedBundle.slice(0, index) + injection 98 | + modifiedBundle.slice(currentIdx + 1); 99 | /* need to take into account that length of bundle now changes since injected wrapper 100 | string length can be different than original */ 101 | const oldLength = currentIdx - index; 102 | const newLength = injection.length; 103 | index = modifiedBundle.indexOf('this.setState', index + 1 + newLength - oldLength); 104 | } 105 | return modifiedBundle; 106 | } 107 | 108 | 109 | function modifyInitialState(modifiedBundle) { 110 | let index = -1; 111 | let newModifiedBundle = modifiedBundle; 112 | if (newModifiedBundle.indexOf('_this.state') >= 0) { 113 | // do webpack 114 | index = newModifiedBundle.indexOf('_this.state', 0); 115 | } else if (newModifiedBundle.indexOf('getInitialState() {', 0) >= 0) { 116 | // do gulp 117 | index = newModifiedBundle.indexOf('getInitialState() {', 0) + 19; 118 | } else if (newModifiedBundle.indexOf('this.state', 0) >= 0) { 119 | // do rollup 120 | index = newModifiedBundle.indexOf('this.state = {'); 121 | } else { 122 | throw new Error('Unable to find component initial state'); 123 | } 124 | 125 | while (index !== -1) { 126 | // looking for index of follow brace, after return statement 127 | const openBraceIdx = newModifiedBundle.indexOf('{', index); 128 | let currentIdx = openBraceIdx + 1; 129 | const parensStack = ['{']; 130 | while (parensStack.length !== 0) { 131 | if (newModifiedBundle[currentIdx] === '{') parensStack.push(newModifiedBundle[currentIdx]); 132 | if (newModifiedBundle[currentIdx] === '}') parensStack.pop(); 133 | currentIdx++; 134 | } 135 | 136 | let injection; 137 | const componentName = getComponentName(newModifiedBundle, index); 138 | const stateStr = newModifiedBundle.slice(openBraceIdx, currentIdx); 139 | if (newModifiedBundle.indexOf('_this.state', 0) >= 0) { 140 | injection = `_this.state = grabInitialState('${componentName}', ${stateStr}),`; 141 | } else if (newModifiedBundle.indexOf('getInitialState() {', 0) >= 0) { 142 | injection = `return grabInitialState('${componentName}', ${stateStr}),`; 143 | } else if (newModifiedBundle.indexOf('this.state = {', 0) >= 0) { 144 | injection = `this.state = grabInitialState('${componentName}', ${stateStr}),`; 145 | } 146 | 147 | newModifiedBundle = newModifiedBundle.slice(0, index) + injection 148 | + newModifiedBundle.slice(currentIdx + 1); 149 | /* need to take into account that length of bundle now changes since injected wrapper 150 | string length can be different than original */ 151 | const oldLength = currentIdx - index; 152 | const newLength = injection.length; 153 | 154 | if (newModifiedBundle.indexOf('_this.state') >= 0) { 155 | index = newModifiedBundle.indexOf('_this.state', index + 1 + newLength - oldLength); 156 | } else if (newModifiedBundle.indexOf('getInitialState() {') >= 0) { 157 | index = newModifiedBundle.indexOf('getInitialState() {', index + 1 + newLength - oldLength); 158 | } else if (newModifiedBundle.indexOf('this.state = grabInitialState') >= 0) { 159 | index = newModifiedBundle.indexOf('this.state = grabInitialState', 160 | index + 1 + newLength - oldLength); 161 | } else { 162 | throw new Error('Unable to find next initial state index'); 163 | } 164 | } 165 | return newModifiedBundle; 166 | } 167 | 168 | 169 | function getDivs(newModifiedBundle) { 170 | let index = newModifiedBundle.indexOf('getElementById(', 0); 171 | let divsArr = []; 172 | while (index !== -1) { 173 | const openParenIdx = newModifiedBundle.indexOf('(', index - 1); 174 | let currentIdx = openParenIdx + 1; 175 | const parensStack = ['(']; 176 | while (parensStack.length !== 0) { 177 | if (newModifiedBundle[currentIdx] === '(') parensStack.push(newModifiedBundle[currentIdx]); 178 | if (newModifiedBundle[currentIdx] === ')') parensStack.pop(); 179 | currentIdx++; 180 | } 181 | divsArr.push(newModifiedBundle.slice(openParenIdx + 2, currentIdx - 2)); 182 | index = newModifiedBundle.indexOf('getElementById(', index + 1); 183 | } 184 | divsArr = divsArr.map(ele => { 185 | return `
    `; 186 | }); 187 | const uniqueArr = []; 188 | for (let i = 0; i < divsArr.length; i++) { 189 | if (uniqueArr.indexOf(divsArr[i]) < 0) { 190 | uniqueArr.push(divsArr[i]); 191 | } 192 | } 193 | return uniqueArr; 194 | } 195 | 196 | module.exports = { 197 | modifySetStateStrings, 198 | modifyInitialState, 199 | getComponentName, 200 | getDivs, 201 | }; 202 | 203 | -------------------------------------------------------------------------------- /src/reactInterceptor.js: -------------------------------------------------------------------------------- 1 | const monocleStore = require('../react/store/monocleStore'); 2 | const updateState = require('../react/actions/index').updateState; 3 | const sendInitialState = require('../react/actions/index').sendInitialState; 4 | 5 | export function reactInterceptor(name, component) { 6 | return (state, callback) => { 7 | monocleStore.dispatch(updateState(name, state)); 8 | return component.setState(state, callback); 9 | }; 10 | } 11 | 12 | export function grabInitialState(name, obj) { 13 | monocleStore.dispatch(sendInitialState(name, obj)); 14 | return obj; 15 | } 16 | 17 | module.exports = { 18 | reactInterceptor, 19 | grabInitialState, 20 | }; 21 | -------------------------------------------------------------------------------- /src/reactParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const acorn = require('acorn-jsx'); 4 | const esrecurse = require('../esrecurse/esrecurse'); 5 | const escodegen = require('escodegen'); 6 | const esquery = require('../esquery/esquery'); 7 | const bfs = require('acorn-bfs'); 8 | 9 | const htmlElements = require('./constants.js').htmlElements; 10 | const reactMethods = require('./constants.js').reactMethods; 11 | 12 | function getReactStates(node) { 13 | const stateStr = escodegen.generate(node); 14 | let states; 15 | eval(`states = ${stateStr}`); 16 | 17 | const output = []; 18 | for (const state in states) { 19 | output.push({ 20 | name: state, 21 | value: states[state], 22 | }); 23 | } 24 | 25 | return output; 26 | } 27 | 28 | /** 29 | * Returns array of props from React component passed to input 30 | * @param {Node} node 31 | * @returns {Array} Array of all JSX props on React component 32 | */ 33 | function getReactProps(node, parent) { 34 | if (node.openingElement.attributes.length === 0 || 35 | htmlElements.indexOf(node.openingElement.name.name) > 0) return {}; 36 | const result = node.openingElement.attributes 37 | .map(attribute => { 38 | const name = attribute.name.name; 39 | let valueName; 40 | if (attribute.value === null) valueName = undefined; 41 | else if (attribute.value.type === 'Literal') valueName = attribute.value.value; 42 | else if (attribute.value.expression.type === 'Literal') { 43 | valueName = attribute.value.expression.value; 44 | } else if (attribute.value.expression.type === 'Identifier') { 45 | valueName = attribute.value.expression.name; 46 | } else if (attribute.value.expression.type === 'CallExpression') { 47 | valueName = attribute.value.expression.callee.object.property.name; 48 | } else if (attribute.value.expression.type === 'BinaryExpression') { 49 | valueName = attribute.value.expression.left.name 50 | + attribute.value.expression.operator 51 | + (attribute.value.expression.right.name 52 | || attribute.value.expression.right.value); 53 | } else if (attribute.value.expression.type === 'MemberExpression') { 54 | let current = attribute.value.expression; 55 | while (current && current.property) { 56 | // && !current.property.name.match(/(state|props)/) 57 | valueName = `.${current.property.name}${valueName || ''}`; 58 | current = current.object; 59 | if (current.type === 'Identifier') { 60 | valueName = `.${current.name}${valueName || ''}`; 61 | break; 62 | } 63 | } 64 | valueName = valueName.replace('.', ''); 65 | } else if (attribute.value.expression.type === 'LogicalExpression') { 66 | valueName = attribute.value.expression.left.property.name; 67 | // valueType = attribute.value.expression.left.object.name; 68 | } else if (attribute.value.expression.type === 'JSXElement') { 69 | const nodez = attribute.value.expression; 70 | const output = { 71 | name: nodez.openingElement.name.name, 72 | children: getChildJSXElements(nodez, parent), 73 | props: getReactProps(nodez, parent), 74 | state: {}, 75 | methods: [], 76 | }; 77 | valueName = output; 78 | } else valueName = escodegen.generate(attribute.value); 79 | 80 | return { 81 | name, 82 | value: valueName, 83 | parent, 84 | }; 85 | }); 86 | return result; 87 | } 88 | 89 | /** 90 | * Returns array of children components of React component passed to input 91 | * @param {Node} node 92 | * @returns {Array} Array of (nested) children of React component passed in 93 | */ 94 | function getChildJSXElements(node, parent) { 95 | if (node.children.length === 0) return []; 96 | const childJsxComponentsArr = node 97 | .children 98 | .filter(jsx => jsx.type === 'JSXElement' 99 | && htmlElements.indexOf(jsx.openingElement.name.name) < 0); 100 | return childJsxComponentsArr 101 | .map(child => { 102 | return { 103 | name: child.openingElement.name.name, 104 | children: getChildJSXElements(child, parent), 105 | props: getReactProps(child, parent), 106 | state: {}, 107 | methods: [], 108 | }; 109 | }); 110 | } 111 | 112 | function forInFinder(arr, name) { 113 | const result = arr.map(ele => { 114 | const jsxnode = esquery(ele, 'JSXElement')[0]; 115 | const obj = {}; 116 | obj.variables = {}; 117 | esquery(ele, 'VariableDeclarator').forEach(vars => { 118 | if (vars.id.name !== 'i' && vars.init) { 119 | obj.variables[vars.id.name] = escodegen.generate(vars.init).replace('this.', ''); 120 | } 121 | }); 122 | if (ele.left.declarations) obj.variables[ele.left.declarations[0].id.name] = '[key]'; 123 | else if (ele.left.type === 'Identifier') obj.variables[ele.left.name] = '[key]'; 124 | 125 | if (jsxnode && htmlElements.indexOf(jsxnode.openingElement.name.name)) { 126 | let current = ele.right; 127 | let found; 128 | while (current && current.property) { 129 | found = `.${current.property.name}${found || ''}`; 130 | current = current.object; 131 | if (current.type === 'Identifier') { 132 | found = `.${current.name}${found || ''}`; 133 | break; 134 | } 135 | } 136 | 137 | obj.jsx = { 138 | name: jsxnode.openingElement.name.name, 139 | children: getChildJSXElements(jsxnode, name), 140 | props: getReactProps(jsxnode, name), 141 | state: {}, 142 | methods: [], 143 | iterated: 'forIn', 144 | source: found.replace('.', ''), 145 | }; 146 | const propsArr = obj.jsx.props; 147 | for (let i = 0; i < propsArr.length; i++) { 148 | for (const key in obj.variables) { 149 | if (propsArr[i].value.includes(key)) { 150 | if (obj.variables[key] === '[key]') { 151 | propsArr[i].value = propsArr[i].value.replace(`.${key}`, obj.variables[key]); 152 | } else propsArr[i].value = propsArr[i].value.replace(key, obj.variables[key]); 153 | } 154 | } 155 | } 156 | } 157 | return obj; 158 | }); 159 | return result; 160 | } 161 | 162 | 163 | function forLoopFinder(arr, name) { 164 | const result = arr.map(ele => { 165 | const jsxnode = esquery(ele, 'JSXElement')[0]; 166 | const obj = {}; 167 | obj.variables = {}; 168 | 169 | // finding variables in case information was reassigned 170 | esquery(ele, 'VariableDeclarator').forEach(vars => { 171 | if (vars.id.name !== 'i' && vars.init) { 172 | obj.variables[vars.id.name] = escodegen.generate(vars.init) 173 | .replace('this.', '').replace('.length', ''); 174 | } 175 | }); 176 | 177 | // defaulting each iteration to be represented by 'i' 178 | if (ele.init.declarations) obj.variables[ele.init.declarations[0].id.name] = '[i]'; 179 | else if (ele.init.type === 'AssignmentExpression') obj.variables[ele.init.left.name] = '[i]'; 180 | 181 | // building the object name 182 | if (jsxnode && htmlElements.indexOf(jsxnode.openingElement.name.name)) { 183 | let current = ele.test.right; 184 | let found; 185 | while (current && current.property) { 186 | found = `.${current.property.name}${found || ''}`; 187 | current = current.object; 188 | if (current.type === 'Identifier') { 189 | found = `.${current.name}${found || ''}`; 190 | break; 191 | } 192 | } 193 | 194 | obj.jsx = { 195 | name: jsxnode.openingElement.name.name, 196 | children: getChildJSXElements(jsxnode, name), 197 | props: getReactProps(jsxnode, name), 198 | state: {}, 199 | methods: [], 200 | iterated: 'forLoop', 201 | source: found.replace('.', '').replace('.length', ''), 202 | }; 203 | 204 | // replacing variables with their properties 205 | const propsArr = obj.jsx.props; 206 | for (let i = 0; i < propsArr.length; i++) { 207 | for (const key in obj.variables) { 208 | if (propsArr[i].value.includes(key)) { 209 | if (obj.variables[key] === '[i]') { 210 | propsArr[i].value = propsArr[i].value.replace(`.${key}`, obj.variables[key]); 211 | } else propsArr[i].value = propsArr[i].value.replace(key, obj.variables[key]); 212 | } 213 | } 214 | } 215 | } 216 | return obj; 217 | }); 218 | return result; 219 | } 220 | 221 | function higherOrderFunctionFinder(arr, name) { 222 | const result = arr.map(ele => { 223 | // since every higher order function will have some parameter 224 | // will be used to replace with what it actually is 225 | const param = ele.arguments[0].params[0].name; 226 | const jsxnode = esquery(ele, 'JSXElement')[0]; 227 | const obj = {}; 228 | obj.variables = {}; 229 | esquery(ele, 'VariableDeclarator').forEach(vars => { 230 | obj.variables[vars.id.name] = escodegen.generate(vars.init); 231 | }); 232 | 233 | if (jsxnode && htmlElements.indexOf(jsxnode.openingElement.name.name)) { 234 | let current = ele.callee.object; 235 | let found; 236 | while (current && current.property) { 237 | found = `.${current.property.name}${found || ''}`; 238 | current = current.object; 239 | if (current.type === 'Identifier') { 240 | found = `.${current.name}${found || ''}`; 241 | break; 242 | } 243 | } 244 | 245 | obj.jsx = { 246 | name: jsxnode.openingElement.name.name, 247 | children: getChildJSXElements(jsxnode, name), 248 | props: getReactProps(jsxnode, name), 249 | state: {}, 250 | methods: [], 251 | iterated: 'higherOrder', 252 | source: found.replace('.', ''), 253 | }; 254 | 255 | const propsArr = obj.jsx.props; 256 | for (let i = 0; i < propsArr.length; i++) { 257 | propsArr[i].value = propsArr[i].value.replace(param, `${obj.jsx.source}[i]`); 258 | for (const key in obj.variables) { 259 | if (propsArr[i].value.includes(key)) { 260 | propsArr[i].value = propsArr[i].value.replace(key, obj.variables[key]); 261 | } 262 | } 263 | } 264 | } 265 | return obj; 266 | }); 267 | return result; 268 | } 269 | 270 | /** 271 | * Returns if AST node is an ES6 React component 272 | * @param {Node} node 273 | * @return {Boolean} Determines if AST node is a React component node 274 | */ 275 | function isES6ReactComponent(node) { 276 | return (node.superClass.property && node.superClass.property.name === 'Component') 277 | || node.superClass.name === 'Component'; 278 | } 279 | 280 | /** 281 | * Recursively walks AST and extracts ES5 React component names, child components, props and state 282 | * @param {ast} ast 283 | * @returns {Object} Nested object containing name, children, 284 | * props and state properties of components 285 | */ 286 | function getES5ReactComponents(ast) { 287 | const output = { 288 | name: '', 289 | state: {}, 290 | props: {}, 291 | methods: [], 292 | children: [], 293 | }; 294 | let iter = []; 295 | let topJsxComponent; 296 | let outside; 297 | const checker = {}; 298 | esrecurse.visit(ast, { 299 | VariableDeclarator(node) { 300 | topJsxComponent = node.id.name; 301 | this.visitChildren(node); 302 | }, 303 | MemberExpression(node) { 304 | if (node.property && node.property.name === 'createClass') { 305 | output.name = topJsxComponent; 306 | } 307 | this.visitChildren(node); 308 | }, 309 | ObjectExpression(node) { 310 | node.properties.forEach(prop => { 311 | if (reactMethods.indexOf(prop.key.name) < 0 312 | && prop.value.type === 'FunctionExpression') { 313 | output.methods.push(prop.key.name); 314 | } 315 | }); 316 | this.visitChildren(node); 317 | }, 318 | JSXElement(node) { 319 | output.children = getChildJSXElements(node, output.name); 320 | output.props = getReactProps(node, output.name); 321 | if (htmlElements.indexOf(node.openingElement.name.name) < 0) { 322 | outside = { 323 | name: node.openingElement.name.name, 324 | children: getChildJSXElements(node, output.name), 325 | props: getReactProps(node, output.name), 326 | state: {}, 327 | methods: [], 328 | }; 329 | } 330 | }, 331 | }); 332 | 333 | const forIn = esquery(ast, 'ForInStatement').filter(ele => { 334 | const searched = bfs(ele).filter(n => { 335 | return n.type === 'JSXElement'; 336 | }); 337 | return searched.length > 0; 338 | }); 339 | if (forIn.length > 0) iter = iter.concat(forInFinder(forIn, output.name)); 340 | 341 | const forLoop = esquery(ast, 'ForStatement').filter(ele => { 342 | const searched = bfs(ele).filter(n => { 343 | return n.type === 'JSXElement'; 344 | }); 345 | return searched.length > 0; 346 | }); 347 | if (forLoop.length > 0) iter = iter.concat(forLoopFinder(forLoop, output.name)); 348 | 349 | const higherOrderFunc = esquery(ast, 'CallExpression').filter(ele => { 350 | let higherOrderChecker = false; 351 | const searched = bfs(ele).filter(n => { 352 | return n.type === 'JSXElement'; 353 | }); 354 | if (ele.callee.property && ele.callee.property.name.match(/(map|forEach|filter|reduce)/)) { 355 | higherOrderChecker = ele.callee.property.name.match(/(map|forEach|filter|reduce)/); 356 | } 357 | return searched.length > 0 && higherOrderChecker; 358 | }); 359 | if (higherOrderFunc.length > 0) { 360 | iter = iter.concat(higherOrderFunctionFinder(higherOrderFunc, output.name)); 361 | } 362 | if (outside) output.children.push(outside); 363 | output.children.forEach((ele, i) => { 364 | checker[ele.name] = i; 365 | }); 366 | 367 | for (let i = 0; i < iter.length; i++) { 368 | if (checker.hasOwnProperty(iter[i].jsx.name)) { 369 | output.children[checker[iter[i].jsx.name]] = iter[i].jsx; 370 | } 371 | } 372 | 373 | return output; 374 | } 375 | 376 | /** 377 | * Recursively walks AST and extracts ES6 React component names, child components, props and state 378 | * @param {ast} ast 379 | * @returns {Object} Nested object containing name, children, 380 | * props and state properties of components 381 | */ 382 | function getES6ReactComponents(ast) { 383 | const output = { 384 | name: '', 385 | props: {}, 386 | state: {}, 387 | methods: [], 388 | children: [], 389 | }; 390 | let iter = []; 391 | let outside; 392 | const checker = {}; 393 | esrecurse.visit(ast, { 394 | ClassDeclaration(node) { 395 | if (isES6ReactComponent(node)) { 396 | output.name = node.id.name; 397 | this.visitChildren(node); 398 | } 399 | }, 400 | MethodDefinition(node) { 401 | if (reactMethods.indexOf(node.key.name) < 0) output.methods.push(node.key.name); 402 | this.visitChildren(node); 403 | }, 404 | JSXElement(node) { 405 | output.children = getChildJSXElements(node, output.name); 406 | output.props = getReactProps(node, output.name); 407 | if (htmlElements.indexOf(node.openingElement.name.name) < 0) { 408 | outside = { 409 | name: node.openingElement.name.name, 410 | children: getChildJSXElements(node, output.name), 411 | props: getReactProps(node, output.name), 412 | state: {}, 413 | methods: [], 414 | }; 415 | } 416 | const forIn = esquery(ast, 'ForInStatement').filter(ele => { 417 | const searched = bfs(ele).filter(n => { 418 | return n.type === 'JSXElement'; 419 | }); 420 | return searched.length > 0; 421 | }); 422 | if (forIn.length > 0) iter = iter.concat(forInFinder(forIn, output.name)); 423 | 424 | const forLoop = esquery(ast, 'ForStatement').filter(ele => { 425 | const searched = bfs(ele).filter(n => { 426 | return n.type === 'JSXElement'; 427 | }); 428 | return searched.length > 0; 429 | }); 430 | if (forLoop.length > 0) iter = iter.concat(forLoopFinder(forLoop, output.name)); 431 | 432 | const higherOrderFunc = esquery(ast, 'CallExpression').filter(ele => { 433 | let higherOrderChecker = false; 434 | const searched = bfs(ele).filter(n => { 435 | return n.type === 'JSXElement'; 436 | }); 437 | if (ele.callee.property && ele.callee.property.name.match(/(map|forEach|filter|reduce)/)) { 438 | higherOrderChecker = ele.callee.property.name.match(/(map|forEach|filter|reduce)/); 439 | } 440 | return searched.length > 0 && higherOrderChecker; 441 | }); 442 | if (higherOrderFunc.length > 0) iter = iter.concat(higherOrderFunctionFinder(higherOrderFunc, output.name)); 443 | }, 444 | }); 445 | 446 | if (outside) output.children.push(outside); 447 | output.children.forEach((ele, i) => { 448 | checker[ele.name] = i; 449 | }); 450 | 451 | for (let i = 0; i < iter.length; i++) { 452 | if (checker.hasOwnProperty(iter[i].jsx.name)) { 453 | output.children[checker[iter[i].jsx.name]] = iter[i].jsx; 454 | } 455 | } 456 | return output; 457 | } 458 | 459 | /** 460 | * Recursively walks AST extracts name, child component, and props for a stateless functional component 461 | * Still a WIP - no way to tell if it is actually a component or just a function 462 | * @param {ast} ast 463 | * @returns {Object} Nested object containing name, children, and props properties of components 464 | */ 465 | function getStatelessFunctionalComponents(ast, name) { 466 | const output = { 467 | name: name, 468 | state: {}, 469 | props: {}, 470 | methods: [], 471 | children: [], 472 | }; 473 | 474 | let iter = []; 475 | let outside; 476 | const checker = {}; 477 | esrecurse.visit(ast, { 478 | ObjectExpression(node) { 479 | node.properties.forEach(prop => { 480 | if (reactMethods.indexOf(prop.key.name) < 0 481 | && prop.value.type === 'FunctionExpression') { 482 | output.methods.push(prop.key.name); 483 | } 484 | }); 485 | this.visitChildren(node); 486 | }, 487 | JSXElement(node) { 488 | output.children = getChildJSXElements(node, output.name); 489 | output.props = getReactProps(node, output.name); 490 | if (htmlElements.indexOf(node.openingElement.name.name) < 0) { 491 | outside = { 492 | name: node.openingElement.name.name, 493 | children: getChildJSXElements(node, output.name), 494 | props: getReactProps(node, output.name), 495 | state: {}, 496 | methods: [], 497 | }; 498 | } 499 | }, 500 | }); 501 | 502 | const forIn = esquery(ast, 'ForInStatement').filter(ele => { 503 | const searched = bfs(ele).filter(n => { 504 | return n.type === 'JSXElement'; 505 | }); 506 | return searched.length > 0; 507 | }); 508 | if (forIn.length > 0) iter = iter.concat(forInFinder(forIn, output.name)); 509 | 510 | const forLoop = esquery(ast, 'ForStatement').filter(ele => { 511 | const searched = bfs(ele).filter(n => { 512 | return n.type === 'JSXElement'; 513 | }); 514 | return searched.length > 0; 515 | }); 516 | if (forLoop.length > 0) iter = iter.concat(forLoopFinder(forLoop, output.name)); 517 | 518 | const higherOrderFunc = esquery(ast, 'CallExpression').filter(ele => { 519 | let higherOrderChecker = false; 520 | const searched = bfs(ele).filter(n => { 521 | return n.type === 'JSXElement'; 522 | }); 523 | if (ele.callee.property && ele.callee.property.name.match(/(map|forEach|filter|reduce)/)) { 524 | higherOrderChecker = ele.callee.property.name.match(/(map|forEach|filter|reduce)/); 525 | } 526 | return searched.length > 0 && higherOrderChecker; 527 | }); 528 | if (higherOrderFunc.length > 0) { 529 | iter = iter.concat(higherOrderFunctionFinder(higherOrderFunc, output.name)); 530 | } 531 | if (outside) output.children.push(outside); 532 | output.children.forEach((ele, i) => { 533 | checker[ele.name] = i; 534 | }); 535 | 536 | for (let i = 0; i < iter.length; i++) { 537 | if (checker.hasOwnProperty(iter[i].jsx.name)) { 538 | output.children[checker[iter[i].jsx.name]] = iter[i].jsx; 539 | } 540 | } 541 | return output; 542 | } 543 | 544 | 545 | /** 546 | * Helper function to convert Javascript stringified code to an AST using acorn-jsx library 547 | * @param js 548 | */ 549 | function jsToAst(js) { 550 | const ast = acorn.parse(js, { 551 | plugins: { jsx: true }, 552 | }); 553 | if (ast.body.length === 0) throw new Error('Empty AST input'); 554 | return ast; 555 | } 556 | 557 | function componentChecker(ast) { 558 | for (let i = 0; i < ast.body.length; i++) { 559 | if (ast.body[i].type === 'ClassDeclaration') return 'ES6'; 560 | if (ast.body[i].type === 'ExportDefaultDeclaration' 561 | && ast.body[i].declaration.type === 'ClassDeclaration') return 'ES6'; 562 | if (ast.body[i].type === 'VariableDeclaration' && ast.body[i].declarations[0].init 563 | && ast.body[i].declarations[0].init.callee 564 | && ast.body[i].declarations[0].init.callee.object 565 | && ast.body[i].declarations[0].init.callee.object.name === 'React' 566 | && ast.body[i].declarations[0].init.callee.property.name === 'createClass') return 'ES5'; 567 | } 568 | return 'SFC'; 569 | } 570 | 571 | module.exports = { 572 | jsToAst, 573 | componentChecker, 574 | getES5ReactComponents, 575 | getES6ReactComponents, 576 | getStatelessFunctionalComponents, 577 | forLoopFinder, 578 | forInFinder, 579 | higherOrderFunctionFinder, 580 | }; 581 | -------------------------------------------------------------------------------- /src/stringRegexHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | regexIndexOf(string, regex, startpos) { 5 | const indexOf = string.substring(startpos || 0).search(regex); 6 | return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf; 7 | }, 8 | regexLastIndexOf(string, regex, startpos) { 9 | let regexCopy = new RegExp(regex); 10 | let startPosCopy = startpos; 11 | if (!regexCopy.global) { 12 | const ignoreCase = regex.ignoreCase ? 'i' : ''; 13 | const multiLine = regex.multiLine ? 'm' : ''; 14 | regexCopy = new RegExp(regexCopy.source, `g${ignoreCase}${multiLine}`); 15 | } 16 | if (typeof (startpos) === 'undefined') { 17 | startPosCopy = string.length; 18 | } else if (startpos < 0) { 19 | startPosCopy = 0; 20 | } 21 | const stringToWorkWith = string.substring(0, startPosCopy + 1); 22 | let lastIndexOf = -1; 23 | let result = regexCopy.exec(stringToWorkWith); 24 | while (result !== null) { 25 | lastIndexOf = result.index; 26 | regexCopy.lastIndex = result.index + result[0].length; 27 | result = regexCopy.exec(stringToWorkWith); 28 | } 29 | return lastIndexOf; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/test/astGeneratorTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const expect = require('chai').expect; 3 | const es5Component = __dirname + '/fixtures/test_components/Foo_es5.jsx'; 4 | const es6Component = __dirname + '/fixtures/test_components/Foo_es6.jsx'; 5 | const ReactDOMRender = __dirname + '/fixtures/test_components/ReactDOMRender.jsx'; 6 | const astGenerator = require('../astGenerator'); 7 | 8 | describe('astGenerator Tests for ES5 Components', function() { 9 | 10 | it('should be a function', function() { 11 | expect(astGenerator).to.be.a.function; 12 | }) 13 | 14 | it('should return an object', function() { 15 | expect(astGenerator(es5Component)).to.be.an('object') 16 | }) 17 | 18 | it('should return an object with one key', function() { 19 | expect(Object.keys(astGenerator(es5Component)).length).to.equal(1); 20 | }) 21 | 22 | it('should have key based on export name', function() { 23 | expect(astGenerator(es5Component).hasOwnProperty('Foo')).to.be.true; 24 | }) 25 | 26 | }) 27 | 28 | describe('astGenerator Tests for ES6 Components', function() { 29 | 30 | it('should be a function', function() { 31 | expect(astGenerator).to.be.a.function; 32 | }) 33 | 34 | it('should return an object', function() { 35 | expect(astGenerator(es6Component)).to.be.an('object') 36 | }) 37 | 38 | it('should return an object with one key', function() { 39 | expect(Object.keys(astGenerator(es6Component)).length).to.equal(1); 40 | }) 41 | 42 | it('should have key based on export name', function() { 43 | expect(astGenerator(es6Component).hasOwnProperty('Foo')).to.be.true; 44 | }) 45 | 46 | }) 47 | 48 | describe('astGenerator Tests to find entry', function() { 49 | it('should return an object', function() { 50 | expect(astGenerator(ReactDOMRender)).to.be.an('object'); 51 | }) 52 | 53 | it('should return an object with one key', function() { 54 | expect(Object.keys(astGenerator(ReactDOMRender)).length).to.equal(1); 55 | }) 56 | 57 | it('should have key called "ENTRY', function() { 58 | expect(astGenerator(ReactDOMRender).hasOwnProperty('ENTRY')).to.be.true; 59 | }) 60 | }) -------------------------------------------------------------------------------- /src/test/d3DataBuilderTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const expect = require('chai').expect; 3 | const assign = require('lodash.assign'); 4 | const d3DataBuilder = require('../d3DataBuilder'); 5 | const astGenerator = require('../astGenerator') 6 | 7 | 8 | describe('d3DataBuilder Unit Tests', function() { 9 | 10 | const app = __dirname + '/fixtures/test_components/app.jsx'; 11 | const BIG = __dirname + '/fixtures/test_components/BIG.jsx'; 12 | const Biggie = __dirname + '/fixtures/test_components/Biggie.jsx'; 13 | const BigPoppa = __dirname + '/fixtures/test_components/BigPoppa.jsx'; 14 | const Notorious = __dirname + '/fixtures/test_components/Notorious.jsx'; 15 | let parseComponents = [app, BIG, Biggie, BigPoppa, Notorious].map(ele => {return astGenerator(ele)}); 16 | const astObj = assign.apply(null, parseComponents), 17 | d3Obj = d3DataBuilder(astObj); 18 | 19 | 20 | it('should be a function', function() { 21 | expect(d3DataBuilder).to.be.a.function; 22 | }) 23 | 24 | it('should return an object', function() { 25 | expect(d3Obj).to.be.an('object'); 26 | }) 27 | 28 | it('should have all components as field properties on object returned', function() { 29 | expect(d3Obj['BIG']).to.exist; 30 | expect(d3Obj['Biggie']).to.exist; 31 | expect(d3Obj['BigPoppa']).to.exist; 32 | expect(d3Obj['Notorious']).to.exist; 33 | }); 34 | 35 | it('should have child length of 2 for BigPoppa and child length of 1 for Notorious components', function() { 36 | expect(d3Obj['BigPoppa'].children.length).to.equal(2); 37 | expect(d3Obj['Notorious'].children.length).to.equal(1); 38 | }); 39 | 40 | /** 41 | * Below unit tests should be deprecated to account for breaking 42 | * change in formatting structure of returned d3 data object 43 | */ 44 | xit('should start with the correct component', function() { 45 | expect(d3Obj.name).to.equal('BigPoppa'); 46 | }) 47 | 48 | xit('should have component nesting', function() { 49 | expect(d3Obj.children.length).to.equal(2); 50 | expect(d3Obj.children[0].children.length).to.equal(1); 51 | }) 52 | 53 | xit('should account for props', function() { 54 | expect(d3Obj.children[0].props.length).to.equal(3); 55 | expect(d3Obj.children[0].children[0].props.length).to.equal(2); 56 | }) 57 | 58 | xit('should account for state', function() { 59 | expect(d3Obj).to.deep.equal(require('./fixtures/dummyTree')); 60 | }) 61 | 62 | }) -------------------------------------------------------------------------------- /src/test/fixtures/bundleFileFixture.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bundledSetState:` 3 | key: 'handleDelete', 4 | value: function handleDelete(index) { 5 | this.setState({ items: this.state.items.filter(function (item, i) { 6 | return i !== index; 7 | }) }); 8 | }, 9 | (0, _reactDom.render)(_react2.default.createElement( 10 | Frame, 11 | null, 12 | _react2.default.createElement(App, null) 13 | ), document.getElementById('app'));`, 14 | 15 | modifiedBundle:` 16 | key: 'handleDelete', 17 | value: function handleDelete(index) { 18 | wrapper(this.setState)({ items: this.state.items.filter(function (item, i) { 19 | return i !== index; 20 | }) }); 21 | }, 22 | (0, _reactDom.render)(_react2.default.createElement( 23 | Frame, 24 | null, 25 | _react2.default.createElement(App, null) 26 | ), document.getElementById("preview"));`, 27 | 28 | bundledES5InitialState:`var Game = _react2.default.createClass({ 29 | displayName: 'Game', 30 | 31 | getInitialState: function getInitialState() { 32 | var boardArr = []; 33 | for (var i = 0; i < 25; i++) { 34 | boardArr.push(this.randomLetterGenerator(DICE)); 35 | } 36 | var buttonStateArr = []; 37 | for (var i = 0; i < 25; i++) { 38 | buttonStateArr.push('inactive'); 39 | } 40 | return { 41 | boardArr: boardArr, 42 | currentWord: '', 43 | buttonStateArr: buttonStateArr, 44 | score: 0, 45 | clickedArr: [] 46 | }; 47 | },`, 48 | 49 | ES5InitialStateOutput:``, 50 | 51 | ES6InitialStateOutput:``, 52 | 53 | bundledES6InitialState:`var TODO = function (_React$Component) { 54 | _inherits(TODO, _React$Component); 55 | 56 | function TODO(props) { 57 | _classCallCheck(this, TODO); 58 | 59 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(TODO).call(this, props)); 60 | 61 | _this.state = { items: ['Learn React', 'Make React App'], another: 'one' }; 62 | return _this; 63 | }`, 64 | 65 | }; -------------------------------------------------------------------------------- /src/test/fixtures/commonReactComponentFixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | forLoopComponentFixture: ` 3 | var Main = React.createClass({ 4 | render: function() { 5 | var items = []; 6 | for (let i = 0; i < this.props.test.length; i++) { 7 | items.push(); 10 | } 11 | return
    12 | {items} 13 |
    14 | } 15 | }); 16 | `, 17 | mapComponentFixture: ` 18 | var Main = React.createClass({ 19 | render: function() { 20 | var items = this.props.test.map(item => ); 23 | return
    24 | {items} 25 |
    26 | } 27 | }); 28 | `, 29 | 30 | forIn: ` 31 | class Foo extends React.Component { 32 | constuctor() { 33 | super(); 34 | } 35 | 36 | 37 | 38 | render() { 39 | const renderArr = []; 40 | for (const key in this.props.bar) { 41 | renderArr.push() 44 | } 45 | 46 | return ( 47 |
    48 | {renderArr} 49 |
    50 | ) 51 | } 52 | } 53 | `, 54 | 55 | forLoop: ` 56 | class Foo extends React.Component { 57 | constuctor() { 58 | super(); 59 | } 60 | 61 | 62 | 63 | render() { 64 | const renderArr = []; 65 | for (let i =0; i < this.props.bar.length; i++) { 66 | renderArr.push() 69 | } 70 | 71 | return ( 72 |
    73 | {renderArr} 74 |
    75 | ) 76 | } 77 | } 78 | 79 | `, 80 | mapFunc: ` 81 | class Foo extends React.Component { 82 | constuctor() { 83 | super(); 84 | } 85 | 86 | 87 | 88 | render() { 89 | const renderArr = this.props.bar.map(ele => { 90 | return () 91 | }) 92 | 93 | return ( 94 |
    95 | {renderArr} 96 |
    97 | ) 98 | } 99 | } 100 | `, 101 | 102 | }; -------------------------------------------------------------------------------- /src/test/fixtures/d3TreeFixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testData1: [{ 3 | "name": "Top Level", 4 | "parent": "null", 5 | "children": [ 6 | { 7 | "name": "Level 2: A", 8 | "parent": "Top Level", 9 | "children": [ 10 | { 11 | "name": "Son of A", 12 | "parent": "Level 2: A" 13 | }, 14 | { 15 | "name": "Daughter of A", 16 | "parent": "Level 2: A" 17 | } 18 | ] 19 | }, 20 | { 21 | "name": "Level 2: B", 22 | "parent": "Top Level" 23 | } 24 | ] 25 | }], 26 | }; -------------------------------------------------------------------------------- /src/test/fixtures/dummyTree.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "name":"BigPoppa", 3 | "methods": [ 'handleClick' ], 4 | "state":{}, 5 | "children": [ 6 | { 7 | "name":"Notorious", 8 | "state":{}, 9 | "methods":[], 10 | "children": [ 11 | { 12 | "name":"BIG", 13 | "children":[], 14 | "state":{}, 15 | "methods":[], 16 | "props": [ 17 | { 18 | "name":"foo", 19 | "parent": "Notorious", 20 | "value": "props.foo" 21 | }, 22 | { 23 | "name":"click", 24 | "parent": "Notorious", 25 | "value": "props.click" 26 | } 27 | ] 28 | } 29 | ], 30 | "props": [ 31 | { 32 | "name":"foo", 33 | "parent": "BigPoppa", 34 | "value": "state.foo" 35 | }, 36 | { 37 | "name":"bar", 38 | "parent": "BigPoppa", 39 | "value": "state.bar" 40 | }, 41 | { 42 | "name":"click", 43 | "parent": "BigPoppa", 44 | "value": "handleClick" 45 | } 46 | ] 47 | }, 48 | { 49 | "name":"Biggie", 50 | "children":[], 51 | "state":{}, 52 | "methods":[], 53 | "props": [ 54 | { 55 | "name":"bar", 56 | "parent": "BigPoppa", 57 | "value": "state.bar" 58 | } 59 | ] 60 | } 61 | ], 62 | "props":[] 63 | } 64 | -------------------------------------------------------------------------------- /src/test/fixtures/es5ReactComponentFixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleMainApp: `var Main = React.createClass({ })`, 3 | nestedComponents: ` 4 | var Main = React.createClass({ 5 | render: function() { 6 | return
    7 | 8 |
    Testing
    9 | 10 | 11 | 12 | 13 |
    14 | } 15 | }); 16 | `, 17 | componentWithProps: ` 18 | var Main = React.createClass({ 19 | render: function () { 20 | return
    21 | 24 | 25 |
    26 | } 27 | }); 28 | `, 29 | componentWithState: ` 30 | var Main = React.createClass({ 31 | getInitialState: function () { 32 | return { 33 | number: 2, 34 | string: 'hello', 35 | boolean: true, 36 | array: [1, 'hello', true], 37 | object: { 38 | name: 'hello again', 39 | age: 27, 40 | engineer: true 41 | } 42 | }; 43 | }, 44 | render: function () { 45 | return
    Test
    46 | } 47 | }); 48 | `, 49 | componentWithMethods: ` 50 | var Main = React.createClass({ 51 | // React Component Lifecycle Methods 52 | componentDidMount: function() { }, 53 | componentWillMount: function() { }, 54 | componentWillReceiveProps: function() { }, 55 | shouldComponentUpdate: function() { }, 56 | componentWillUpdate: function() { }, 57 | componentDidUpdate: function() { }, 58 | componentWillUnmount: function() { }, 59 | 60 | // Custom Component-Level Methods 61 | handleSubmit: function(e) { 62 | this.setState({ 63 | 64 | }); 65 | }, 66 | handleReceiveData: function(e) { }, 67 | render: function() { 68 | return
    Test
    69 | } 70 | }); 71 | `, 72 | } -------------------------------------------------------------------------------- /src/test/fixtures/es6ReactComponentFixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleMainApp: `class Main extends Component {}`, 3 | nestedComponents: ` 4 | class Main extends Component { 5 | render () { 6 | return
    7 | 8 |
    Testing
    9 | 10 | 11 | 12 | 13 |
    14 | } 15 | } 16 | `, 17 | componentWithProps: ` 18 | class Main extends Component { 19 | render () { 20 | return
    21 | 24 | 25 |
    26 | } 27 | } 28 | `, 29 | componentWithState: ` 30 | class Main extends Component { 31 | constructor () { 32 | this.state = { 33 | number: 2, 34 | string: 'hello', 35 | boolean: true, 36 | array: [1, 'hello', true], 37 | object: { 38 | name: 'hello again', 39 | age: 27, 40 | engineer: true 41 | } 42 | } 43 | } 44 | render () { 45 |
    Test
    46 | } 47 | } 48 | `, 49 | componentWithMethods: ` 50 | class Main extends Component { 51 | // React Component Lifecycle Methods 52 | componentDidMount () { } 53 | componentWillMount () { } 54 | componentWillReceiveProps () { } 55 | shouldComponentUpdate () { } 56 | componentWillUpdate () { } 57 | componentDidUpdate () { } 58 | componentWillUnmount () { } 59 | 60 | // Custom Component-Level Methods 61 | handleSubmit (e) { } 62 | handleReceiveData (e) { } 63 | render () { 64 | return
    Test
    65 | } 66 | } 67 | `, 68 | } -------------------------------------------------------------------------------- /src/test/fixtures/es6StatelessFunctionalComponentFixtures.js: -------------------------------------------------------------------------------- 1 | const statelessNonNested = ` 2 | 3 | const Foo = (props) => { 4 | return ( 5 |
    6 | I'm like hey wsup hello 7 |
    8 | ) 9 | } 10 | ` 11 | 12 | const statelessNested = ` 13 | 14 | const Foo = (props) => { 15 | return ( 16 |
    17 | Trap Queens 18 | 19 |
    20 | ) 21 | } 22 | ` 23 | 24 | module.exports = { 25 | statelessNonNested, 26 | statelessNested 27 | } -------------------------------------------------------------------------------- /src/test/fixtures/reactParserOutputFixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleMainAppOutput: { 3 | name: 'Main', 4 | props: {}, 5 | state: {}, 6 | children: [], 7 | methods: [], 8 | }, 9 | nestedComponentsOutput: { 10 | name: 'Main', 11 | props: {}, 12 | state: {}, 13 | methods: [], 14 | children: [ 15 | { 16 | name: 'SearchBar', 17 | props: {}, 18 | state: {}, 19 | methods: [], 20 | children: [], 21 | }, 22 | { 23 | name: 'SearchResults', 24 | props: {}, 25 | state: {}, 26 | methods: [], 27 | children: [ 28 | { 29 | name: 'Result', 30 | props: {}, 31 | state: {}, 32 | methods: [], 33 | children: [], 34 | }, 35 | { 36 | name: 'Result', 37 | props: {}, 38 | state: {}, 39 | methods: [], 40 | children: [], 41 | }, 42 | ], 43 | } 44 | ], 45 | }, 46 | componentWithPropsOutput: { 47 | name: 'Main', 48 | props: {}, 49 | state: {}, 50 | methods: [], 51 | children: [ 52 | { name: 'SearchBar' , 53 | children: [], 54 | state: {}, 55 | methods: [], 56 | props: [ 57 | { 58 | name: 'onChange', 59 | parent: "Main", 60 | value: "handleTextChange" 61 | }, 62 | { 63 | name: 'onSubmit', 64 | parent: "Main", 65 | value: "handleSubmit" 66 | } 67 | ] 68 | } 69 | ], 70 | }, 71 | componentWithStateOutput: { 72 | name: 'Main', 73 | props: {}, 74 | state: {}, 75 | children: [], 76 | methods: [], 77 | }, 78 | componentWithMethodsOutput: { 79 | name: 'Main', 80 | props: {}, 81 | children: [], 82 | state: {}, 83 | methods: ['handleSubmit', 'handleReceiveData'], 84 | }, 85 | nestedForLoopOutput: { 86 | name: 'Main', 87 | props: {}, 88 | children: [ 89 | { 90 | name: 'ListItem', 91 | props: [ 92 | { 93 | name: 'onChange', 94 | parent: 'Main', 95 | value: 'handleChange' 96 | }, 97 | { 98 | name: 'onSubmit', 99 | parent: 'Main', 100 | value: 'handleSubmit' 101 | } 102 | ], 103 | children: [], 104 | state: {}, 105 | methods: [], 106 | iterated: 'forLoop', 107 | source: 'props.test', 108 | } 109 | ], 110 | state: {}, 111 | methods: [], 112 | }, 113 | nestedHigherOrderOutput: { 114 | name: 'Main', 115 | props: {}, 116 | children: [ 117 | { 118 | name: 'ListItem', 119 | props: [ 120 | { 121 | name: 'onChange', 122 | parent: 'Main', 123 | value: 'handleChange' 124 | }, 125 | { 126 | name: 'onSubmit', 127 | parent: 'Main', 128 | value: 'handleSubmit' 129 | } 130 | ], 131 | children: [], 132 | state: {}, 133 | methods: [], 134 | iterated: 'higherOrder', 135 | source: 'props.test', 136 | } 137 | ], 138 | state: {}, 139 | methods: [], 140 | }, 141 | } -------------------------------------------------------------------------------- /src/test/fixtures/test_components/BIG.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export default class BIG extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | let favorite; 11 | if (foo) favorite = 'Notorious BIG' 12 | else favorite = '2pac' 13 | return( 14 |
    15 |

    {favorite} is my favorite rapper!

    16 |
    17 | ) 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/fixtures/test_components/BigPoppa.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Notorious from './Notorious.jsx'; 3 | import Biggie from './Biggie.jsx'; 4 | 5 | 6 | class BigPoppa extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | foo: true, 11 | bar: 'yo ima string' 12 | } 13 | this.handleClick = this.handleClick.bind(this); 14 | } 15 | 16 | handleClick() { 17 | return this.setState({ 18 | baz: 'im bazzing' 19 | }) 20 | } 21 | 22 | render() { 23 | return( 24 |
    25 | 30 | 33 |
    34 | ) 35 | } 36 | } 37 | 38 | 39 | export default BigPoppa; -------------------------------------------------------------------------------- /src/test/fixtures/test_components/Biggie.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | 5 | 6 | class Biggie extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | return( 13 |
    14 |

    {this.props.bar} and I spit hot fire

    15 |
    16 | ) 17 | } 18 | } 19 | 20 | 21 | export default Biggie; -------------------------------------------------------------------------------- /src/test/fixtures/test_components/Foo_es5.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | 4 | 5 | 6 | 7 | 8 | 9 | var Foo = React.createClass({ 10 | render: function() { 11 | return ( 12 |
    13 | Hello World! 14 |
    15 | ) 16 | } 17 | }) 18 | 19 | 20 | module.exports = Foo; -------------------------------------------------------------------------------- /src/test/fixtures/test_components/Foo_es6.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | 5 | 6 | export default class Foo extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | 12 | 13 | render() { 14 | return ( 15 |
    16 | Hello World! 17 |
    18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/fixtures/test_components/Notorious.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class Notorious extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return( 11 |
    12 | {this.props.bar}, and I like it when you call me big poppa 13 | 17 |
    18 | ) 19 | } 20 | } 21 | 22 | 23 | export default Notorious; -------------------------------------------------------------------------------- /src/test/fixtures/test_components/ReactDOMRender.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom' 3 | import Foo from './Foo_es6.jsx'; 4 | 5 | 6 | 7 | 8 | ReactDOM.render(, 'content'); -------------------------------------------------------------------------------- /src/test/fixtures/test_components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import BigPoppa from './BigPoppa.jsx'; 4 | 5 | ReactDOM.render(, 'content'); -------------------------------------------------------------------------------- /src/test/iterTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const expect = require('chai').expect; 3 | const esquery = require('../../esquery/esquery'); 4 | const acorn = require('acorn-jsx'); 5 | const bfs = require('acorn-bfs'); 6 | const fixtures = require('./fixtures/commonReactComponentFixtures'); 7 | const reactParser = require('../reactParser'); 8 | 9 | 10 | describe('iterations', function() { 11 | const forLoopAST = acorn.parse(fixtures.forLoop, { 12 | plugins: { jsx: true }, 13 | }); 14 | const forLoop = esquery(forLoopAST, 'ForStatement').filter(ele => { 15 | const searched = bfs(ele).filter(n => { 16 | return n.type === 'JSXElement'; 17 | }); 18 | return searched.length > 0; 19 | }); 20 | 21 | const forInAST = acorn.parse(fixtures.forIn, { 22 | plugins: { jsx: true }, 23 | }); 24 | const forIn = esquery(forInAST, 'ForInStatement').filter(ele => { 25 | const searched = bfs(ele).filter(n => { 26 | return n.type === 'JSXElement'; 27 | }); 28 | return searched.length > 0; 29 | }); 30 | 31 | const mapAST = acorn.parse(fixtures.mapFunc, { 32 | plugins: { jsx: true }, 33 | }); 34 | const mapFunc = esquery(mapAST, 'CallExpression').filter(ele => { 35 | const searched = bfs(ele).filter(n => { 36 | return n.type === 'JSXElement'; 37 | }); 38 | return searched.length > 0; 39 | }); 40 | 41 | 42 | it ('should do something with forIn', function() { 43 | const forInFinder = reactParser.forInFinder; 44 | console.log(forInFinder(forIn, 'test')); 45 | }) 46 | 47 | it ('should do something with for loop', function() { 48 | const forLoopFinder = reactParser.forLoopFinder; 49 | console.log(forLoopFinder(forLoop, 'test')); 50 | }) 51 | 52 | it('should do something with map', function() { 53 | const higherOrderFunctionFinder = reactParser.higherOrderFunctionFinder; 54 | console.log(higherOrderFunctionFinder(mapFunc, 'test')); 55 | console.log(higherOrderFunctionFinder(mapFunc, 'test')[0].jsx.props); 56 | }) 57 | 58 | }) -------------------------------------------------------------------------------- /src/test/previewParserTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const fs = require('fs'); 5 | 6 | describe('ReactApp AST Parser Tests', function() { 7 | const previewParserFixtures = require('./fixtures/bundleFileFixture.js'); 8 | const modifySetStateStrings = require('../previewParser.js').modifySetStateStrings; 9 | const modifyInitialState = require('../previewParser.js').modifyInitialState; 10 | 11 | it('modifySetStateStrings should be a function', function() { 12 | expect(modifySetStateStrings).to.be.a.function; 13 | }); 14 | 15 | it('modifySetStateStrings should throw error when parser receives invalid file path', function() { 16 | expect(modifySetStateStrings.bind(modifySetStateStrings, '')) 17 | .to.throw(Error, /Invalid bundle file path specified. Please enter a valid path to your app\'s bundle file/); 18 | }); 19 | 20 | it('modifySetStateStrings should return a string', function(done) { 21 | expect(modifySetStateStrings(__dirname + '/fixtures/modifySetStateStringsInputFixture.js').length) 22 | .to.equal(fs.readFileSync(__dirname + '/fixtures/modifySetStateStringsOutputFixture.js', { encoding: 'utf-8' }).length); 23 | done(); 24 | }); 25 | 26 | it('modifyInitialState should be a function', function() { 27 | expect(modifyInitialState).to.be.a.function; 28 | }); 29 | 30 | xit('modifyInitialState should return a string', function() { 31 | expect(modifyInitialState(fs.readFileSync(__dirname + '/fixtures/modifySetStateStringsInputFixture.js', { encoding: 'utf-8' })).length) 32 | .to.equal(fs.readFileSync(__dirname + '/fixtures/modifyInitialStateOutputFixture.js').length) 33 | }) 34 | 35 | describe('getComponentName Tests', function() { 36 | const getComponentName = require('../previewParser.js').getComponentName; 37 | 38 | it('should return a valid function', function() { 39 | expect(getComponentName).to.be.a('function'); 40 | }); 41 | 42 | it('should return a component name for ES6 using Component', function() { 43 | const bundle = 'App=function(_Component)'; 44 | expect(getComponentName(bundle, bundle.length)).to.equal('App'); 45 | }); 46 | 47 | it('should return a component name for ES6 using React.Component', function() { 48 | const bundle = 'Base=function(_React$Component)'; 49 | expect(getComponentName(bundle, bundle.length)).to.equal('Base'); 50 | }); 51 | 52 | it('should return a component name for ES5 using React.createClass with spaces', function() { 53 | const bundle = 'var Node = (324, _react.createClass)'; 54 | expect(getComponentName(bundle, bundle.length)).to.equal('Node'); 55 | }); 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/test/reactInterceptorTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | 5 | describe('React Interceptor Helper Tests', function() { 6 | const monocleHook = require('../reactInterceptor.js').reactInterceptor; 7 | const initialStateWrapper = require('../reactInterceptor.js').grabInitialState; 8 | 9 | it('monocleHook should return a valid function', function() { 10 | expect(monocleHook).to.be.a('function'); 11 | }); 12 | 13 | it('should update mocked react component\'s state property', function() { 14 | const MockReactComponent = { 15 | state: { }, 16 | setState: function(newState, callback) { 17 | this.state = Object.assign(this.state, newState); 18 | if (callback) callback(); 19 | } 20 | }; 21 | 22 | const hijackedSetState = monocleHook('App', MockReactComponent); 23 | hijackedSetState({ name: 'John' }); 24 | expect(MockReactComponent.state).to.deep.equal({ name: 'John' }); 25 | }); 26 | 27 | it('initialStateWrapper should be a function', function() { 28 | expect(initialStateWrapper).to.be.a('function'); 29 | }); 30 | 31 | it('initialStateWrapper should be a function', function() { 32 | expect(initialStateWrapper).to.be.a('function'); 33 | }); 34 | 35 | }); -------------------------------------------------------------------------------- /src/test/reactParserTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | 5 | describe('ESTree AST Parser Tests', function() { 6 | const jsToAst = require('../reactParser.js').jsToAst; 7 | let reactParserOutputFixtures = require('./fixtures/reactParserOutputFixtures.js'); 8 | 9 | it('jsToAst should be a function', function() { 10 | expect(jsToAst).to.be.a.function; 11 | }); 12 | 13 | it('should throw error when parser receives empty js code string', function() { 14 | expect(jsToAst.bind(jsToAst,'')).to.throw(Error, /Empty AST input/); 15 | }); 16 | 17 | describe('ES5 React Component Parsing Tests', function() { 18 | const getES5ReactComponents = require('../reactParser.js').getES5ReactComponents; 19 | const es5ParserFixtures = require('./fixtures/es5ReactComponentFixtures.js'); 20 | 21 | it('getES5ReactComponents should be a function', function() { 22 | expect(getES5ReactComponents).to.be.a.function; 23 | }); 24 | 25 | it('should return object with \'Main\' as top-level component with child property containing array with single object with name property equal \'SearchBar\'', function() { 26 | expect(getES5ReactComponents(jsToAst(es5ParserFixtures.nestedComponents))) 27 | .to.deep.equal(reactParserOutputFixtures.nestedComponentsOutput); 28 | }); 29 | 30 | it('should return object with props property', function() { 31 | expect(getES5ReactComponents(jsToAst(es5ParserFixtures.componentWithProps))) 32 | .to.deep.equal(reactParserOutputFixtures.componentWithPropsOutput); 33 | }); 34 | 35 | it('should return object with state property', function() { 36 | expect(getES5ReactComponents(jsToAst(es5ParserFixtures.componentWithState))) 37 | .to.deep.equal(reactParserOutputFixtures.componentWithStateOutput); 38 | }); 39 | 40 | it('should return object with methods property', function() { 41 | expect(getES5ReactComponents(jsToAst(es5ParserFixtures.componentWithMethods))) 42 | .to.deep.equal(reactParserOutputFixtures.componentWithMethodsOutput); 43 | }); 44 | }); 45 | 46 | describe('ES6 React Component Parsing Tests', function() { 47 | const getES6ReactComponents = require('../reactParser.js').getES6ReactComponents; 48 | const es6ParserFixtures = require('./fixtures/es6ReactComponentFixtures.js'); 49 | 50 | it('should return object with name of top-level components in js file using es6', function() { 51 | expect(getES6ReactComponents(jsToAst(es6ParserFixtures.singleMainApp))) 52 | .to.deep.equal(reactParserOutputFixtures.singleMainAppOutput); 53 | }); 54 | 55 | it('should return object with \'Main\' as top-level component with nested children components', function() { 56 | expect(getES6ReactComponents(jsToAst(es6ParserFixtures.nestedComponents))) 57 | .to.deep.equal(reactParserOutputFixtures.nestedComponentsOutput); 58 | }); 59 | 60 | it('should return object with props property', function() { 61 | expect(getES6ReactComponents(jsToAst(es6ParserFixtures.componentWithProps))) 62 | .to.deep.equal(reactParserOutputFixtures.componentWithPropsOutput); 63 | }); 64 | 65 | it('should return object with state property', function() { 66 | expect(getES6ReactComponents(jsToAst(es6ParserFixtures.componentWithState))) 67 | .to.deep.equal(reactParserOutputFixtures.componentWithStateOutput); 68 | }); 69 | 70 | it('should return object with methods property', function() { 71 | expect(getES6ReactComponents(jsToAst(es6ParserFixtures.componentWithMethods))) 72 | .to.deep.equal(reactParserOutputFixtures.componentWithMethodsOutput); 73 | }); 74 | }); 75 | 76 | describe('ES6 Stateless Functional Component Parsing Tests', function() { 77 | const getStatelessFunctionalComponents = require('../reactParser').getStatelessFunctionalComponents; 78 | const statelessFuncFixtures = require('./fixtures/es6StatelessFunctionalComponentFixtures'); 79 | 80 | it('should be a function', function() { 81 | expect(getStatelessFunctionalComponents).to.be.a.function; 82 | }) 83 | 84 | it('should return an object with correct name property', function() { 85 | expect(getStatelessFunctionalComponents(jsToAst(statelessFuncFixtures.statelessNested), 'Foo').name).to.equal('Foo'); 86 | }) 87 | 88 | it('should account for nested children components', function() { 89 | expect(getStatelessFunctionalComponents(jsToAst(statelessFuncFixtures.statelessNested), 'Foo').children[0].name).to.equal('Bar'); 90 | }) 91 | 92 | it('should return an object with correct props', function(){ 93 | expect(getStatelessFunctionalComponents(jsToAst(statelessFuncFixtures.statelessNested), 'Foo').children[0].props.length).to.equal(1); 94 | }) 95 | }) 96 | 97 | describe('React JSX Component Composition Tests', function() { 98 | const getES5ReactComponents = require('../reactParser').getES5ReactComponents; 99 | const commonComponentFixtures = require('./fixtures/commonReactComponentFixtures.js'); 100 | const reactParserOutputFixtures = require('./fixtures/reactParserOutputFixtures.js'); 101 | 102 | it('should have getES5ReactComponents return as a valid function', function() { 103 | expect(getES5ReactComponents).to.be.a('function'); 104 | }); 105 | 106 | it('should have for loop construct in render return nested components', function() { 107 | const forLoopComponentFixture = commonComponentFixtures.forLoopComponentFixture; 108 | expect(getES5ReactComponents(jsToAst(forLoopComponentFixture))) 109 | .to.deep.equal(reactParserOutputFixtures.nestedForLoopOutput); 110 | }); 111 | 112 | it('should have MAP construct in render return nested components', function() { 113 | const mapComponentFixture = commonComponentFixtures.mapComponentFixture; 114 | expect(getES5ReactComponents(jsToAst(mapComponentFixture))) 115 | .to.deep.equal(reactParserOutputFixtures.nestedHigherOrderOutput); 116 | }); 117 | }); 118 | }); -------------------------------------------------------------------------------- /src/test/test.js: -------------------------------------------------------------------------------- 1 | global.window = global; 2 | describe('react-monocle Test Suite', function() { 3 | require('./reactParserTest.js'); 4 | require('./astGeneratorTest.js'); 5 | require('./d3DataBuilderTest.js'); 6 | require('./previewParserTest.js'); 7 | require('./reactInterceptorTest.js'); 8 | // require('./d3TreeTest.js'); need to investigate how to test with headless browser 9 | }); 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:9090', 8 | 'webpack/hot/only-dev-server', 9 | './react/index', 10 | ], 11 | 12 | // This will not actually create a bundle.js file in ./client. It is used 13 | // by the dev server for dynamic hot loading. 14 | output: { 15 | path: __dirname + '/client/', 16 | filename: 'app.js', 17 | publicPath: 'http://localhost:9090/client/', 18 | }, 19 | resolve: { 20 | extensions: ['', '.js', '.jsx'], 21 | }, 22 | module: { 23 | loaders: [{ 24 | test: /\.jsx?$/, 25 | loaders: ['react-hot', 'babel-loader'], 26 | exclude: /node_modules/, 27 | }, 28 | ], 29 | }, 30 | plugins: [ 31 | new webpack.HotModuleReplacementPlugin(), 32 | new webpack.NoErrorsPlugin(), 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | 4 | module.exports = { 5 | entry: { 6 | app: './react/index.jsx', 7 | hooks: './react/hooks.jsx', 8 | }, 9 | output: { 10 | path: __dirname + '/src/d3Tree/', 11 | filename: '[name].js', 12 | }, 13 | resolve: { 14 | extensions: ['', '.js', '.jsx'], 15 | }, 16 | module: { 17 | loaders: [{ 18 | test: /\.jsx?$/, 19 | loaders: ['react-hot', 'babel-loader'], 20 | exclude: /node_modules/, 21 | }, 22 | { 23 | test: /\.svg$/, 24 | loader: 'svg-inline', 25 | }, 26 | 27 | // { 28 | // loader: 'uglify', 29 | // }, 30 | ], 31 | }, 32 | plugins: [ 33 | // new ExtractTextPlugin('style.css', { allChunks: true }) 34 | new webpack.DefinePlugin({ 35 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 36 | }), 37 | new webpack.optimize.DedupePlugin(), 38 | new webpack.NoErrorsPlugin(), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | mangle: true, 41 | }), 42 | ], 43 | }; 44 | --------------------------------------------------------------------------------