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 |
--------------------------------------------------------------------------------