From 9f61e44d1c8435608e60eecd13519a5ac30c56cf Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 1 Dec 2024 12:25:49 -0800 Subject: [PATCH 001/194] hook parsing function works for webpack transformed, direct, and namespcae useState patterns --- demo-app/package.json | 2 + demo-app/src/client/Components/Buttons.tsx | 68 ++++++++-- demo-app/src/client/Components/Increment.tsx | 13 -- demo-app/src/client/Components/Nav.tsx | 5 +- .../src/client/Components/ReducerCounter.tsx | 112 ++++++++++++++++ demo-app/src/client/Router.tsx | 2 + demo-app/src/client/style.css | 99 ++++++++++---- demo-app/webpack.config.js | 34 ++++- package.json | 2 + src/backend/controllers/createTree.ts | 21 ++- .../controllers/statePropExtractors.ts | 124 +++++++++++------- 11 files changed, 373 insertions(+), 109 deletions(-) delete mode 100644 demo-app/src/client/Components/Increment.tsx create mode 100644 demo-app/src/client/Components/ReducerCounter.tsx diff --git a/demo-app/package.json b/demo-app/package.json index 17ed638c0..4003e82fa 100644 --- a/demo-app/package.json +++ b/demo-app/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@babel/core": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.16.7", "@babel/preset-react": "^7.16.7", "@types/express": "^4.17.13", @@ -17,6 +18,7 @@ "@types/react-dom": "^17.0.19", "babel-loader": "^8.2.3", "copy-webpack-plugin": "^10.2.0", + "core-js": "^3.39.0", "css-loader": "^6.5.1", "html-webpack-plugin": "^5.5.0", "node": "^16.0.0", diff --git a/demo-app/src/client/Components/Buttons.tsx b/demo-app/src/client/Components/Buttons.tsx index b27abfe24..66ffdc06d 100644 --- a/demo-app/src/client/Components/Buttons.tsx +++ b/demo-app/src/client/Components/Buttons.tsx @@ -1,22 +1,64 @@ -import React from 'react'; -import Increment from './Increment'; +import React, { Component, useState } from 'react'; -function Buttons(): JSX.Element { - const buttons = []; - for (let i = 0; i < 4; i++) { - buttons.push(); +// Type for IncrementClass state +type IncrementClassState = { + count: number; +}; + +// Class-based Increment Component +class IncrementClass extends Component<{}, IncrementClassState> { + state = { + count: 0, + }; + + handleClick = (): void => { + this.setState((prevState: IncrementClassState) => ({ + count: prevState.count + 1, + })); + }; + + render(): JSX.Element { + return ( +
+ +
+ ); } +} + +// Function-based Increment Component +const IncrementFunction = (): JSX.Element => { + const [count, setCount] = useState(0); + + const handleClick = (): void => { + setCount((prev) => prev + 1); + }; return ( -
-

Stateful Buttons

-

- These buttons are functional components that each manage their own state with the useState - hook. -

- {buttons} +
+
); +}; + +// Main Buttons Component +class Buttons extends Component { + render(): JSX.Element { + return ( +
+

Mixed State Counter

+

First two buttons use class components, last two use function components.

+ + + + +
+ ); + } } export default Buttons; diff --git a/demo-app/src/client/Components/Increment.tsx b/demo-app/src/client/Components/Increment.tsx deleted file mode 100644 index 3836152a7..000000000 --- a/demo-app/src/client/Components/Increment.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useState } from 'react'; -function Increment(): JSX.Element { - const [count, setCount] = useState(0); - return ( -
- -
- ); -} - -export default Increment; diff --git a/demo-app/src/client/Components/Nav.tsx b/demo-app/src/client/Components/Nav.tsx index c289f8d81..53e12d553 100644 --- a/demo-app/src/client/Components/Nav.tsx +++ b/demo-app/src/client/Components/Nav.tsx @@ -11,7 +11,10 @@ function Nav(): JSX.Element { Tic-Tac-Toe - Counter + State Counter + + + Reducer Counter
); diff --git a/demo-app/src/client/Components/ReducerCounter.tsx b/demo-app/src/client/Components/ReducerCounter.tsx new file mode 100644 index 000000000..c54b662e5 --- /dev/null +++ b/demo-app/src/client/Components/ReducerCounter.tsx @@ -0,0 +1,112 @@ +import React, { Component } from 'react'; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number }; + +// Static reducer function to handle state updates +class ReducerCounter extends Component<{}, CounterState> { + // Initial state definition + static initialState: CounterState = { + count: 0, + history: [], + lastAction: 'none', + }; + + // Static reducer method to handle state updates + static reducer(state: CounterState, action: CounterAction): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + 1, + history: [...state.history, state.count + 1], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - 1, + history: [...state.history, state.count - 1], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + ...ReducerCounter.initialState, + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + default: + return state; + } + } + + constructor(props: {}) { + super(props); + this.state = ReducerCounter.initialState; + + // Bind dispatch method + this.dispatch = this.dispatch.bind(this); + } + + // Method to handle state updates using the reducer + dispatch(action: CounterAction): void { + this.setState((currentState) => ReducerCounter.reducer(currentState, action)); + } + + render(): JSX.Element { + return ( +
+

Class-based Reducer Counter

+
+

Current Count: {this.state.count}

+
+ +
+ + + + + +
+ +
+

Last Action: {this.state.lastAction}

+

History:

+
+ {this.state.history.map((value, index) => ( + + {value} + {index < this.state.history.length - 1 ? ' → ' : ''} + + ))} +
+
+
+ ); + } +} + +export default ReducerCounter; diff --git a/demo-app/src/client/Router.tsx b/demo-app/src/client/Router.tsx index 47dc6e14b..1e51d9e6c 100644 --- a/demo-app/src/client/Router.tsx +++ b/demo-app/src/client/Router.tsx @@ -6,6 +6,7 @@ import Nav from './Components/Nav'; import Board from './Components/Board'; import Home from './Components/Home'; import Buttons from './Components/Buttons'; +import ReducerCounter from './Components/ReducerCounter'; // import ButtonsWithMoreHooks from './Components/ButtonsWithMoreHooks'; const domNode = document.getElementById('root'); @@ -21,6 +22,7 @@ root.render( the public facing Buttons page and the fiber node hooks research page "ButtonsWithMoreHooks" */} } /> {/* } /> */} + } /> , diff --git a/demo-app/src/client/style.css b/demo-app/src/client/style.css index a5c315ab6..f111f0c38 100644 --- a/demo-app/src/client/style.css +++ b/demo-app/src/client/style.css @@ -1,10 +1,10 @@ @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); :root { - --primary-red-color: #F00008; + --primary-red-color: #f00008; --background-color1: #f6f6f6; --secondary-blue-color: #62d6fb; - --fire-rose-red: #FF6569; + --fire-rose-red: #ff6569; --secondary-color: #6288fb; --text-color: #330002; } @@ -15,7 +15,9 @@ body { background-color: var(--background-color1); } -h1, h2, h4 { +h1, +h2, +h4 { text-align: center; } @@ -23,18 +25,18 @@ h1, h2, h4 { .nav { background-image: linear-gradient( - 0deg, - hsl(358deg 100% 47%) 5%, - hsl(359deg 90% 51%) 24%, - hsl(360deg 91% 54%) 33%, - hsl(360deg 93% 57%) 41%, - hsl(360deg 94% 59%) 48%, - hsl(360deg 95% 60%) 54%, - hsl(359deg 97% 62%) 61%, - hsl(359deg 98% 64%) 69%, - hsl(359deg 99% 65%) 79%, - hsl(358deg 100% 67%) 100% -); + 0deg, + hsl(358deg 100% 47%) 5%, + hsl(359deg 90% 51%) 24%, + hsl(360deg 91% 54%) 33%, + hsl(360deg 93% 57%) 41%, + hsl(360deg 94% 59%) 48%, + hsl(360deg 95% 60%) 54%, + hsl(359deg 97% 62%) 61%, + hsl(359deg 98% 64%) 69%, + hsl(359deg 99% 65%) 79%, + hsl(358deg 100% 67%) 100% + ); display: flex; justify-content: space-evenly; @@ -50,7 +52,7 @@ h1, h2, h4 { text-decoration: none; text-align: center; - color: #FFF4F4; + color: #fff4f4; } .link:hover { @@ -83,7 +85,7 @@ h1, h2, h4 { color: #330002; margin-top: 2em; - + padding-top: 1em; padding-bottom: 1em; padding-left: 4em; @@ -114,7 +116,7 @@ h1, h2, h4 { background-color: #ffffff; border-style: solid; - border-color: #FF6569; + border-color: #ff6569; border-radius: 5px; margin-top: 20px; @@ -122,12 +124,12 @@ h1, h2, h4 { width: 100%; - padding: .5em; + padding: 0.5em; } #reset:hover { color: #ffffff; - background-color: var(--primary-red-color) + background-color: var(--primary-red-color); } /* Counter */ @@ -163,7 +165,7 @@ h1, h2, h4 { width: 100%; - padding: .5em; + padding: 0.5em; } .increment:hover { @@ -187,4 +189,57 @@ h1, h2, h4 { .hook-data-section p { text-align: center; margin: 8px; -} \ No newline at end of file +} +/* Add these styles to your style.css file */ + +.reducer-counter { + background-color: #ffffff; + color: #330002; + padding: 2em; + margin-top: 2em; + max-width: 500px; + margin-left: auto; + margin-right: auto; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; +} + +.counter-value { + text-align: center; + font-size: 1.2em; + margin: 1em 0; +} + +.counter-buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + margin: 1em 0; +} + +.counter-buttons button { + color: #ffffff; + font-size: 1em; + background-color: var(--primary-red-color); + border: 2px solid #ffffff; + border-radius: 5px; + padding: 0.5em 1em; + cursor: pointer; +} + +.counter-buttons button:hover { + background-color: var(--secondary-blue-color); +} + +.counter-info { + text-align: center; + margin-top: 2em; +} + +.history-list { + margin: 1em 0; + padding: 1em; + background-color: var(--background-color1); + border-radius: 5px; + word-wrap: break-word; +} diff --git a/demo-app/webpack.config.js b/demo-app/webpack.config.js index 84e00598d..8dae226ab 100644 --- a/demo-app/webpack.config.js +++ b/demo-app/webpack.config.js @@ -17,13 +17,43 @@ module.exports = { use: { loader: 'babel-loader', options: { - presets: ['@babel/preset-env', '@babel/preset-react'], + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + browsers: ['last 2 versions', 'not dead', 'not < 2%', 'not ie 11'], + }, + useBuiltIns: 'usage', + corejs: 3, + }, + ], + [ + '@babel/preset-react', + { + runtime: 'automatic', + }, + ], + ], + plugins: ['@babel/plugin-transform-runtime'], }, }, }, { test: /\.tsx?$/, - use: 'ts-loader', + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + compilerOptions: { + target: 'es2018', + module: 'esnext', + }, + }, + }, + ], exclude: /node_modules/, }, ], diff --git a/package.json b/package.json index 6a006e972..606c1417b 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,8 @@ ], "license": "ISC", "devDependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", diff --git a/src/backend/controllers/createTree.ts b/src/backend/controllers/createTree.ts index 86f6aee4c..940fd619d 100644 --- a/src/backend/controllers/createTree.ts +++ b/src/backend/controllers/createTree.ts @@ -217,13 +217,21 @@ export default function createTree(currentFiberNode: Fiber): Tree { tag === ContextProvider) && memoizedState ) { + if (memoizedState) { + console.log('memoizedState structure:', { + queue: memoizedState.queue, + state: memoizedState.memoizedState, + next: memoizedState.next, + }); + } if (memoizedState.queue) { try { // Obtain all hooksStates & the corresponding udpate method from memoizedState const hooksStates = getHooksStateAndUpdateMethod(memoizedState); // Obtain variable names by parsing the function definition stored in elementType. + console.log('Component definition:', elementType.toString()); const hooksNames = getHooksNames(elementType.toString()); - + console.log('Extracted hook names:', hooksNames); // Intialize state & index: componentData.hooksState = {}; componentData.hooksIndex = []; @@ -237,12 +245,11 @@ export default function createTree(currentFiberNode: Fiber): Tree { // Pass to front end newState = componentData.hooksState; } catch (err) { - // COMMENT OUT TO AVOID PRINTING ON THE CONSOLE OF USER - KEEP IT FOR DEBUGGING PURPOSE - // console.log({ - // Message: 'Error in createTree during obtaining state from functionalComponent', - // componentName, - // err, - // }); + console.log('Error extracting functional component state:', { + componentName, + memoizedState, + error: err, + }); } } } diff --git a/src/backend/controllers/statePropExtractors.ts b/src/backend/controllers/statePropExtractors.ts index c01adfe21..2056b102e 100644 --- a/src/backend/controllers/statePropExtractors.ts +++ b/src/backend/controllers/statePropExtractors.ts @@ -1,4 +1,5 @@ -import parse from 'html-react-parser'; +import { parse } from '@babel/parser'; +import { Node, CallExpression, MemberExpression, Identifier } from '@babel/types'; import { HookStateItem, Fiber } from '../types/backendTypes'; import { exclude } from '../models/filterConditions'; @@ -86,62 +87,83 @@ export function getHooksStateAndUpdateMethod( * @returns - An array of objects with key: hookName (the name of setState method) | value: varName (the state variable name) */ export function getHooksNames(elementType: string): { hookName: string; varName: string }[] { - // Initialize empty object to store the setters and getter - // Abstract Syntax Tree - let AST: any; try { - AST = parse(elementType).body; - // Begin search for hook names, only if ast has a body property. - // Statements get all the names of the hooks. For example: useCount, useWildcard, ... + const AST = parse(elementType, { + sourceType: 'module', + plugins: ['jsx', 'typescript'], + }); + const statements: { hookName: string; varName: string }[] = []; - /** All module exports always start off as a single 'FunctionDeclaration' type - * Other types: "BlockStatement" / "ExpressionStatement" / "ReturnStatement" - * Iterate through AST of every functional component declaration - * Check within each functional component declaration if there are hook declarations & variable name declaration */ - AST.forEach((functionDec: any) => { - let declarationBody: any; - if (functionDec.expression?.body) declarationBody = functionDec.expression.body.body; - // check if functionDec.expression.body exists, then set declarationBody to functionDec's body - else declarationBody = functionDec.body?.body ?? []; - // Traverse through the function's funcDecs and Expression Statements - declarationBody.forEach((elem: any) => { - // Hooks will always be contained in a variable declaration - if (elem.type === 'VariableDeclaration') { - // Obtain the declarations array from elem. - const { declarations } = elem; - // Obtain the reactHook: - // Due to difference in babel transpilation in browser vs for jest test, expression is stored in differen location - const expression = - declarations[0]?.init?.callee?.expressions || //work for browser - declarations[0]?.init?.arguments?.[0]?.callee?.expressions; //work for jest test; - //For a functional definition that isn't a hook, it won't have the callee being searched for above. This line will cause this forEach execution to stop here in this case. - if (expression === undefined) return; - let reactHook: string; - reactHook = expression[1].property?.name; - if (reactHook === 'useState') { - // Obtain the variable being set: - let varName: string = - // Points to second to last element of declarations because webpack adds an extra variable when converting files that use ES6 - declarations[declarations.length - 2]?.id?.name || // work react application; - (Array.isArray(declarations[0]?.id?.elements) - ? declarations[0]?.id?.elements[0]?.name - : undefined); //work for nextJS application - // Obtain the setState method: - let hookName: string = - //Points to last element of declarations because webpack adds an extra variable when converting files that use ES6 - declarations[declarations.length - 1]?.id?.name || // work react application; - (Array.isArray(declarations[0]?.id?.elements) - ? declarations[0]?.id?.elements[0]?.name - : undefined); //work for nextJS & Remix - // Push reactHook & varName to statements array - statements.push({ hookName, varName }); + const isIdentifierWithName = (node: any, name: string): boolean => { + return node?.type === 'Identifier' && node.name === name; + }; + + const processArrayPattern = (pattern: any): { setter: string; getter: string } | null => { + if (pattern.type === 'ArrayPattern' && pattern.elements.length === 2) { + const result = { + getter: pattern.elements[0].name, + setter: pattern.elements[1].name, + }; + return result; + } + return null; + }; + + function traverse(node: Node) { + if (!node) return; + + if (node.type === 'VariableDeclaration') { + node.declarations.forEach((declaration) => { + if (declaration.init?.type === 'CallExpression') { + // Check for Webpack transformed pattern: (0, react__WEBPACK_IMPORTED_MODULE_0__.useState)(0) + const isWebpackPattern = + declaration.init.callee?.type === 'SequenceExpression' && + declaration.init.callee.expressions?.length === 2 && + declaration.init.callee.expressions[1]?.type === 'MemberExpression' && + declaration.init.callee.expressions[1].property && + isIdentifierWithName(declaration.init.callee.expressions[1].property, 'useState'); + + // Check for direct useState pattern: useState("test") + const isDirectPattern = + declaration.init.callee?.type === 'Identifier' && + declaration.init.callee.name === 'useState'; + + // Check for namespaced useState pattern: React.useState("test") + const isNamespacedPattern = + declaration.init.callee?.type === 'MemberExpression' && + declaration.init.callee.property && + isIdentifierWithName(declaration.init.callee.property, 'useState'); + + if (isWebpackPattern || isDirectPattern || isNamespacedPattern) { + const arrayPattern = processArrayPattern(declaration.id); + if (arrayPattern) { + statements.push({ + hookName: arrayPattern.setter, + varName: arrayPattern.getter, + }); + } + } + } + }); + } + + // Recursively traverse + for (const key in node) { + if (node[key] && typeof node[key] === 'object') { + if (Array.isArray(node[key])) { + node[key].forEach((child: Node) => traverse(child)); + } else { + traverse(node[key] as Node); } } - }); - }); + } + } + + traverse(AST); return statements; } catch (err) { - throw new Error('getHooksNameError' + err.message); + console.error('AST Parsing Error:', err); + throw new Error('getHooksNameError: ' + err.message); } } From c29ac21c1201a8836097cc7ce11b83ac674754d3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 1 Dec 2024 16:14:21 -0800 Subject: [PATCH 002/194] added function based reducer counter in demo app --- .../Components/FunctionalReducerCounter.tsx | 96 +++++++++++++++++++ demo-app/src/client/Router.tsx | 10 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 demo-app/src/client/Components/FunctionalReducerCounter.tsx diff --git a/demo-app/src/client/Components/FunctionalReducerCounter.tsx b/demo-app/src/client/Components/FunctionalReducerCounter.tsx new file mode 100644 index 000000000..3c00ac7d2 --- /dev/null +++ b/demo-app/src/client/Components/FunctionalReducerCounter.tsx @@ -0,0 +1,96 @@ +import React, { useReducer } from 'react'; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number }; + +const initialState: CounterState = { + count: 0, + history: [], + lastAction: 'none', +}; + +function counterReducer(state: CounterState, action: CounterAction): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + 1, + history: [...state.history, state.count + 1], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - 1, + history: [...state.history, state.count - 1], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + ...initialState, + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + default: + return state; + } +} + +function FunctionalReducerCounter(): JSX.Element { + const [state, dispatch] = useReducer(counterReducer, initialState); + + return ( +
+

Function-based Reducer Counter

+
+

Current Count: {state.count}

+
+ +
+ + + + + +
+ +
+

Last Action: {state.lastAction}

+

History:

+
+ {state.history.map((value, index) => ( + + {value} + {index < state.history.length - 1 ? ' → ' : ''} + + ))} +
+
+
+ ); +} + +export default FunctionalReducerCounter; diff --git a/demo-app/src/client/Router.tsx b/demo-app/src/client/Router.tsx index 1e51d9e6c..d01a8ccd4 100644 --- a/demo-app/src/client/Router.tsx +++ b/demo-app/src/client/Router.tsx @@ -7,11 +7,19 @@ import Board from './Components/Board'; import Home from './Components/Home'; import Buttons from './Components/Buttons'; import ReducerCounter from './Components/ReducerCounter'; +import FunctionalReducerCounter from './Components/FunctionalReducerCounter'; // import ButtonsWithMoreHooks from './Components/ButtonsWithMoreHooks'; const domNode = document.getElementById('root'); const root = createRoot(domNode); +const CounterPage = () => ( +
+ + +
+); + root.render(