|
| 1 | +/* eslint-disable indent */ |
| 2 | +/* eslint-disable brace-style */ |
| 3 | +/* eslint-disable comma-dangle */ |
| 4 | +/** |
| 5 | + * This file contains core module functionality. |
| 6 | + * |
| 7 | + * It exports an anonymous |
| 8 | + * @function |
| 9 | + * that is invoked on |
| 10 | + * @param snap --> Current snapshot |
| 11 | + * @param mode --> Current mode (jumping i.e. time-traveling, locked, or paused) |
| 12 | + * and @returns a function to be invoked on the rootContainer HTMLElement |
| 13 | + * |
| 14 | + * @function updateSnapShotTree |
| 15 | + * --> Middleware #1: Updates snap object with latest snapshot |
| 16 | + * |
| 17 | + * @function sendSnapshot |
| 18 | + * --> Middleware #2: Gets a copy of the current snap.tree and posts a message to the window |
| 19 | + * |
| 20 | + * @function changeSetState |
| 21 | + * @param component : stateNode property on a stateful class component's FiberNode object |
| 22 | + * --> Binds class component setState method to the component |
| 23 | + * --> Injects middleware into class component's setState method |
| 24 | + * |
| 25 | + * @function changeUseState |
| 26 | + * @param component : memoizedState property on a stateful functional component's FiberNode object |
| 27 | + * --> Binds functional component dispatch method to the component |
| 28 | + * --> Injects middleware into component's dispatch method |
| 29 | + * Note: dispatch is hook equivalent to setState() |
| 30 | + * |
| 31 | + * @function traverseHooks |
| 32 | + * @param memoizedState : memoizedState property on a stateful fctnl component's FiberNode object |
| 33 | + * --> Helper function to traverse through memoizedState |
| 34 | + * --> Invokes @changeUseState on each stateful functional component |
| 35 | + * |
| 36 | + * @function createTree |
| 37 | + * @param currentFiber : a FiberNode object |
| 38 | + * --> Recursive function to traverse from FiberRootNode and create |
| 39 | + * an instance of custom Tree class and build up state snapshot |
| 40 | + */ |
| 41 | + |
| 42 | +/* eslint-disable no-underscore-dangle */ |
| 43 | +/* eslint-disable func-names */ |
| 44 | +/* eslint-disable no-use-before-define */ |
| 45 | +/* eslint-disable no-param-reassign */ |
| 46 | + |
| 47 | +const Tree = require('./tree'); |
| 48 | +const componentActionsRecord = require('./masterState'); |
| 49 | + |
| 50 | +module.exports = (snap, mode) => { |
| 51 | + let fiberRoot = null; |
| 52 | + |
| 53 | + async function sendSnapshot() { |
| 54 | + // Don't send messages while jumping or while paused |
| 55 | + if (mode.jumping || mode.paused) return; |
| 56 | + console.log('PAYLOAD: before cleaning', snap.tree); |
| 57 | + const payload = snap.tree.cleanTreeCopy();// snap.tree.getCopy(); |
| 58 | + console.log('PAYLOAD: after cleaning', payload); |
| 59 | + try { |
| 60 | + await window.postMessage({ |
| 61 | + action: 'recordSnap', |
| 62 | + payload, |
| 63 | + }); |
| 64 | + } catch (e) { |
| 65 | + console.log('failed to send postMessage:', e); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + // Carlos: Injects instrumentation to update our state tree every time |
| 70 | + // a hooks component changes state |
| 71 | + function traverseHooks(memoizedState) { |
| 72 | + const hooksStates = []; |
| 73 | + while (memoizedState && memoizedState.queue) { |
| 74 | + // Carlos: these two are legacy comments, we should look into them later |
| 75 | + // prevents useEffect from crashing on load |
| 76 | + // if (memoizedState.next.queue === null) { // prevents double pushing snapshot updates |
| 77 | + if (memoizedState.memoizedState) { |
| 78 | + console.log('memoizedState in traverseHooks is:', memoizedState); |
| 79 | + hooksStates.push({ |
| 80 | + component: memoizedState.queue, |
| 81 | + state: memoizedState.memoizedState, |
| 82 | + }); |
| 83 | + } |
| 84 | + // console.log('GOT STATE', memoizedState.memoizedState); |
| 85 | + memoizedState = memoizedState.next !== memoizedState |
| 86 | + ? memoizedState.next : null; |
| 87 | + } |
| 88 | + return hooksStates; |
| 89 | + } |
| 90 | + |
| 91 | + // Carlos: This runs after EVERY Fiber commit. It creates a new snapshot, |
| 92 | + // |
| 93 | + function createTree(currentFiber, tree = new Tree('root')) { |
| 94 | + // Base case: child or sibling pointed to null |
| 95 | + if (!currentFiber) return null; |
| 96 | + if (!tree) return tree; |
| 97 | + |
| 98 | + // These have the newest state. We update state and then |
| 99 | + // called updateSnapshotTree() |
| 100 | + const { |
| 101 | + sibling, |
| 102 | + stateNode, |
| 103 | + child, |
| 104 | + memoizedState, |
| 105 | + elementType, |
| 106 | + tag, |
| 107 | + actualDuration, |
| 108 | + actualStartTime, |
| 109 | + selfBaseDuration, |
| 110 | + treeBaseDuration, |
| 111 | + } = currentFiber; |
| 112 | + |
| 113 | + let newState = null; |
| 114 | + let componentData = {}; |
| 115 | + let componentFound = false; |
| 116 | + |
| 117 | + // Check if node is a stateful setState component |
| 118 | + if (stateNode && stateNode.state && (tag === 0 || tag === 1)) { |
| 119 | + console.log('in create tree if'); |
| 120 | + console.log('this is currentFiber from createTree', currentFiber); |
| 121 | + // Save component's state and setState() function to our record for future |
| 122 | + // time-travel state changing. Add record index to snapshot so we can retrieve. |
| 123 | + componentData.index = componentActionsRecord.saveNew(stateNode.state, stateNode); |
| 124 | + newState.state = stateNode.state; |
| 125 | + componentFound = true; |
| 126 | + } |
| 127 | + |
| 128 | + // Check if node is a hooks useState function |
| 129 | + let hooksIndex; |
| 130 | + if (memoizedState && (tag === 0 || tag === 1 || tag === 10)) { |
| 131 | + console.log('in create tree if'); |
| 132 | + console.log('this is currentFiber from createTree', currentFiber); |
| 133 | + if (memoizedState.queue) { |
| 134 | + // Hooks states are stored as a linked list using memoizedState.next, |
| 135 | + // so we must traverse through the list and get the states. |
| 136 | + // We then store them along with the corresponding memoizedState.queue, |
| 137 | + // which includes the dispatch() function we use to change their state. |
| 138 | + const hooksStates = traverseHooks(memoizedState); |
| 139 | + hooksStates.forEach(state => { |
| 140 | + hooksIndex = componentActionsRecord.saveNew(state.state, state.component); |
| 141 | + if (newState && newState.hooksState) { |
| 142 | + newState.hooksState.push([state.state, hooksIndex]); |
| 143 | + } else if (newState) { |
| 144 | + newState.hooksState = [[state.state, hooksIndex]]; |
| 145 | + } else { |
| 146 | + newState = { hooksState: [[state.state, hooksIndex]] }; |
| 147 | + } |
| 148 | + componentFound = true; |
| 149 | + }); |
| 150 | + } |
| 151 | + } else { |
| 152 | + console.log('in create tree else'); |
| 153 | + console.log('this is currentFiber from createTree', currentFiber); |
| 154 | + console.log('this is memoizedState from createTree', memoizedState); |
| 155 | + // grab stateless components here |
| 156 | + if (elementType) { |
| 157 | + if (elementType.name) { |
| 158 | + tree.appendChild('stateless', elementType.name, index); |
| 159 | + } else { |
| 160 | + tree.appendChild('stateless', elementType, index); |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + // This grabs stateless components |
| 166 | + if (!componentFound && (tag === 0 || tag === 1)) { |
| 167 | + newState = 'stateless'; |
| 168 | + } |
| 169 | + |
| 170 | + // Adds performance metrics to the component data |
| 171 | + componentData = { |
| 172 | + ...componentData, |
| 173 | + actualDuration, |
| 174 | + actualStartTime, |
| 175 | + selfBaseDuration, |
| 176 | + treeBaseDuration, |
| 177 | + }; |
| 178 | + |
| 179 | + if (componentFound) { |
| 180 | + tree.addChild(newState, elementType.name ? elementType.name : elementType, componentData); |
| 181 | + } else if (newState === 'stateless') { |
| 182 | + tree.addChild(newState, elementType.name ? elementType.name : elementType, componentData); |
| 183 | + } |
| 184 | + |
| 185 | + // Recurse on children |
| 186 | + if (child) { |
| 187 | + // If this node had state we appended to the children array, |
| 188 | + // so attach children to the newly appended child. |
| 189 | + // Otherwise, attach children to this same node. |
| 190 | + if (tree.children.length > 0) { |
| 191 | + createTree(child, tree.children[tree.children.length - 1]); |
| 192 | + } else { |
| 193 | + createTree(child, tree); |
| 194 | + } |
| 195 | + } |
| 196 | + // Recurse on siblings |
| 197 | + if (sibling) createTree(sibling, tree); |
| 198 | + |
| 199 | + return tree; |
| 200 | + } |
| 201 | + |
| 202 | + // ! BUG: skips 1st hook click |
| 203 | + function updateSnapShotTree() { |
| 204 | + /* let current; |
| 205 | + // If concurrent mode, grab current.child |
| 206 | + if (concurrent) { |
| 207 | + // we need a way to wait for current child to populate |
| 208 | + const promise = new Promise((resolve, reject) => { |
| 209 | + setTimeout(() => resolve(fiberRoot.current.child), 400); |
| 210 | + }); |
| 211 | + current = await promise; |
| 212 | + current = fiberRoot.current.child; |
| 213 | + } else { |
| 214 | + current = fiberRoot.current; |
| 215 | + } */ |
| 216 | + const { current } = fiberRoot; // Carlos: get rid of concurrent mode for now |
| 217 | + |
| 218 | + // console.log('FIBER COMMITTED, new fiber is:', util.inspect(current, false, 4)); |
| 219 | + // fs.appendFile('fiberlog.txt', util.inspect(current, false, 10)); |
| 220 | + snap.tree = createTree(current); // Carlos: pass new hooks state here? |
| 221 | + } |
| 222 | + |
| 223 | + return async container => { |
| 224 | + // Point fiberRoot to FiberRootNode |
| 225 | + if (container._internalRoot) { |
| 226 | + fiberRoot = container._internalRoot; |
| 227 | + } else { |
| 228 | + const { |
| 229 | + _reactRootContainer: { _internalRoot }, |
| 230 | + _reactRootContainer, |
| 231 | + } = container; |
| 232 | + // Only assign internal root if it actually exists |
| 233 | + fiberRoot = _internalRoot || _reactRootContainer; |
| 234 | + } |
| 235 | + const devTools = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; |
| 236 | + console.log('this is devTools', devTools); |
| 237 | + const reactInstance = devTools ? devTools.renderers.get(1) : null; |
| 238 | + |
| 239 | + if (reactInstance && reactInstance.version) { |
| 240 | + devTools.onCommitFiberRoot = (function (original) { |
| 241 | + return function (...args) { |
| 242 | + fiberRoot = args[1]; |
| 243 | + console.log('this is fiberRoot', fiberRoot); |
| 244 | + updateSnapShotTree(); |
| 245 | + sendSnapshot(); |
| 246 | + return original(...args); |
| 247 | + }; |
| 248 | + }(devTools.onCommitFiberRoot)); |
| 249 | + } |
| 250 | + updateSnapShotTree(); |
| 251 | + // Send the initial snapshot once the content script has started up |
| 252 | + // This message is sent from contentScript.js in chrome extension bundles |
| 253 | + window.addEventListener('message', ({ data: { action } }) => { |
| 254 | + if (action === 'contentScriptStarted') { |
| 255 | + sendSnapshot(); |
| 256 | + } |
| 257 | + }); |
| 258 | + }; |
| 259 | +}; |
0 commit comments