From 564107456fb72456e8995cc1ad185e8a71151b52 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 4 Dec 2024 10:06:53 -0800 Subject: [PATCH 1/2] added overrideReducer function to intercept useReducer hooks --- .../ComponentMap/ToolTipDataDisplay.tsx | 1 - src/backend/types/backendTypes.ts | 20 ++ src/extension/build/devtools.html | 2 +- src/extension/build/manifest.json | 11 +- src/extension/overrideReducer.js | 257 ++++++++++++++++++ webpack.config.js | 1 + 6 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 src/extension/overrideReducer.js diff --git a/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx b/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx index 71309bb4c..ce961e1f5 100644 --- a/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx +++ b/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx @@ -37,7 +37,6 @@ const ToolTipDataDisplay = ({ containerName, dataObj }) => { return reducerStates.reduce((acc, reducer) => { acc[reducer.hookName || 'Reducer'] = { state: reducer.state, - lastAction: reducer.lastAction, }; return acc; }, {}); diff --git a/src/backend/types/backendTypes.ts b/src/backend/types/backendTypes.ts index 863a4d38d..ea18224ac 100644 --- a/src/backend/types/backendTypes.ts +++ b/src/backend/types/backendTypes.ts @@ -252,3 +252,23 @@ export type Fiber = { export type FiberRoot = { current: Fiber; }; + +export interface Window { + __REACTIME_TIME_TRAVEL__: { + getReducerState: (reducerId: symbol) => any; + travelToState: (reducerId: symbol, targetState: any) => void; + }; + __REACTIME_REDUCER_MAP__: Map< + symbol, + { + actionHistory: any[]; + dispatch: (action: any) => void; + initialState: any; + currentState: any; + reducer: (state: any, action: any) => any; + } + >; + __REACTIME_DEBUG__?: { + checkOverride: () => void; + }; +} diff --git a/src/extension/build/devtools.html b/src/extension/build/devtools.html index bc851747d..f3df4e272 100644 --- a/src/extension/build/devtools.html +++ b/src/extension/build/devtools.html @@ -4,7 +4,7 @@ - Reactime v23 + Reactime v26 diff --git a/src/extension/build/manifest.json b/src/extension/build/manifest.json index d0fbf3db8..eea78aab5 100644 --- a/src/extension/build/manifest.json +++ b/src/extension/build/manifest.json @@ -1,6 +1,6 @@ { "name": "Reactime", - "version": "25.0.0", + "version": "26.0.0", "devtools_page": "devtools.html", "description": "A Chrome extension that helps debug React applications by memorizing the state of components with every render.", "manifest_version": 3, @@ -13,6 +13,13 @@ "128": "assets/whiteBlackSquareIcon128.png" }, "content_scripts": [ + { + "matches": [""], + "js": ["bundles/overrideReducer.bundle.js"], + "run_at": "document_start", + "all_frames": true, + "world": "MAIN" + }, { "matches": ["http://localhost/*"], "js": ["bundles/content.bundle.js"] @@ -20,7 +27,7 @@ ], "web_accessible_resources": [ { - "resources": ["bundles/backend.bundle.js"], + "resources": ["bundles/backend.bundle.js", "bundles/overrideReducer.bundle.js"], "matches": [""] } ], diff --git a/src/extension/overrideReducer.js b/src/extension/overrideReducer.js new file mode 100644 index 000000000..5b3005264 --- /dev/null +++ b/src/extension/overrideReducer.js @@ -0,0 +1,257 @@ +// (function () { +// console.log('[Reactime Debug] Initial override script loaded'); + +// let attempts = 0; +// const MAX_ATTEMPTS = 50; + +// function verifyReactHooks() { +// return ( +// window.React && +// typeof window.React.useReducer === 'function' && +// typeof window.React.useState === 'function' +// ); +// } + +// const checkReact = () => { +// attempts++; +// console.log(`[Reactime Debug] Checking for React (attempt ${attempts})`); + +// // Only proceed if we can verify React hooks exist +// if (verifyReactHooks()) { +// console.log('[Reactime Debug] Found React with hooks via window.React'); +// setupOverride(); +// return; +// } + +// // Look for React devtools hook +// if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) { +// console.log('[Reactime Debug] Found React DevTools hook'); + +// // Watch for React registration +// const originalInject = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; +// console.log('[Reactime Debug] Original inject method:', originalInject); //ellie + +// window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function (reactInstance) { +// console.log( +// '[Reactime Debug] React registered with DevTools, verifying hooks availability', +// ); + +// // Give React a moment to fully initialize +// setTimeout(() => { +// if (verifyReactHooks()) { +// console.log('[Reactime Debug] Hooks verified after DevTools registration'); +// setupOverride(); +// } else { +// console.log( +// '[Reactime Debug] Hooks not available after DevTools registration, continuing to check', +// ); +// waitForHooks(); +// } +// }, 2000); + +// return originalInject.apply(this, arguments); +// }; + +// return; +// } + +// waitForHooks(); +// }; + +// function waitForHooks() { +// if (attempts < MAX_ATTEMPTS) { +// const delay = Math.min(100 * attempts, 2000); +// console.log(`[Reactime Debug] Hooks not found, retrying in ${delay}ms`); +// setTimeout(checkReact, delay); +// } else { +// console.log('[Reactime Debug] Max attempts reached, React hooks not found'); +// } +// } + +// function setupOverride() { +// try { +// console.log('[Reactime Debug] Setting up useReducer override'); + +// if (!verifyReactHooks()) { +// throw new Error('React hooks not available during override setup'); +// } + +// const originalUseReducer = window.React.useReducer; +// window.__REACTIME_REDUCER_MAP__ = new Map(); + +// window.React.useReducer = function (reducer, initialArg, init) { +// console.log('[Reactime Debug] useReducer called with:', { +// reducerName: reducer?.name || 'anonymous', +// hasInitialArg: initialArg !== undefined, +// hasInit: !!init, +// }); + +// const actualInitialState = init ? init(initialArg) : initialArg; + +// const wrappedReducer = (state, action) => { +// try { +// console.log('[Reactime Debug] Reducer called:', { +// actionType: action?.type, +// isTimeTravel: action?.type === '__REACTIME_TIME_TRAVEL__', +// currentState: state, +// action, +// }); + +// if (action && action.type === '__REACTIME_TIME_TRAVEL__') { +// return action.payload; +// } +// return reducer(state, action); +// } catch (error) { +// console.error('[Reactime Debug] Error in wrapped reducer:', error); +// return state; +// } +// }; + +// const [state, dispatch] = originalUseReducer(wrappedReducer, actualInitialState); +// const reducerId = Symbol('reactimeReducer'); + +// console.log('[Reactime Debug] New reducer instance created:', { +// reducerId: reducerId.toString(), +// initialState: actualInitialState, +// currentState: state, +// }); + +// window.__REACTIME_REDUCER_MAP__.set(reducerId, { +// actionHistory: [], +// dispatch, +// initialState: actualInitialState, +// currentState: state, +// reducer: wrappedReducer, +// }); + +// const wrappedDispatch = (action) => { +// try { +// console.log('[Reactime Debug] Dispatch called:', { +// reducerId: reducerId.toString(), +// action, +// currentMapSize: window.__REACTIME_REDUCER_MAP__.size, +// }); + +// const reducerInfo = window.__REACTIME_REDUCER_MAP__.get(reducerId); +// reducerInfo.actionHistory.push(action); +// reducerInfo.currentState = wrappedReducer(reducerInfo.currentState, action); +// dispatch(action); +// } catch (error) { +// console.error('[Reactime Debug] Error in wrapped dispatch:', error); +// dispatch(action); +// } +// }; + +// return [state, wrappedDispatch]; +// }; + +// console.log('[Reactime Debug] useReducer successfully overridden'); +// } catch (error) { +// console.error('[Reactime Debug] Error during override setup:', error); +// // If override fails, try again after a delay +// setTimeout(checkReact, 500); +// } +// } + +// // Start checking for React +// checkReact(); + +// // Watch for dynamic React loading +// const observer = new MutationObserver((mutations) => { +// if (verifyReactHooks()) { +// console.log('[Reactime Debug] React hooks found after DOM mutation'); +// observer.disconnect(); +// setupOverride(); +// } +// }); + +// observer.observe(document, { +// childList: true, +// subtree: true, +// }); +// })(); + + +(function () { + console.log('[Reactime Debug] Initial override script loaded'); + + // Retry constants + const MAX_ATTEMPTS = 50; + let attempts = 0; + + // Verify React hooks via registered renderers + function verifyReactHooks() { + try { + const renderers = Array.from( + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values() + ); + return renderers.some((renderer) => renderer?.currentDispatcher?.useReducer); + } catch (err) { + console.error('[Reactime Debug] Error verifying React hooks:', err); + return false; + } + } + + // Set up the useReducer override + function setupOverride() { + console.log('[Reactime Debug] Setting up useReducer override'); + + try { + const renderers = Array.from( + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values() + ); + const renderer = renderers[0]; // Assume first renderer for simplicity + const originalUseReducer = renderer?.currentDispatcher?.useReducer; + + if (!originalUseReducer) { + throw new Error('useReducer not found in React renderer.'); + } + + renderer.currentDispatcher.useReducer = function (reducer, initialArg, init) { + console.log('[Reactime Debug] useReducer intercepted:', reducer.name || 'anonymous'); + + const wrappedReducer = (state, action) => { + console.log('[Reactime Debug] Reducer called:', { state, action }); + return reducer(state, action); + }; + + return originalUseReducer(wrappedReducer, initialArg, init); + }; + + console.log('[Reactime Debug] useReducer successfully overridden.'); + } catch (err) { + console.error('[Reactime Debug] Error in setupOverride:', err); + } + } + + // Attempt to detect React and set up override + function checkReact() { + attempts++; + console.log(`[Reactime Debug] Checking for React (attempt ${attempts}/${MAX_ATTEMPTS})`); + + if (verifyReactHooks()) { + console.log('[Reactime Debug] React hooks found. Setting up overrides.'); + setupOverride(); + } else if (attempts < MAX_ATTEMPTS) { + setTimeout(checkReact, Math.min(100 * attempts, 2000)); + } else { + console.log('[Reactime Debug] Max attempts reached. React hooks not found.'); + } + } + + // Hook into the inject method of React DevTools + if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) { + const originalInject = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; + + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function (renderer) { + console.log('[Reactime Debug] React renderer registered.'); + setupOverride(); // React is registered, so immediately set up overrides + return originalInject.apply(this, arguments); + }; + + console.log('[Reactime Debug] React DevTools hook overridden.'); + } else { + console.log('[Reactime Debug] React DevTools hook not found. Starting manual checks.'); + checkReact(); // Start retries if no DevTools hook + } +})(); diff --git a/webpack.config.js b/webpack.config.js index b64c21032..b455c200f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,6 +17,7 @@ module.exports = { background: './src/extension/background.js', content: './src/extension/contentScript.ts', backend: './src/backend/index.ts', + overrideReducer: './src/extension/overrideReducer.js', }, watchOptions: { aggregateTimeout: 1000, From 1efadc0334d085c7dafc4bd6ff467f0b8b9d89cd Mon Sep 17 00:00:00 2001 From: elliesimens Date: Mon, 9 Dec 2024 15:14:45 -0500 Subject: [PATCH 2/2] removed cartesian versus polar view due to polar view being nonfunctional --- src/app/FrontendTypes.ts | 3 -- src/app/components/StateRoute/AxMap/Ax.tsx | 54 ++++++------------- .../StateRoute/AxMap/axLinkControls.tsx | 32 ++++------- .../StateRoute/AxMap/getAxLinkComponents.tsx | 14 +---- .../StateRoute/ComponentMap/ComponentMap.tsx | 41 ++++---------- .../StateRoute/ComponentMap/LinkControls.tsx | 22 ++------ .../ComponentMap/getLinkComponent.ts | 18 ++----- 7 files changed, 43 insertions(+), 141 deletions(-) diff --git a/src/app/FrontendTypes.ts b/src/app/FrontendTypes.ts index b32f0d068..91bbb5389 100644 --- a/src/app/FrontendTypes.ts +++ b/src/app/FrontendTypes.ts @@ -297,12 +297,10 @@ export interface StepsObj { } export interface LinkControlProps { - layout: string; orientation: string; linkType: string; stepPercent: number; selectedNode: string; - setLayout: (layout: string) => void; setOrientation: (orientation: string) => void; setLinkType: (linkType: string) => void; setStepPercent: (percent: number) => void; @@ -333,7 +331,6 @@ export interface Node { } export interface LinkComponent { - layout: string; linkType: string; orientation: string; } diff --git a/src/app/components/StateRoute/AxMap/Ax.tsx b/src/app/components/StateRoute/AxMap/Ax.tsx index 6093e8293..4d5fa499d 100644 --- a/src/app/components/StateRoute/AxMap/Ax.tsx +++ b/src/app/components/StateRoute/AxMap/Ax.tsx @@ -52,7 +52,7 @@ export default function AxTree(props) { showTooltip, // function to set tooltip state hideTooltip, // function to close a tooltip } = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component - + const { containerRef, // Access to the container's bounding box. This will be empty on first render. TooltipInPortal, // TooltipWithBounds in a Portal, outside of your component DOM tree @@ -75,7 +75,6 @@ export default function AxTree(props) { pointerEvents: 'all !important', }; - const [layout, setLayout] = useState('cartesian'); const [orientation, setOrientation] = useState('horizontal'); const [linkType, setLinkType] = useState('diagonal'); const [stepPercent, setStepPercent] = useState(0.5); @@ -87,32 +86,23 @@ export default function AxTree(props) { let sizeWidth: number; let sizeHeight: number; - if (layout === 'polar') { - origin = { - x: innerWidth / 2, - y: innerHeight / 2, - }; - sizeWidth = 2 * Math.PI; - sizeHeight = Math.min(innerWidth, innerHeight) / 2; + origin = { x: 0, y: 0 }; + if (orientation === 'vertical') { + sizeWidth = innerWidth; + sizeHeight = innerHeight; } else { - origin = { x: 0, y: 0 }; - if (orientation === 'vertical') { - sizeWidth = innerWidth; - sizeHeight = innerHeight; - } else { - sizeWidth = innerHeight; - sizeHeight = innerWidth; - } + sizeWidth = innerHeight; + sizeHeight = innerWidth; } - const LinkComponent = getLinkComponent({ layout, linkType, orientation }); + const LinkComponent = getLinkComponent({ linkType, orientation }); const currAxSnapshot = JSON.parse(JSON.stringify(axSnapshots[currLocation.index])); // root node of currAxSnapshot const rootAxNode = JSON.parse(JSON.stringify(currAxSnapshot[0])); - // array that holds each ax tree node with children property + // array that holds each ax tree node with children property const nodeAxArr = []; // populates ax nodes with children property; visx recognizes 'children' in order to properly render a nested tree @@ -164,11 +154,9 @@ export default function AxTree(props) {
- + { + onClick={() => { hideTooltip(); - }}/> + }} + /> (d.isExpanded ? null : d.children))} @@ -223,11 +212,7 @@ export default function AxTree(props) { let top: number; let left: number; - if (layout === 'polar') { - const [radialX, radialY] = pointRadial(node.x, node.y); - top = radialY; - left = radialX; - } else if (orientation === 'vertical') { + if (orientation === 'vertical') { top = node.y; left = node.x; } else { @@ -430,8 +415,8 @@ export default function AxTree(props) {
{/*tooltipData['name'].value cannot be referred to using dot notation so using brackets here overrides typescript's strict data typing which was interfering with accessiccing this property */} - {JSON.stringify(tooltipData['name'].value)} -
+ {JSON.stringify(tooltipData['name'].value)} +
{/* Ax Node Info below names the tooltip title because of how its passed to the ToolTipDataDisplay container*/} @@ -440,12 +425,7 @@ export default function AxTree(props) { )} -
- { axLegendButtonClicked ? - : '' - } -
- +
{axLegendButtonClicked ? : ''}
); } diff --git a/src/app/components/StateRoute/AxMap/axLinkControls.tsx b/src/app/components/StateRoute/AxMap/axLinkControls.tsx index 433936919..cb5d83db4 100644 --- a/src/app/components/StateRoute/AxMap/axLinkControls.tsx +++ b/src/app/components/StateRoute/AxMap/axLinkControls.tsx @@ -3,47 +3,33 @@ import React from 'react'; const controlStyles = { fontSize: 10 }; type Props = { - layout: string; orientation: string; linkType: string; stepPercent: number; - setLayout: (layout: string) => void; setOrientation: (orientation: string) => void; setLinkType: (linkType: string) => void; setStepPercent: (percent: number) => void; }; export default function LinkControls({ - layout, orientation, linkType, stepPercent, - setLayout, setOrientation, setLinkType, setStepPercent, }: Props) { return (
-   -           @@ -52,24 +38,24 @@ export default function LinkControls({ onChange={(e) => setLinkType(e.target.value)} value={linkType} > - - - - + + + + - {linkType === 'step' && layout !== 'polar' && ( + {linkType === 'step' && ( <>      e.stopPropagation()} - type="range" + type='range' min={0} max={1} step={0.1} onChange={(e) => setStepPercent(Number(e.target.value))} value={stepPercent} - disabled={linkType !== 'step' || layout === 'polar'} + disabled={linkType !== 'step'} /> )} diff --git a/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx b/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx index 33316542f..26ce0426f 100644 --- a/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx +++ b/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx @@ -15,27 +15,15 @@ import { } from '@visx/shape'; export default function getLinkComponent({ - layout, linkType, orientation, }: { - layout: string; linkType: string; orientation: string; }) { let LinkComponent; - if (layout === 'polar') { - if (linkType === 'step') { - LinkComponent = LinkRadialStep; - } else if (linkType === 'curve') { - LinkComponent = LinkRadialCurve; - } else if (linkType === 'line') { - LinkComponent = LinkRadialLine; - } else { - LinkComponent = LinkRadial; - } - } else if (orientation === 'vertical') { + if (orientation === 'vertical') { if (linkType === 'step') { LinkComponent = LinkVerticalStep; } else if (linkType === 'curve') { diff --git a/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx b/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx index c4e813e15..547142499 100644 --- a/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx +++ b/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx @@ -57,7 +57,6 @@ export default function ComponentMap({ margin = defaultMargin, currentSnapshot, // from 'tabs[currentTab].stateSnapshot object in 'MainContainer' }: LinkTypesProps): JSX.Element { - const [layout, setLayout] = useState('cartesian'); // We create a local state "layout" and set it to a string 'cartesian' const [orientation, setOrientation] = useState('vertical'); // We create a local state "orientation" and set it to a string 'vertical'. const [linkType, setLinkType] = useState('diagonal'); // We create a local state "linkType" and set it to a string 'diagonal'. const [stepPercent, setStepPercent] = useState(0.5); // We create a local state "stepPercent" and set it to a number '0.5'. This will be used to scale the Map component's link: Step to 50% @@ -80,33 +79,20 @@ export default function ComponentMap({ /* We begin setting the starting position for the root node on the maps display. - The 'polar layout' sets the root node to the relative center of the display box based on the size of the browser window. - The 'cartesian layout' (else conditional) sets the root nodes location either in the left middle *or top middle of the browser window relative to the size of the browser. + The default view sets the root nodes location either in the left middle *or top middle of the browser window relative to the size of the browser. */ - if (layout === 'polar') { - // 'polar layout' option - origin = { - x: innerWidth / 2, - y: innerHeight / 2, - }; - // set the sizeWidth and sizeHeight - sizeWidth = 2 * Math.PI; - sizeHeight = Math.min(innerWidth, innerHeight) / 2; + origin = { x: 0, y: 0 }; + if (orientation === 'vertical') { + sizeWidth = innerWidth; + sizeHeight = innerHeight; } else { - // 'cartesian layout' option - origin = { x: 0, y: 0 }; - if (orientation === 'vertical') { - sizeWidth = innerWidth; - sizeHeight = innerHeight; - } else { - // if the orientation isn't vertical, swap the width and the height - sizeWidth = innerHeight; - sizeHeight = innerWidth; - } + // if the orientation isn't vertical, swap the width and the height + sizeWidth = innerHeight; + sizeHeight = innerWidth; } - +//} const { tooltipData, // value/data that tooltip may need to render tooltipLeft, // number used for tooltip positioning @@ -268,20 +254,17 @@ export default function ComponentMap({ // controls for the map const LinkComponent: React.ComponentType = getLinkComponent({ - layout, linkType, orientation, }); return totalWidth < 10 ? null : (
{ }; export default function LinkControls({ - layout, // from the layout local state (initially 'cartesian') in 'ComponentMap' linkType, // from linkType local state (initially 'vertical') in 'ComponentMap' stepPercent, // from stepPercent local state (initially '0.5') in 'ComponentMap' - setLayout, // from the layout local state in 'ComponentMap' setOrientation, // from the orientation local state in 'ComponentMap' setLinkType, // from the linkType local state in 'ComponentMap' setStepPercent, // from the stepPercent local state in 'ComponentMap' @@ -51,29 +49,15 @@ export default function LinkControls({ return (
{' '} - {/* Controls for the layout selection */} - -  {' '} {/* This is a non-breaking space - Prevents an automatic line break at this position */} -    {' '} {/* Toggle record button to pause state changes on target application */} - {/* Controls for the Orientation selection, this dropdown will be disabled when the polar layout is selected as it is not needed */} + {/* Controls for the Orientation selection */}   {/* This is the slider control for the step option */} - {linkType === 'step' && layout !== 'polar' && ( + {linkType === 'step' && ( <>    @@ -122,7 +106,7 @@ export default function LinkControls({ step={0.1} onChange={(e) => setStepPercent(Number(e.target.value))} value={stepPercent} - disabled={linkType !== 'step' || layout === 'polar'} + disabled={linkType !== 'step'} /> )} diff --git a/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts b/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts index 979141275..600c884fa 100644 --- a/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts +++ b/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts @@ -15,29 +15,17 @@ import { import { LinkComponent } from '../../../FrontendTypes'; /* - Changes the shape of the LinkComponent based on the layout, linkType, and orientation + Changes the shape of the LinkComponent based on the linkType, and orientation */ export default function getLinkComponent({ - layout, linkType, orientation, }: LinkComponent): React.ComponentType { let LinkComponent: React.ComponentType; - if (layout === 'polar') { - // if the layout is polar, linkType can be either step, curve, line, or a plain LinkRadial. - if (linkType === 'step') { - LinkComponent = LinkRadialStep; - } else if (linkType === 'curve') { - LinkComponent = LinkRadialCurve; - } else if (linkType === 'line') { - LinkComponent = LinkRadialLine; - } else { - LinkComponent = LinkRadial; - } - } else if (orientation === 'vertical') { - // if the layout isn't polar and the orientation is vertical, linkType can be either step, curve, line, or a plain LinkVertical +if (orientation === 'vertical') { + // if the orientation is vertical, linkType can be either step, curve, line, or a plain LinkVertical if (linkType === 'step') { LinkComponent = LinkVerticalStep; } else if (linkType === 'curve') {