├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── gh-pages.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── .gitignore └── .npmignore ├── examples ├── demo-app │ ├── package-lock.json │ ├── package.json │ ├── server.js │ └── src │ │ ├── index.html │ │ └── index.jsx ├── next │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ └── _document.tsx │ └── tsconfig.json ├── playwright │ ├── package-lock.json │ ├── package.json │ └── script.mjs └── puppeteer │ ├── package-lock.json │ ├── package.json │ └── script.js ├── jest.config.js ├── package-lock.json ├── package.json ├── playground ├── cases │ ├── app.tsx │ ├── bailouts.tsx │ ├── basic-nested-set-state.tsx │ ├── basic-parent-element-change.tsx │ ├── class-component.tsx │ ├── complex-composition-on-one-component.tsx │ ├── context.tsx │ ├── hooks.tsx │ ├── index.ts │ ├── mount-unmount.tsx │ ├── props-changes.tsx │ ├── screenshot-demo.tsx │ ├── set-state-by-event-handler.tsx │ ├── suspense.tsx │ └── use-effects.tsx ├── create-test-case-wrapper.ts ├── data-client-example.js ├── dom-utils.ts ├── index.css ├── index.html ├── index.tsx ├── react-dom.tsx ├── react.tsx └── types.ts ├── scripts ├── build-gh-pages.js ├── build.js └── server.js ├── src ├── common │ ├── constants.ts │ ├── consumer-types.ts │ ├── rempl.d.ts │ └── types.d.ts ├── data-client │ ├── headless-browser.ts │ └── index.ts ├── data │ ├── fiber-dataset.ts │ ├── index.ts │ ├── process-events.ts │ ├── subscription.ts │ ├── tree.test.ts │ └── tree.ts ├── publisher │ ├── config.ts │ ├── index.ts │ ├── overlay.ts │ ├── react-devtools-hook.ts │ ├── react-integration │ │ ├── core.ts │ │ ├── devtools-hook-handlers.ts │ │ ├── dispatcher-trap.ts │ │ ├── highlight-api.ts │ │ ├── index.ts │ │ ├── interaction-api.ts │ │ ├── unmounted-fiber-leak-detector.ts │ │ └── utils │ │ │ ├── arrayDiff.ts │ │ │ ├── constants.ts │ │ │ ├── getDisplayName.ts │ │ │ ├── getDisplayNameFromJsx.ts │ │ │ ├── getFiberFlags.ts │ │ │ ├── getInternalReactConstants.ts │ │ │ ├── isPlainObject.ts │ │ │ ├── objectDiff.ts │ │ │ ├── separateDisplayNameAndHOCs.ts │ │ │ ├── simpleValueSerialization.ts │ │ │ └── stackTrace.ts │ ├── rempl-publisher.ts │ ├── types.ts │ └── utils │ │ ├── renderer-info.ts │ │ └── resolveSourceLoc.ts └── ui │ ├── App.css │ ├── App.tsx │ ├── components │ ├── appbar │ │ ├── AppBar.css │ │ ├── AppBar.tsx │ │ ├── Renderer.css │ │ └── Renderer.tsx │ ├── common │ │ ├── ButtonToggle.css │ │ ├── ButtonToggle.tsx │ │ ├── FiberHocNames.css │ │ ├── FiberHocNames.tsx │ │ ├── FiberId.css │ │ ├── FiberId.tsx │ │ ├── FiberKey.css │ │ ├── FiberKey.tsx │ │ ├── FiberMaybeLeak.css │ │ ├── FiberMaybeLeak.tsx │ │ ├── SourceLoc.css │ │ ├── SourceLoc.tsx │ │ └── icons.tsx │ ├── details │ │ ├── CallStack.css │ │ ├── CallStack.tsx │ │ ├── Details.css │ │ ├── Details.tsx │ │ ├── EventChangesSummary.css │ │ ├── EventChangesSummary.tsx │ │ ├── Fiber.css │ │ ├── Fiber.tsx │ │ ├── FiberHeader.css │ │ ├── FiberHeader.tsx │ │ ├── FiberLink.css │ │ ├── FiberLink.tsx │ │ ├── diff │ │ │ ├── Diff.css │ │ │ ├── Diff.tsx │ │ │ ├── DiffArray.tsx │ │ │ ├── DiffObject.tsx │ │ │ ├── DiffSimple.tsx │ │ │ ├── ShallowEqual.css │ │ │ └── ShallowEqual.tsx │ │ ├── event-list │ │ │ ├── EventList.css │ │ │ ├── EventList.tsx │ │ │ ├── EventListCommitEvent.css │ │ │ ├── EventListCommitEvent.tsx │ │ │ ├── EventListEntry.css │ │ │ ├── EventListEntry.tsx │ │ │ ├── EventListFiberEvent.css │ │ │ ├── EventListFiberEvent.tsx │ │ │ ├── EventRenderReasons.css │ │ │ ├── EventRenderReasons.tsx │ │ │ ├── EventRenderReasonsItem.css │ │ │ └── EventRenderReasonsItem.tsx │ │ └── info │ │ │ ├── ChangesMatrix.css │ │ │ ├── ChangesMatrix.tsx │ │ │ ├── FiberInfo.css │ │ │ ├── FiberInfo.tsx │ │ │ ├── FiberInfoSection.css │ │ │ ├── FiberInfoSection.tsx │ │ │ ├── FiberInfoSectionAncestors.css │ │ │ ├── FiberInfoSectionAncestors.tsx │ │ │ ├── FiberInfoSectionConsumers.css │ │ │ ├── FiberInfoSectionConsumers.tsx │ │ │ ├── FiberInfoSectionContexts.css │ │ │ ├── FiberInfoSectionContexts.tsx │ │ │ ├── FiberInfoSectionEvents.css │ │ │ ├── FiberInfoSectionEvents.tsx │ │ │ ├── FiberInfoSectionHooks.css │ │ │ ├── FiberInfoSectionHooks.tsx │ │ │ ├── FiberInfoSectionLeakedHooks.tsx │ │ │ ├── FiberInfoSectionMemoHooks.css │ │ │ ├── FiberInfoSectionMemoHooks.tsx │ │ │ ├── FiberInfoSectionProps.css │ │ │ └── FiberInfoSectionProps.tsx │ ├── fiber-tree │ │ ├── ButtonExpand.css │ │ ├── ButtonExpand.svg │ │ ├── ButtonExpand.tsx │ │ ├── ScrollSelectedIntoViewIfNeeded.tsx │ │ ├── Tree.css │ │ ├── Tree.tsx │ │ ├── TreeHeader.css │ │ ├── TreeHeader.tsx │ │ ├── TreeLeaf.css │ │ ├── TreeLeaf.tsx │ │ ├── TreeLeafCaption.css │ │ ├── TreeLeafCaption.tsx │ │ ├── TreeLeafCaptionContent.css │ │ ├── TreeLeafCaptionContent.tsx │ │ ├── TreeLeafTimings.css │ │ ├── TreeLeafTimings.tsx │ │ └── contexts.ts │ ├── misc │ │ ├── FiberTreeKeyboardNav.tsx │ │ ├── WaitingForReady.css │ │ ├── WaitingForReady.tsx │ │ ├── WaitingForRenderer.css │ │ └── WaitingForRenderer.tsx │ ├── statebar │ │ ├── StateBar.css │ │ └── StateBar.tsx │ ├── statusbar │ │ ├── StatusBar.css │ │ └── StatusBar.tsx │ └── toolbar │ │ ├── ComponentSearch.css │ │ ├── ComponentSearch.tsx │ │ ├── SearchMatchesNav.css │ │ ├── SearchMatchesNav.tsx │ │ ├── SelectionHistoryNavigation.css │ │ ├── SelectionHistoryNavigation.tsx │ │ ├── Toolbar.css │ │ └── Toolbar.tsx │ ├── images │ ├── dots.svg │ ├── event-effect-create.svg │ ├── event-effect-destroy.svg │ ├── event-mount.svg │ ├── event-unmount.svg │ ├── event-update-bailout-memo.svg │ ├── event-update-bailout-state.svg │ ├── event-update.svg │ ├── expander.svg │ ├── expose-to-global.svg │ ├── fiber-key-tail.svg │ ├── pick-component.svg │ ├── pin.svg │ ├── source-loc-open.svg │ ├── source-loc.svg │ ├── table-sort-asc.svg │ ├── table-sort-desc.svg │ ├── table-sortable.svg │ ├── update-trigger.svg │ ├── warning-exc-sign.svg │ └── warning.svg │ ├── index.css │ ├── index.tsx │ ├── pages │ ├── Commits.css │ ├── Commits.tsx │ ├── Components.css │ ├── Components.tsx │ ├── ComponentsTree.css │ ├── ComponentsTree.tsx │ ├── MaybeLeaks.css │ ├── MaybeLeaks.tsx │ ├── components │ │ ├── ComponentSearch.css │ │ ├── ComponentSearch.tsx │ │ ├── ComponentsTable.css │ │ ├── ComponentsTable.tsx │ │ ├── Toolbar.css │ │ └── Toolbar.tsx │ ├── index.tsx │ └── maybe-leaks │ │ ├── Fiber.css │ │ ├── Fiber.tsx │ │ ├── FiberGroup.css │ │ ├── FiberGroup.tsx │ │ ├── LeaksList.css │ │ ├── LeaksList.tsx │ │ ├── Toolbar.css │ │ └── Toolbar.tsx │ ├── rempl-subscriber.ts │ ├── types.ts │ └── utils │ ├── duration.ts │ ├── events.tsx │ ├── fiber-maps.tsx │ ├── fiber.ts │ ├── find-match.tsx │ ├── highlighting.tsx │ ├── layout.ts │ ├── memory-leaks.tsx │ ├── open-file.tsx │ ├── page.tsx │ ├── pinned.tsx │ ├── react-renderers.tsx │ ├── selection.tsx │ ├── source-locations.tsx │ ├── subscription.ts │ └── tree.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true // Allows for the parsing of JSX 13 | } 14 | }, 15 | "settings": { 16 | "react": { 17 | "version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 18 | } 19 | }, 20 | "parser": "@typescript-eslint/parser", 21 | "plugins": ["@typescript-eslint"], 22 | "extends": [ 23 | "plugin:react/recommended", 24 | "plugin:@typescript-eslint/eslint-recommended", 25 | "plugin:@typescript-eslint/recommended" 26 | ], 27 | "ignorePatterns": ["dist", "scripts"], 28 | "rules": { 29 | "react/prop-types": "off", 30 | "no-duplicate-case": 2, 31 | "@typescript-eslint/ban-ts-comment": "off", 32 | "@typescript-eslint/explicit-module-boundary-types": "off", 33 | "@typescript-eslint/no-explicit-any": "off", 34 | "@typescript-eslint/no-this-alias": "off", 35 | "@typescript-eslint/no-var-requires": "off", 36 | "@typescript-eslint/no-unused-vars": [ 37 | 2, 38 | { 39 | "vars": "all", 40 | "args": "after-used" 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: 18 16 | cache: "npm" 17 | - run: npm ci 18 | - run: npm run lint 19 | - run: npm run tscheck 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GH-pages 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | # This workflow contains a single job called "build" 11 | build: 12 | # The type of runner that the job will run on 13 | runs-on: ubuntu-latest 14 | 15 | # Steps represent a sequence of tasks that will be executed as part of the job 16 | steps: 17 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 18 | - uses: actions/checkout@v2 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | cache: "npm" 24 | - run: npm ci 25 | - run: npm run build-gh-pages 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./.gh-pages 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .gh-pages 4 | .next 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "auto" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | 7 | "esbenp.prettier-vscode", // Prettier 8 | "dbaeumer.vscode-eslint" // ESLint 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "[typescript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !.npmignore -------------------------------------------------------------------------------- /dist/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !data-client.js 3 | !headless-browser-client.js 4 | !headless-browser-client.mjs 5 | !react-render-tracker.js 6 | -------------------------------------------------------------------------------- /examples/demo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-render-tracker-demo-app", 3 | "version": "1.0.0", 4 | "description": "Demo app for React Render Tracker examples", 5 | "private": true, 6 | "main": "src/index.jsx", 7 | "scripts": { 8 | "start": "node server" 9 | }, 10 | "dependencies": { 11 | "esbuild": "^0.14.38", 12 | "express": "^4.18.1", 13 | "react": "^18.1.0", 14 | "react-dom": "^18.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/demo-app/server.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | const express = require("express"); 3 | const server = express(); 4 | 5 | function asyncResponse(asyncFn, contentType = "text/javascript") { 6 | return async (req, res) => { 7 | // console.log("request", req.url); 8 | try { 9 | const content = await asyncFn(); 10 | 11 | res.type(contentType); 12 | res.send(content); 13 | res.end(); 14 | } catch (e) { 15 | res.status(500); 16 | res.end(e.message); 17 | } 18 | }; 19 | } 20 | 21 | exports.startAppServer = function (options) { 22 | options = options || {}; 23 | 24 | const port = options.port || 0; 25 | 26 | server.use(express.static(__dirname + "/src")); 27 | for (let [url, generator] of Object.entries({ 28 | "/app.js": async () => { 29 | const bundle = await esbuild.build({ 30 | entryPoints: [__dirname + "/src/index.jsx"], 31 | bundle: true, 32 | // minify: true, // React Render Tracker currently doesn't support for a production version of React 33 | format: "esm", 34 | write: false, 35 | }); 36 | 37 | return bundle.outputFiles[0].text; 38 | }, 39 | })) { 40 | server.get(url, asyncResponse(generator)); 41 | } 42 | 43 | return new Promise(resolve => { 44 | server.listen(port, async function () { 45 | const host = `http://localhost:${this.address().port}`; 46 | 47 | console.log(`Server listen on ${host}`); 48 | console.log(); 49 | 50 | resolve({ 51 | host, 52 | close: () => this.close(), 53 | }); 54 | }); 55 | }); 56 | }; 57 | 58 | if (require.main === module) { 59 | exports.startAppServer({ port: 3111 }); 60 | } 61 | -------------------------------------------------------------------------------- /examples/demo-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo app 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/demo-app/src/index.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | 4 | function App() { 5 | const [count, setCount] = React.useState(0); 6 | 7 | React.useEffect(() => { 8 | console.log("rendered"); 9 | }); 10 | 11 | return ( 12 | <> 13 |

Hello world

14 |
{count}
15 | 18 | 19 | ); 20 | } 21 | 22 | const reactRoot = ReactDOM.createRoot(document.getElementById("app")); 23 | 24 | reactRoot.render(); 25 | -------------------------------------------------------------------------------- /examples/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | webpack: config => { 6 | // append the CopyPlugin to copy the file to your public dir 7 | config.plugins.push( 8 | new CopyPlugin({ 9 | patterns: [ 10 | { 11 | from: "node_modules/react-render-tracker/dist/react-render-tracker.js", 12 | to: "static", 13 | }, 14 | ], 15 | }) 16 | ); 17 | 18 | // Important: return the modified config 19 | return config; 20 | }, 21 | }; 22 | 23 | module.exports = nextConfig; 24 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "22.13.2", 13 | "@types/react": "18.2.25", 14 | "@types/react-dom": "18.2.10", 15 | "copy-webpack-plugin": "^12.0.2", 16 | "eslint": "9.20.1", 17 | "eslint-config-next": "15.1.7", 18 | "next": "15.1.7", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "react-render-tracker": "file:../..", 22 | "typescript": "5.7.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | function Demo() { 4 | const [count, setCount] = React.useState(0); 5 | 6 | return ( 7 |
8 | Counter is {count} 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | 16 | export default function App() { 17 | return ( 18 | <> 19 |

My App

20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/next/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Head, Html, Main, NextScript } from "next/document"; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 |