Skip to content

Commit 0935511

Browse files
authored
Merge pull request #159 from oslabs-beta/master
Master
2 parents afc59e4 + 2dafbed commit 0935511

File tree

183 files changed

+42706
-53
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

183 files changed

+42706
-53
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
node_modules
1+
/node_modules
22
.DS_Store
33
src/extension/build/bundles
44
package/reactime-*.tgz
@@ -8,4 +8,5 @@ coverage
88
src/extension/build.zip
99
src/extension/build.crx
1010
src/extension/build.pem
11-
bower_components
11+
bower_components
12+
sandboxes/manual-tests/NextJS/.next

package/astParser.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
/* eslint-disable no-inner-declarations */
2-
const acorn = require('acorn'); // javascript parser
1+
/* eslint-disable no-inner-declarations, no-loop-func */
32
// eslint-disable-next-line import/newline-after-import
3+
const acorn = require('acorn'); // javascript parser
44
const jsx = require('acorn-jsx');
5+
56
const JSXParser = acorn.Parser.extend(jsx());
67

78
// Helper function to grab the getters/setters from `elementType`
@@ -11,6 +12,7 @@ module.exports = elementType => {
1112
const hookState = {};
1213

1314
while (Object.hasOwnProperty.call(ast, 'body')) {
15+
let tsCount = 0; // Counter for the number of TypeScript hooks seen (to distinguish in masterState)
1416
ast = ast.body;
1517
const statements = [];
1618

@@ -26,7 +28,19 @@ module.exports = elementType => {
2628
body.forEach(elem => {
2729
if (elem.type === 'VariableDeclaration') {
2830
elem.declarations.forEach(hook => {
29-
statements.push(hook.id.name);
31+
// * TypeScript hooks appear to have no "VariableDeclarator"
32+
// * with id.name of _useState, _useState2, etc...
33+
// * hook.id.type relevant for TypeScript applications
34+
// *
35+
// * Works for useState hooks
36+
if (hook.id.type === 'ArrayPattern') {
37+
hook.id.elements.forEach(hook => {
38+
statements.push(hook.name);
39+
// * Unshift a wildcard name to achieve similar functionality as before
40+
statements.unshift(`_useWildcard${tsCount}`);
41+
tsCount += 1;
42+
});
43+
} else statements.push(hook.id.name);
3044
});
3145
}
3246
});

package/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* 'reactime' module has a single export
3+
* @function linkFiber
4+
*/
5+
6+
// * State snapshot object initialized here
17
const snapShot = { tree: null };
28

39
const mode = {
@@ -21,10 +27,11 @@ function getRouteURL(node) {
2127
}
2228
}
2329

24-
window.addEventListener('message', ({ data: { action, payload } }) => { // runs automatically twice per second with inspectedElement
30+
// * Event listener for time-travel actions
31+
window.addEventListener('message', ({ data: { action, payload } }) => {
2532
switch (action) {
2633
case 'jumpToSnap':
27-
timeJump(payload);
34+
timeJump(payload); // * This sets state with given payload
2835
// Get the pathname from payload and add new entry to browser history
2936
// MORE: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
3037
window.history.pushState('', '', getRouteURL(payload));

package/linkFiber.js

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
1+
/**
2+
* This file contains core module functionality.
3+
*
4+
* It exports an anonymous
5+
* @function
6+
* that is invoked on
7+
* @param snap --> Current snapshot
8+
* @param mode --> Current mode (jumping i.e. time-traveling, locked, or paused)
9+
* and @returns a function to be invoked on the rootContainer HTMLElement
10+
*
11+
* @function updateSnapShotTree
12+
* --> Middleware #1: Updates snap object with latest snapshot
13+
*
14+
* @function sendSnapshot
15+
* --> Middleware #2: Gets a copy of the current snap.tree and posts a message to the window
16+
*
17+
* @function changeSetState
18+
* @param component : stateNode property on a stateful class component's FiberNode object
19+
* --> Binds class component setState method to the component
20+
* --> Injects middleware into class component's setState method
21+
*
22+
* @function changeUseState
23+
* @param component : memoizedState property on a stateful functional component's FiberNode object
24+
* --> Binds functional component dispatch method to the component
25+
* --> Injects middleware into component's dispatch method
26+
* Note: dispatch is hook equivalent to setState()
27+
*
28+
* @function traverseHooks
29+
* @param memoizedState : memoizedState property on a stateful fctnl component's FiberNode object
30+
* --> Helper function to traverse through memoizedState
31+
* --> Invokes @changeUseState on each stateful functional component
32+
*
33+
* @function createTree
34+
* @param currentFiber : a FiberNode object
35+
* --> Recursive function to traverse from FiberRootNode and create
36+
* an instance of custom Tree class and build up state snapshot
37+
*/
38+
139
/* eslint-disable no-underscore-dangle */
240
/* eslint-disable func-names */
341
/* eslint-disable no-use-before-define */
442
/* eslint-disable no-param-reassign */
5-
// links component state tree to library
6-
// changes the setState method to also update our snapshot
43+
744
const Tree = require('./tree');
845
const astParser = require('./astParser');
946
const { saveState } = require('./masterState');
@@ -14,80 +51,80 @@ module.exports = (snap, mode) => {
1451
let concurrent = false; // flag to check if we are in concurrent mode
1552

1653
function sendSnapshot() {
17-
// don't send messages while jumping or while paused
54+
// Don't send messages while jumping or while paused
1855
// DEV: So that when we are jumping to an old snapshot it
19-
// wouldn't think we want to create new snapshots
2056
if (mode.jumping || mode.paused) return;
2157
const payload = snap.tree.getCopy();
22-
// console.log('payload', payload);
2358
window.postMessage({
2459
action: 'recordSnap',
2560
payload,
2661
});
2762
}
2863

2964
function changeSetState(component) {
30-
// check that setState hasn't been changed yet
3165
if (component.setState.linkFiberChanged) return;
32-
// make a copy of setState
66+
67+
// Persist the old setState and bind to component so we can continue to setState({})
3368
const oldSetState = component.setState.bind(component);
34-
// replace component's setState so developer doesn't change syntax
35-
// component.setState = newSetState.bind(component);
69+
3670
component.setState = (state, callback = () => {}) => {
37-
// don't do anything if state is locked
38-
// UNLESS we are currently jumping through time
71+
// Don't do anything if state is locked UNLESS we are currently jumping through time
3972
if (mode.locked && !mode.jumping) return;
40-
// continue normal setState functionality, except add sending message middleware
73+
// Continue normal setState functionality, with middleware in callback
4174
oldSetState(state, () => {
4275
updateSnapShotTree();
4376
sendSnapshot();
4477
callback.bind(component)();
4578
});
4679
};
80+
// Set a custom property to ensure we don't change this method again
4781
component.setState.linkFiberChanged = true;
4882
}
4983

5084
function changeUseState(component) {
5185
if (component.queue.dispatch.linkFiberChanged) return;
52-
// store the original dispatch function definition
86+
87+
// Persist the old dispatch and bind to component so we can continue to dispatch()
5388
const oldDispatch = component.queue.dispatch.bind(component.queue);
54-
// redefine the dispatch function so we can inject our code
89+
5590
component.queue.dispatch = (fiber, queue, action) => {
56-
// don't do anything if state is locked
5791
if (mode.locked && !mode.jumping) return;
5892
oldDispatch(fiber, queue, action);
93+
// * Uncomment setTimeout to prevent snapshot lag-effect
94+
// * (i.e. getting the prior snapshot on each state change)
5995
// setTimeout(() => {
6096
updateSnapShotTree();
6197
sendSnapshot();
6298
// }, 100);
6399
};
100+
// Set a custom property to ensure we don't change this method again
64101
component.queue.dispatch.linkFiberChanged = true;
65102
}
66103

67-
// Helper function to traverse through the memoized state
68104
// TODO: WE NEED TO CLEAN IT UP A BIT
69105
function traverseHooks(memoizedState) {
70106
// Declare variables and assigned to 0th index and an empty object, respectively
71107
const memoized = {};
72108
let index = 0;
73109
astHooks = Object.values(astHooks);
74-
// while memoizedState is truthy, save the value to the object
110+
// While memoizedState is truthy, save the value to the object
75111
while (memoizedState && memoizedState.queue) {
76-
// prevents useEffect from crashing on load
112+
// // prevents useEffect from crashing on load
77113
// if (memoizedState.next.queue === null) { // prevents double pushing snapshot updates
78114
changeUseState(memoizedState);
79115
// }
80116
// memoized[astHooks[index]] = memoizedState.memoizedState;
81117
memoized[astHooks[index]] = memoizedState.memoizedState;
82118
// Reassign memoizedState to its next value
83119
memoizedState = memoizedState.next;
84-
// Increment the index by 2
120+
// See astParser.js for explanation of this increment
85121
index += 2;
86122
}
87123
return memoized;
88124
}
89125

90126
function createTree(currentFiber, tree = new Tree('root')) {
127+
// Base case: child or sibling pointed to null
91128
if (!currentFiber) return tree;
92129

93130
const {
@@ -99,19 +136,17 @@ module.exports = (snap, mode) => {
99136
} = currentFiber;
100137

101138
let nextTree = tree;
102-
// check if stateful component
139+
140+
// Check if stateful component
103141
if (stateNode && stateNode.state) {
104-
// add component to tree
105-
nextTree = tree.appendChild(stateNode);
106-
// change setState functionality
107-
changeSetState(stateNode);
142+
nextTree = tree.appendChild(stateNode); // Add component to tree
143+
changeSetState(stateNode); // Change setState functionality
108144
}
109-
// Check if the component uses hooks
110-
// console.log("memoizedState", memoizedState);
111145

146+
// Check if the component uses hooks
112147
if (
113-
memoizedState &&
114-
Object.hasOwnProperty.call(memoizedState, 'baseState')
148+
memoizedState
149+
&& Object.hasOwnProperty.call(memoizedState, 'baseState')
115150
) {
116151
// 'catch-all' for suspense elements (experimental)
117152
if (typeof elementType.$$typeof === 'symbol') return;
@@ -123,18 +158,19 @@ module.exports = (snap, mode) => {
123158
memoizedState.traversed = traverseHooks(memoizedState);
124159
nextTree = tree.appendChild(memoizedState);
125160
}
126-
// iterate through siblings
161+
162+
// Recurse on siblings
127163
createTree(sibling, tree);
128-
// iterate through children
164+
// Recurse on children
129165
createTree(child, nextTree);
130166

131167
return tree;
132168
}
133-
// runs when page initially loads
134-
// but skips 1st hook click
169+
170+
// ! BUG: skips 1st hook click
135171
async function updateSnapShotTree() {
136172
let current;
137-
// if concurrent mode, grab current.child'
173+
// If concurrent mode, grab current.child
138174
if (concurrent) {
139175
// we need a way to wait for current child to populate
140176
const promise = new Promise((resolve, reject) => {
@@ -152,6 +188,7 @@ module.exports = (snap, mode) => {
152188
}
153189

154190
return async container => {
191+
// Point fiberRoot to FiberRootNode
155192
if (container._internalRoot) {
156193
fiberRoot = container._internalRoot;
157194
concurrent = true;
@@ -160,12 +197,13 @@ module.exports = (snap, mode) => {
160197
_reactRootContainer: { _internalRoot },
161198
_reactRootContainer,
162199
} = container;
163-
// only assign internal root if it actually exists
200+
// Only assign internal root if it actually exists
164201
fiberRoot = _internalRoot || _reactRootContainer;
165202
}
166203

167204
await updateSnapShotTree();
168-
// send the initial snapshot once the content script has started up
205+
// Send the initial snapshot once the content script has started up
206+
// This message is sent from contentScript.js in chrome extension bundles
169207
window.addEventListener('message', ({ data: { action } }) => {
170208
if (action === 'contentScriptStarted') sendSnapshot();
171209
});

package/timeJump.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
/**
2+
* This file contains necessary functionality for time-travel feature
3+
*
4+
* It exports an anonymous
5+
* @function
6+
* that is invoked on
7+
* @param target --> a target snapshot portraying some past state
8+
* and recursively sets state for any stateful components.
9+
*
10+
*/
11+
112
/* eslint-disable no-param-reassign */
2-
// traverses given tree by accessing children through coords array
13+
314
const { returnState } = require('./masterState');
415

16+
// Traverses given tree by accessing children through coords array
517
function traverseTree(tree, coords) {
618
let curr = tree;
719
coords.forEach(coord => {
@@ -11,23 +23,33 @@ function traverseTree(tree, coords) {
1123
}
1224

1325
module.exports = (origin, mode) => {
14-
// recursively change state of tree
26+
// Recursively change state of tree
1527
function jump(target, coords = []) {
1628
const originNode = traverseTree(origin.tree, coords);
17-
// set the state of the origin tree if the component is stateful
29+
30+
// Set the state of the origin tree if the component is stateful
1831
if (originNode.component.setState) {
19-
originNode.component.setState(target.state, () => {
20-
// iterate through new children once state has been set
32+
// * Use the function argument when setting state to account for any state properties
33+
// * that may not have existed in the past
34+
originNode.component.setState(prevState => {
35+
Object.keys(prevState).forEach(key => {
36+
if (target.state[key] === undefined) {
37+
target.state[key] = undefined;
38+
}
39+
});
40+
return target.state;
41+
}, () => {
42+
// Iterate through new children once state has been set
2143
target.children.forEach((child, i) => {
2244
jump(child, coords.concat(i));
2345
});
2446
});
2547
} else {
26-
// if component uses hooks, traverse through the memoize tree
48+
// If component uses hooks, traverse through the memoize tree
2749
let current = originNode.component;
2850
let index = 0;
2951
const hooks = returnState();
30-
// while loop through the memoize tree
52+
// While loop through the memoize tree
3153
while (current && current.queue) { // allows time travel with useEffect
3254
current.queue.dispatch(target.state[hooks[index]]);
3355
// Reassign the current value
@@ -38,7 +60,7 @@ module.exports = (origin, mode) => {
3860
}
3961

4062
return target => {
41-
// setting mode disables setState from posting messages to window
63+
// * Setting mode disables setState from posting messages to window
4264
mode.jumping = true;
4365
jump(target);
4466
setTimeout(() => {

sandboxes/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Reactime End-to-End Testing
2+
3+
This folder contains automated tests as well as manual tests. The automated Jest / Puppeteer tests can be run by issuing the npm test command from within the root of the automated-tests directory.
4+
5+
Manual tests have been created for both Concurrent Mode and NextJS. These sandboxes can be launched by issuing the npm start command from within either of the directories inside of the manual-tests folder. Note that for the NextJS sandbox, npm run build should be issued before npm start. These manual sandboxes can also be automated by using the same approach as the automated testing sandboxes.
6+
7+
Automated Tests
8+
9+
- useState
10+
- useEffect
11+
- useContext
12+
- useMemo
13+
- Redux
14+
- React Router
15+
- setState
16+
- Conditional setState
17+
- componentDidMount
18+
19+
Manual Tests
20+
21+
- Concurrent Mode / Suspense
22+
- Next.js

0 commit comments

Comments
 (0)