Full Code of aidenybai/react-scan for AI

main b5be21653951 cached
238 files
1.5 MB
410.6k tokens
524 symbols
1 requests
Download .txt
Showing preview only (1,649K chars total). Download the full file or copy to clipboard to get everything.
Repository: aidenybai/react-scan
Branch: main
Commit: b5be21653951
Files: 238
Total size: 1.5 MB

Directory structure:
gitextract_xag52wf6/

├── .changeset/
│   ├── README.md
│   └── config.json
├── .github/
│   ├── CODE_OF_CONDUCT.md
│   └── workflows/
│       ├── build-extension.yml
│       └── pkg-pr-new.yaml
├── .gitignore
├── .npmrc
├── .oxlintrc.json
├── .vscode/
│   └── settings.json
├── AGENTS.md
├── BROWSER_EXTENSION_GUIDE.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│   ├── generate-certs.sh
│   └── serve-scan.sh
├── docs/
│   └── installation/
│       ├── astro.md
│       ├── cdn.md
│       ├── create-react-app.md
│       ├── next-js-app-router.md
│       ├── next-js-page-router.md
│       ├── parcel.md
│       ├── react-router.md
│       ├── remix.md
│       ├── rsbuild.md
│       ├── tanstack-start.md
│       └── vite.md
├── e2e/
│   ├── helpers.ts
│   ├── inspector.spec.ts
│   ├── notifications.spec.ts
│   ├── outlines.spec.ts
│   └── toolbar.spec.ts
├── kitchen-sink/
│   ├── index.html
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── src/
│   │   ├── examples/
│   │   │   ├── e2e-fixture/
│   │   │   │   └── index.tsx
│   │   │   ├── sierpinski/
│   │   │   │   ├── index.tsx
│   │   │   │   └── styles.css
│   │   │   └── todo-list/
│   │   │       ├── index.tsx
│   │   │       └── styles.css
│   │   ├── index.css
│   │   ├── index.tsx
│   │   ├── main.css
│   │   └── main.tsx
│   ├── tailwind.config.mjs
│   ├── tsconfig.json
│   └── vite.config.ts
├── package.json
├── packages/
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── assets/
│   │   │   │   └── css/
│   │   │   │       └── no-react.css
│   │   │   ├── background/
│   │   │   │   ├── icon.ts
│   │   │   │   └── index.ts
│   │   │   ├── content/
│   │   │   │   └── index.ts
│   │   │   ├── inject/
│   │   │   │   ├── index.ts
│   │   │   │   ├── notification.ts
│   │   │   │   └── react-scan.ts
│   │   │   ├── manifest.chrome.json
│   │   │   ├── manifest.firefox.json
│   │   │   ├── types/
│   │   │   │   ├── global.d.ts
│   │   │   │   └── messages.ts
│   │   │   ├── utils/
│   │   │   │   ├── constants.ts
│   │   │   │   └── helpers.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── scan/
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── auto.d.ts
│   │   ├── bin/
│   │   │   └── cli.js
│   │   ├── global.d.ts
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── postcss.rem2px.mjs
│   │   ├── scripts/
│   │   │   └── bump-version.js
│   │   ├── src/
│   │   │   ├── auto.ts
│   │   │   ├── cli-utils.mts
│   │   │   ├── cli-utils.test.mts
│   │   │   ├── cli.mts
│   │   │   ├── core/
│   │   │   │   ├── all-environments.ts
│   │   │   │   ├── fast-serialize.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── instrumentation.ts
│   │   │   │   ├── notifications/
│   │   │   │   │   ├── event-tracking.ts
│   │   │   │   │   ├── interaction-store.ts
│   │   │   │   │   ├── outline-overlay.ts
│   │   │   │   │   ├── performance-store.ts
│   │   │   │   │   ├── performance-utils.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── types.ts
│   │   │   │   └── utils.ts
│   │   │   ├── index.ts
│   │   │   ├── install-hook.ts
│   │   │   ├── monitoring/
│   │   │   │   └── next.ts
│   │   │   ├── new-outlines/
│   │   │   │   ├── canvas.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── offscreen-canvas.worker.ts
│   │   │   │   └── types.ts
│   │   │   ├── polyfills.ts
│   │   │   ├── react-component-name/
│   │   │   │   ├── __tests__/
│   │   │   │   │   ├── arrow-function.test.ts
│   │   │   │   │   ├── complex-patterns.test.ts
│   │   │   │   │   ├── function-declarations.test.ts
│   │   │   │   │   ├── general-cases.test.ts
│   │   │   │   │   ├── react-patterns.test.ts
│   │   │   │   │   ├── ts-patterns.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── astro.ts
│   │   │   │   ├── babel/
│   │   │   │   │   ├── get-descriptive-name.ts
│   │   │   │   │   ├── get-root-statement-path.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── is-componentish-name.ts
│   │   │   │   │   ├── is-nested-expression.ts
│   │   │   │   │   ├── is-path-valid.ts
│   │   │   │   │   ├── is-statement-top-level.ts
│   │   │   │   │   ├── path-references-import.ts
│   │   │   │   │   └── unwrap.ts
│   │   │   │   ├── core/
│   │   │   │   │   └── options.ts
│   │   │   │   ├── esbuild.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── rolldown.ts
│   │   │   │   ├── rollup.ts
│   │   │   │   ├── rspack.ts
│   │   │   │   ├── tsconfig.json
│   │   │   │   ├── vite.ts
│   │   │   │   └── webpack.ts
│   │   │   ├── types.d.ts
│   │   │   ├── types.ts
│   │   │   ├── web/
│   │   │   │   ├── assets/
│   │   │   │   │   └── css/
│   │   │   │   │       └── styles.tailwind.css
│   │   │   │   ├── components/
│   │   │   │   │   ├── copy-to-clipboard/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── icon/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── slider/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── sticky-section/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── svg-sprite/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   └── toggle/
│   │   │   │   │       └── index.tsx
│   │   │   │   ├── constants.ts
│   │   │   │   ├── hooks/
│   │   │   │   │   ├── use-delayed-value.ts
│   │   │   │   │   ├── use-merged-refs.ts
│   │   │   │   │   └── use-virtual-list.ts
│   │   │   │   ├── state.ts
│   │   │   │   ├── toolbar.tsx
│   │   │   │   ├── utils/
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── create-store.ts
│   │   │   │   │   ├── geiger.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── log.ts
│   │   │   │   │   ├── pin.ts
│   │   │   │   │   └── preact/
│   │   │   │   │       └── constant.ts
│   │   │   │   ├── views/
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── inspector/
│   │   │   │   │   │   ├── components-tree/
│   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   └── state.ts
│   │   │   │   │   │   ├── diff-value.tsx
│   │   │   │   │   │   ├── flash-overlay.ts
│   │   │   │   │   │   ├── header.tsx
│   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   ├── logging.ts
│   │   │   │   │   │   ├── overlay/
│   │   │   │   │   │   │   └── index.tsx
│   │   │   │   │   │   ├── properties.tsx
│   │   │   │   │   │   ├── states.ts
│   │   │   │   │   │   ├── timeline/
│   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   └── utils.ts
│   │   │   │   │   │   ├── utils.ts
│   │   │   │   │   │   ├── what-changed.tsx
│   │   │   │   │   │   └── whats-changed/
│   │   │   │   │   │       └── use-change-store.ts
│   │   │   │   │   ├── notifications/
│   │   │   │   │   │   ├── collapsed-event.tsx
│   │   │   │   │   │   ├── data.ts
│   │   │   │   │   │   ├── details-routes.tsx
│   │   │   │   │   │   ├── icons.tsx
│   │   │   │   │   │   ├── notification-header.tsx
│   │   │   │   │   │   ├── notification-tabs.tsx
│   │   │   │   │   │   ├── notifications.tsx
│   │   │   │   │   │   ├── optimize.tsx
│   │   │   │   │   │   ├── other-visualization.tsx
│   │   │   │   │   │   ├── popover.tsx
│   │   │   │   │   │   ├── render-bar-chart.tsx
│   │   │   │   │   │   ├── render-explanation.tsx
│   │   │   │   │   │   └── slowdown-history.tsx
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   └── header.tsx
│   │   │   │   │   └── toolbar/
│   │   │   │   │       └── index.tsx
│   │   │   │   └── widget/
│   │   │   │       ├── fps-meter.tsx
│   │   │   │       ├── header.tsx
│   │   │   │       ├── helpers.ts
│   │   │   │       ├── index.tsx
│   │   │   │       ├── resize-handle.tsx
│   │   │   │       └── types.ts
│   │   │   └── worker-shim.ts
│   │   ├── tailwind.config.mjs
│   │   ├── tsconfig.json
│   │   ├── tsup.config.ts
│   │   ├── vite.config.mts
│   │   └── worker-plugin.ts
│   ├── vite-plugin-react-scan/
│   │   ├── .npmignore
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── global.d.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── website/
│       ├── .gitignore
│       ├── .oxlintrc.json
│       ├── AGENTS.md
│       ├── README.md
│       ├── app/
│       │   ├── api/
│       │   │   └── waitlist/
│       │   │       └── route.ts
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   ├── react-scan.ts
│       │   └── replay/
│       │       └── page.tsx
│       ├── components/
│       │   ├── cli.tsx
│       │   ├── code.tsx
│       │   ├── companies.tsx
│       │   ├── counter.tsx
│       │   ├── footer.tsx
│       │   ├── header.tsx
│       │   ├── icons/
│       │   │   ├── icon-discord.tsx
│       │   │   ├── icon-github.tsx
│       │   │   └── types.ts
│       │   ├── install-guide.tsx
│       │   ├── test-data-types.tsx
│       │   └── todo-demo.tsx
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── public/
│       │   └── auto.global.js
│       ├── tailwind.config.ts
│       └── tsconfig.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── scripts/
│   ├── build-worker.ts
│   ├── bump-version.js
│   ├── version-warning.mjs
│   └── workspace.mjs
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .changeset/README.md
================================================
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)


================================================
FILE: .changeset/config.json
================================================
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [["react-scan", "@react-scan/extension"]],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@react-scan/website"]
}


================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery and unwelcome sexual attention or
  advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
  address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at aiden.bai05@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

================================================
FILE: .github/workflows/build-extension.yml
================================================
name: Build Extension

on:
  push:
    branches:
      - main
    paths:
      - 'packages/extension/**'

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: main

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Setup PNPM
        uses: pnpm/action-setup@v2
        with:
          version: 9.x

      - name: Install dependencies
        run: pnpm install

      - name: Get package info
        id: package
        run: |
          echo "name=$(node -p "require('./packages/extension/package.json').name")" >> $GITHUB_OUTPUT
          echo "version=$(node -p "require('./packages/extension/package.json').version")" >> $GITHUB_OUTPUT

      - name: Build extensions
        run: |
          pnpm build
          cd packages/extension
          rm -rf build
          pnpm pack:all

      - name: Commit changes
        if: github.ref == 'refs/heads/main'
        run: |
          git checkout main
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add -f packages/extension/build/*.zip
          git diff --staged --quiet || (git commit -m "chore: update extension builds [skip ci]" && git push)


================================================
FILE: .github/workflows/pkg-pr-new.yaml
================================================
name: Publish Any Commit
on:
  push:
    branches:
      - "**"
  pull_request:
    branches:
      - "**"

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18]

    steps:
      - uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9.1.0

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile --strict-peer-dependencies=false

      - name: Build
        run: |
          cd packages/scan
          NODE_ENV=production pnpm build
        env:
          NODE_ENV: production

      - name: Publish NPM Package to pkg-pr-new
        run: pnpx pkg-pr-new publish ./packages/scan
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
.env
dist
**/*.tgz
*.log
build
!packages/extension/build/
playgrounds
# SSL Certificates
bin/certs/*.pem
bin/certs/*.key
.cursor
# Playwright
test-results/
playwright-report/


================================================
FILE: .npmrc
================================================
prefer-workspace-packages=true
link-workspace-packages=true

================================================
FILE: .oxlintrc.json
================================================
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["typescript", "react", "import"],
  "ignorePatterns": [
    "dist",
    "build",
    "node_modules",
    "**/*.css",
    "**/*.astro"
  ],
  "categories": {},
  "rules": {
    "no-unused-vars": [
      "warn",
      {
        "vars": "all",
        "args": "all",
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrors": "none"
      }
    ],
    "no-unused-labels": "warn",
    "no-unused-private-class-members": "warn",
    "no-console": "warn",
    "typescript/no-explicit-any": "warn",
    "typescript/no-non-null-assertion": "warn",
    "react/no-danger": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "css.lint.unknownAtRules": "ignore",
  "oxc.lint.enable": true,
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "typescript.tsdk": "node_modules/typescript/lib"
}


================================================
FILE: AGENTS.md
================================================
## General Rules

- MUST: Use TypeScript interfaces over types.
- MUST: Keep all types in the global scope.
- MUST: Use arrow functions over function declarations
- MUST: Never comment unless absolutely necessary.
  - If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack
- MUST: Use kebab-case for files
- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).
  - Example: for .map(), you can use `innerX` instead of `x`
  - Example: instead of `moved` use `didPositionChange`
- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
- MUST: Do not type cast ("as") unless absolutely necessary
- MUST: Remove unused code and don't repeat yourself.
- MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution.
- MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`).
- MUST: Put small, focused utility functions in `utils/` with one utility per file.
- MUST: Use Boolean over !!.

## Testing

Run checks always before committing with:

```bash
pnpm build
pnpm lint
pnpm format
```


================================================
FILE: BROWSER_EXTENSION_GUIDE.md
================================================
# Browser Extension Installation Guide

> [!NOTE]
> The React Scan browser extension currently uses `react-scan@0.4.3`

## Chrome

> You can download the Chrome browser extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/react-scan/anmmhkomejbdklkhoiloeaehppaffmdf). Below is a guide to installing the extension manually.

1. Download the [`chrome-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file.
2. Unzip the file.
3. Open Chrome and navigate to `chrome://extensions/`.
4. Enable "Developer mode" if it is not already enabled.
5. Click "Load unpacked" and select the unzipped folder (or drag the folder into the page).

## Firefox

> React Scan's Browser extension is still pending approvals from Firefox Add-ons. Below is a guide to installing the extension manually.

1. Download the [`firefox-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file.
2. Unzip the file.
3. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`.
4. Click "Load Temporary Add-on..."
5. Select `manifest.json` from the unzipped folder

## Brave

> React Scan's Browser extension is still pending approvals from Brave Browser. Below is a guide to installing the extension manually.

1. Download the [`brave-extension-v1.1.4.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file.
2. Unzip the file.
3. Open Brave and navigate to `brave://extensions`.
4. Click "Load unpacked" and select the unzipped folder (or drag the folder into the page).


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to React Scan

First off, thanks for taking the time to contribute! ❤️

## Table of Contents

- [Contributing to React Scan](#contributing-to-react-scan)
  - [Table of Contents](#table-of-contents)
  - [Project Structure](#project-structure)
  - [Development Setup](#development-setup)
  - [Contributing Guidelines](#contributing-guidelines)
    - [Commits](#commits)
    - [Pull Request Process](#pull-request-process)
    - [Development Workflow](#development-workflow)
  - [Getting Help](#getting-help)

## Project Structure

This is a monorepo containing several packages:

- `packages/scan` - Core React Scan package
- `packages/vite-plugin-react-scan` - Vite plugin for React Scan
- `packages/extension` - VS Code extension

## Development Setup

1. **Clone and Install**
   ```bash
   git clone https://github.com/aidenybai/react-scan.git
   cd react-scan
   pnpm install
   ```

2. **Build all packages**
   ```bash
   pnpm build
   ```

3. **Testing React Scan**
   ```bash
   cd packages/scan
   pnpm build:copy
   ```
   - This will build the package and then copy it to your clipboard as an IIFE (immedietely invoked function expression). This will allow you to paste it into the browser console to test it on any website

https://github.com/user-attachments/assets/f279e664-479f-4e39-bff4-1bbfee30af22

## Contributing Guidelines

### Commits

We use conventional commits to ensure consistent commit messages:

- `feat:` New features
- `fix:` Bug fixes
- `docs:` Documentation changes
- `chore:` Maintenance tasks
- `test:` Adding or updating tests
- `refactor:` Code changes that neither fix bugs nor add features

Example: `fix(scan): fix a typo`

### Pull Request Process

1. Fork the repository
2. Create your feature branch (`git checkout -b feat/amazing-feature`)
3. Commit your changes using conventional commits
4. Push to your branch
5. Open a Pull Request
6. Ask for reviews (@pivanov, @RobPruzan are your friends in this journey)

### Development Workflow

1. **TypeScript**
   - All code must be written in TypeScript
   - Ensure strict type checking passes
   - No `any` types unless absolutely necessary

2. **Code Style**
   - We use Biome for formatting and linting
   - Run `pnpm format` to format code
   - Run `pnpm lint` to check for issues

3. **Documentation**
   - Update relevant documentation
   - Add JSDoc comments for public APIs
   - Update README if needed

## Getting Help
- Check existing issues
- Create a new issue

<br />

⚛️ Happy coding! 🚀


================================================
FILE: LICENSE
================================================
Copyright 2025 Aiden Bai, Million Software, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# <img src="https://github.com/aidenybai/react-scan/blob/main/.github/assets/logo.svg" width="30" height="30" align="center" /> React Scan

React Scan automatically detects performance issues in your React app.

- Requires no code changes -- just drop it in
- Highlights exactly the components you need to optimize
- Always accessible through a toolbar on page

### Quick Start

```bash
npx -y react-scan@latest init
```

### [**Try out a demo! →**](https://react-scan.million.dev)
<img
  src="https://github.com/user-attachments/assets/c21b3afd-c7e8-458a-a760-9a027be7dc02"
  alt="React Scan in action"
  width="600"
/>

## Install

The `init` command will automatically detect your framework, install `react-scan` via npm, and set up your project.

```bash
npx -y react-scan@latest init
```

### Manual Installation

Install the package:

```bash
npm install -D react-scan
```

Then add the script tag to your app. Pick the guide for your framework:

#### Script Tag

Paste this before any scripts in your `index.html`:

```html
<!-- paste this BEFORE any scripts -->
<script
  crossOrigin="anonymous"
  src="//unpkg.com/react-scan/dist/auto.global.js"
></script>
```

#### Next.js (App Router)

Add this inside of your `app/layout.tsx`:

```tsx
import Script from "next/script";

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Script
          src="//unpkg.com/react-scan/dist/auto.global.js"
          crossOrigin="anonymous"
          strategy="beforeInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

#### Next.js (Pages Router)

Add this into your `pages/_document.tsx`:

```tsx
import { Html, Head, Main, NextScript } from "next/document";
import Script from "next/script";

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <Script
          src="//unpkg.com/react-scan/dist/auto.global.js"
          crossOrigin="anonymous"
          strategy="beforeInteractive"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
```

#### Vite

Example `index.html` with React Scan enabled:

```html
<!doctype html>
<html lang="en">
  <head>
    <script
      crossOrigin="anonymous"
      src="//unpkg.com/react-scan/dist/auto.global.js"
    ></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

#### Remix

Add this inside your `app/root.tsx`:

```tsx
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";

export default function App() {
  return (
    <html>
      <head>
        <Meta />
        <script
          crossOrigin="anonymous"
          src="//unpkg.com/react-scan/dist/auto.global.js"
        />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}
```

### Browser Extension

Install the extension by following the guide [here](https://github.com/aidenybai/react-scan/blob/main/BROWSER_EXTENSION_GUIDE.md).

## API Reference

<details>
<summary><code>Options</code></summary>

<br />

```tsx
export interface Options {
  /**
   * Enable/disable scanning
   * @default true
   */
  enabled?: boolean;

  /**
   * Force React Scan to run in production (not recommended)
   * @default false
   */
  dangerouslyForceRunInProduction?: boolean;

  /**
   * Log renders to the console
   * @default false
   */
  log?: boolean;

  /**
   * Show toolbar bar
   * @default true
   */
  showToolbar?: boolean;

  /**
   * Animation speed
   * @default "fast"
   */
  animationSpeed?: "slow" | "fast" | "off";

  onCommitStart?: () => void;
  onRender?: (fiber: Fiber, renders: Array<Render>) => void;
  onCommitFinish?: () => void;
}
```

</details>

- `scan(options: Options)`: Imperative API to start scanning
- `useScan(options: Options)`: Hook API to start scanning
- `setOptions(options: Options): void`: Set options at runtime
- `getOptions()`: Get the current options
- `onRender(Component, onRender: (fiber: Fiber, render: Render) => void)`: Hook into a specific component's renders

## Why React Scan?

React can be tricky to optimize.

The issue is that component props are compared by reference, not value. This is intentional -- rendering can be cheap to run.

However, this makes it easy to accidentally cause unnecessary renders, making the app slow. Even production apps with hundreds of engineers can't fully optimize their apps (see [GitHub](https://github.com/aidenybai/react-scan/blob/main/.github/assets/github.mp4), [Twitter](https://github.com/aidenybai/react-scan/blob/main/.github/assets/twitter.mp4), and [Instagram](https://github.com/aidenybai/react-scan/blob/main/.github/assets/instagram.mp4)).

```jsx
<ExpensiveComponent onClick={() => alert("hi")} style={{ color: "purple" }} />
```

React Scan helps you identify these issues by automatically detecting and highlighting renders that cause performance issues.

## Resources & Contributing

Want to try it out? Check the [demo](https://react-scan.million.dev).

Looking to contribute? Check the [Contributing Guide](https://github.com/aidenybai/react-scan/blob/main/CONTRIBUTING.md).

Want to talk to the community? Join our [Discord](https://discord.gg/X9yFbcV2rF).

Find a bug? Head to our [issue tracker](https://github.com/aidenybai/react-scan/issues).

[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-scan/blob/main/CONTRIBUTING.md)

## Acknowledgments

- [React Devtools](https://react.dev/learn/react-developer-tools) for the initial idea of highlighting renders
- [Million Lint](https://million.dev) for scanning and linting approaches
- [Why Did You Render?](https://github.com/welldone-software/why-did-you-render) for the concept of detecting unnecessary renders

## License

React Scan is [MIT-licensed](LICENSE) open-source software by Aiden Bai, [Million Software, Inc.](https://million.dev), and [contributors](https://github.com/aidenybai/react-scan/graphs/contributors).


================================================
FILE: bin/generate-certs.sh
================================================
#!/bin/bash

mkdir -p bin/certs
openssl req -x509 -newkey rsa:2048 -keyout bin/certs/key.pem -out bin/certs/cert.pem -days 365 -nodes -subj "/CN=127.0.0.1"


================================================
FILE: bin/serve-scan.sh
================================================
#!/bin/bash

# Determine the directory of the script
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

# Default values
DEFAULT_PATH="./packages/scan/dist"
DEFAULT_PORT="4000"
DEFAULT_CERT="$SCRIPT_DIR/certs/cert.pem"
DEFAULT_KEY="$SCRIPT_DIR/certs/key.pem"

# Positional arguments
SERVE_PATH="$1" # First argument is the path

# Get optional flags
shift # Remove the first argument from the list
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --port) PORT_ARG="$2"; shift ;;
        --cert) CERT_ARG="$2"; shift ;;
        --key) KEY_ARG="$2"; shift ;;
        *) echo "Unknown parameter: $1" >&2; exit 1 ;;
    esac
    shift
done

# Use provided arguments or defaults
SERVE_PATH="${SERVE_PATH:-$DEFAULT_PATH}"
SERVE_PORT="${PORT_ARG:-$DEFAULT_PORT}"
SERVE_CERT="${CERT_ARG:-$DEFAULT_CERT}"
SERVE_KEY="${KEY_ARG:-$DEFAULT_KEY}"

# Run the server with CORS enabled
http-server "$SERVE_PATH" -p "$SERVE_PORT" --ssl --cert "$SERVE_CERT" --key "$SERVE_KEY" --cors


================================================
FILE: docs/installation/astro.md
================================================
# Astro Guide

## As a script tag

Add the script tag to your root layout.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```astro
<!doctype html>
<html lang="en">
  <head>
    <script is:inline src="https://unpkg.com/react-scan/dist/auto.global.js" />

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```

## As a module import

Add the script to your root layout

```astro
<!doctype html>
<html lang="en">
  <head>
    <script>
      import { scan } from 'react-scan';

      scan({
        enabled: true,
      });
    </script>
    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```

If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```


================================================
FILE: docs/installation/cdn.md
================================================
# CDN

You can choose one of the following URLs to initialize React Scan via CDN.

## Usage

```html
<script src="https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js"></script>
```

## Available URLs

### JSDelivr

```txt
https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js
```

### UNPKG

```txt
https://unpkg.com/react-scan/dist/auto.global.js
```


================================================
FILE: docs/installation/create-react-app.md
================================================
# Create React App (CRA) Guide

## As a script tag

Add the script tag to your `index.html`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```html
<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```

## As a module import

In your project entrypoint (e.g. `src/index`, `src/main`):

```jsx
// src/index.jsx

// must be imported before React and React DOM
import { scan } from "react-scan";
import React from "react";

scan({
  enabled: true,
});
```
If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it.


================================================
FILE: docs/installation/next-js-app-router.md
================================================
# NextJS App Router Guide

## As a script tag

Add the script tag to your `app/layout`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```jsx
// app/layout
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <script src="https://unpkg.com/react-scan/dist/auto.global.js" />
        {/* rest of your scripts go under */}
      </head>
      <body>{children}</body>
    </html>
  );
}
```

## As a module import

Create a `<ReactScan>` client component:

```jsx
// path/to/ReactScanComponent

"use client";
// react-scan must be imported before react
import { scan } from "react-scan";
import { JSX, useEffect } from "react";

export function ReactScan(): JSX.Element {
  useEffect(() => {
    scan({
      enabled: true,
    });
  }, []);

  return <></>;
}
```

Import the `<ReactScan>` component into `app/layout`:

```jsx
// app/layout

// This component must be the top-most import in this file!
import { ReactScan } from "path/to/ReactScanComponent";

// ...

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <ReactScan />
      <body>{children}</body>
    </html>
  );
}
```

If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

================================================
FILE: docs/installation/next-js-page-router.md
================================================
# NextJS Page Router Guide

## As a script tag

Add the script tag to your `pages/_document`

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```jsx
// pages/_document
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script src="https://unpkg.com/react-scan/dist/auto.global.js" />

        {/* rest of your scripts go under */}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
```

## As a module import

Add the following code to your `App` component in `pages/_app`:

```jsx
// pages/_app

// react-scan must be the top-most import
import { scan } from "react-scan";
import { useEffect } from "react";

export default function App({ Component, pageProps }) {
  useEffect(() => {
    // Make sure to run React Scan after hydration
    scan({
      enabled: true,
    });
  }, []);
  return <Component {...pageProps} />;
}
```


If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

================================================
FILE: docs/installation/parcel.md
================================================
# Parcel Guide

## As a script tag

Add the script tag to your `index.html`

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```html
<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```


================================================
FILE: docs/installation/react-router.md
================================================
# React Router v7 Guide

## As a script tag

Add the script tag to your `Layout` component in the `app/root`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```jsx
// app/root
// ...
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <script src="https://unpkg.com/react-scan/dist/auto.global.js" />
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
// ...
```

> [!CAUTION]
> This only works for React 19

## As an import

Add the following code to your `app/root`

```jsx
// app/root

// Must be imported before React Router
import { scan } from "react-scan"; 
import { Links, Meta, Scripts, ScrollRestoration } from "react-router";
import { useEffect } from "react";

export function Layout({ children }) {
  useEffect(() => {
    // Make sure to run react-scan only after hydration
    scan({
      enabled: true,
    });
  }, []);

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

// ...
```

If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM), as well as React Router, in your entire project, as it needs to hijack React DevTools before React gets to access it.


================================================
FILE: docs/installation/remix.md
================================================
# Remix Guide

## As a script tag

Add the script tag to your `<Layout>` component in `app/root`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```jsx
// app/root.jsx
import {
  Links,
  Meta,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        {/* Must run before any of your scripts */}
        <script src="https://unpkg.com/react-scan/dist/auto.global.js" />
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

// ...
```

> [!CAUTION]
> This only works for React 19

## As a module import

Add the following code to your `app/root`:

```jsx
// app/root.jsx
import { scan } from "react-scan"; // Must be imported before Remix
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export function Layout({ children }) {
  useEffect(() => {
    // Make sure to run React Scan after hydration
    scan({
      enabled: true,
    });
  }, []);

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
```

If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM), as well as Remix, in your entire project, as it needs to hijack React DevTools before React gets to access it.

Alternatively you can also do the following code in `app/entry.client`:

```jsx
// app/entry.client.jsx
import { RemixBrowser } from "@remix-run/react";
import { StrictMode, startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
import { scan } from "react-scan";

scan({
  enabled: true,
});

// Hydration must happen in sync!
// startTransition(() => {
hydrateRoot(
  document,
  <StrictMode>
    <RemixBrowser />
  </StrictMode>
);
// });
```

> [!CAUTION]
> This only works for React 19


================================================
FILE: docs/installation/rsbuild.md
================================================
# Rsbuild Guide

## As a script tag

If you are using Rsbuild's default HTML template, add the script tag via [html.tags](https://rsbuild.dev/config/html/tags).

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```ts
// rsbuild.config.ts
export default {
  html: {
    tags: [
      {
        tag: "script",
        attrs: {
          src: "https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js",
        },
        append: false,
      },
    ],
  },
};
```

If you are using a custom HTML template, add the script tag to your template file.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js"></script>

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```

## As a module import

In your project entrypoint (e.g. `src/index`, `src/main`):

```jsx
// src/index.jsx

// must be imported before React and React DOM
import { scan } from "react-scan";
import React from "react";

scan({
  enabled: true,
});
```

If you want react-scan to also run in production, use the react-scan/all-environments import path
```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```

> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it.


================================================
FILE: docs/installation/tanstack-start.md
================================================
# TanStack Router Guide

## As a script tag

Add the script tag to your `<RootDocument>` component at `app/routes/__root`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```jsx
// app/routes/__root
import { Meta, Scripts } from "@tanstack/start";
// ...

function RootDocument({ children }) {
  return (
    <html>
      <head>
        <script src="https://unpkg.com/react-scan/dist/auto.global.js" />
        <Meta />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
}

// ...
```

> [!CAUTION]
> This only works for React 19

## As a module import

Add the following code to your `<RootDocument>` component at `app/routes/__root`:

```jsx
// app/routes/__root

// react-scan must be imported before React and TanStack Start
import { scan } from "react-scan";
import { Meta, Scripts } from "@tanstack/start";
import { useEffect } from "react";

// ...

function RootDocument({ children }) {
  useEffect(() => {
    // Make sure to run this only after hydration
    scan({
      enabled: true,
    });
  }, []);
  return (
    <html>
      <head>
        <Meta />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
}
```

> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it.

Alternatively you can also do the following code in `app/client`:

```jsx
// app/client
import { scan } from "react-scan"; // must be imported before React and React DOM
import { hydrateRoot } from "react-dom/client";
import { StartClient } from "@tanstack/start";
import { createRouter } from "./router";

scan({
  enabled: true,
});

const router = createRouter();

hydrateRoot(document, <StartClient router={router} />);
```

> [!CAUTION]
> This only works for React 19

If you want react-scan to also run in production, use the react-scan/all-environments import path

```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```


================================================
FILE: docs/installation/vite.md
================================================
# Vite Guide

## As a script tag

Add the script tag to your `index.html`.

Refer to the [CDN Guide](https://github.com/aidenybai/react-scan/blob/main/docs/installation/cdn.md) for the available URLs.

```html
<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
```

## As a module import

In your project entrypoint (e.g. `src/index`, `src/main`):

```jsx
// src/index
import { scan } from "react-scan"; // must be imported before React and React DOM
import React from "react";

scan({
  enabled: true,
});
```

If you want react-scan to also run in production, use the react-scan/all-environments import path

```diff
- import { scan } from "react-scan";
+ import { scan } from "react-scan/all-environments";
```


> [!CAUTION]
> React Scan must be imported before React (and other React renderers like React DOM) in your entire project, as it needs to hijack React DevTools before React gets to access it.

## Vite plugin

TODO

## Preserving component names

TODO


================================================
FILE: e2e/helpers.ts
================================================
import { type Page } from '@playwright/test';

export const FIXTURE_URL = '/?example=e2e-fixture';

export async function gotoFixture(page: Page): Promise<void> {
  await page.goto(FIXTURE_URL);
  await page.waitForSelector('[data-testid="heading"]', { timeout: 10_000 });
  // Wait for React Scan to boot and expose __REACT_SCAN__
  await page.waitForFunction(
    () => typeof (window as any).__REACT_SCAN__?.ReactScanInternals !== 'undefined',
    { timeout: 15_000 },
  );
  // Install a render counter by patching the onRender option on the signal
  await page.evaluate(() => {
    (window as any).__E2E_RENDER_COUNT__ = 0;
    const internals = (window as any).__REACT_SCAN__?.ReactScanInternals;
    if (internals?.options) {
      const prev = internals.options.value;
      const prevOnRender = prev.onRender;
      internals.options.value = {
        ...prev,
        onRender: (...args: any[]) => {
          (window as any).__E2E_RENDER_COUNT__++;
          if (prevOnRender) prevOnRender(...args);
        },
      };
    }
  });
  // Wait for initial mount renders to settle then reset
  await page.waitForTimeout(500);
  await page.evaluate(() => {
    (window as any).__E2E_RENDER_COUNT__ = 0;
  });
}

export async function getRenderCount(page: Page): Promise<number> {
  return page.evaluate(() => (window as any).__E2E_RENDER_COUNT__ ?? 0);
}

export async function waitForRenders(
  page: Page,
  timeout = 5000,
): Promise<number> {
  const startCount = await getRenderCount(page);
  return page.evaluate(
    ({ start, t }) => {
      return new Promise<number>((resolve) => {
        const check = () => {
          const current = (window as any).__E2E_RENDER_COUNT__ ?? 0;
          if (current > start) {
            resolve(current - start);
            return true;
          }
          return false;
        };
        if (check()) return;
        const interval = setInterval(() => {
          if (check()) clearInterval(interval);
        }, 50);
        setTimeout(() => {
          clearInterval(interval);
          resolve(0);
        }, t);
      });
    },
    { start: startCount, t: timeout },
  );
}

export async function isReactScanActive(page: Page): Promise<boolean> {
  return page.evaluate(() => {
    return typeof (window as any).__REACT_SCAN__ !== 'undefined';
  });
}

export async function hasShadowRoot(page: Page): Promise<boolean> {
  return page.evaluate(() => {
    return document.getElementById('react-scan-root')?.shadowRoot != null;
  });
}


================================================
FILE: e2e/inspector.spec.ts
================================================
import { test, expect } from '@playwright/test';
import { gotoFixture } from './helpers';

test.describe('Inspector', () => {
  test.beforeEach(async ({ page }) => {
    await gotoFixture(page);
  });

  test('inspect state is available in React Scan internals', async ({ page }) => {
    const hasInspectState = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      if (!scan?.ReactScanInternals?.Store) return false;
      const inspectState = scan.ReactScanInternals.Store.inspectState;
      return inspectState !== undefined && inspectState !== null;
    });

    expect(hasInspectState).toBe(true);
  });

  test('inspect state starts as inspect-off', async ({ page }) => {
    const kind = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      return scan?.ReactScanInternals?.Store?.inspectState?.value?.kind ?? null;
    });

    expect(kind).toBe('inspect-off');
  });

  test('shadow DOM contains toolbar elements', async ({ page }) => {
    const elementCount = await page.evaluate(() => {
      const root = document.getElementById('react-scan-root');
      return root?.shadowRoot?.querySelectorAll('*').length ?? 0;
    });
    expect(elementCount).toBeGreaterThan(5);
  });

  test('inspect state can be set programmatically', async ({ page }) => {
    const activated = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      if (!scan?.ReactScanInternals?.Store?.inspectState) return false;
      scan.ReactScanInternals.Store.inspectState.value = { kind: 'focused', focusedDomElement: null };
      return scan.ReactScanInternals.Store.inspectState.value.kind === 'focused';
    });

    expect(activated).toBe(true);
  });
});


================================================
FILE: e2e/notifications.spec.ts
================================================
import { test, expect } from '@playwright/test';
import { gotoFixture } from './helpers';

test.describe('Notifications', () => {
  test.beforeEach(async ({ page }) => {
    await gotoFixture(page);
  });

  test('slow interaction is detected and recorded', async ({ page }) => {
    await page.click('[data-testid="trigger-slow"]');
    await page.waitForTimeout(2000);

    const hasActiveStore = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      if (!scan?.ReactScanInternals?.Store) return false;
      // Verify the notification system is wired up (interactionListeningForRenders is a function when active)
      return typeof scan.ReactScanInternals.Store.interactionListeningForRenders === 'function';
    });

    expect(hasActiveStore).toBe(true);
  });

  test('notification system initializes with the toolbar', async ({ page }) => {
    const hasCanvas = await page.evaluate(() => {
      return document.querySelectorAll('canvas').length > 0;
    });
    expect(hasCanvas).toBe(true);
  });

  test('repeated slow interactions do not break the toolbar', async ({ page }) => {
    for (let i = 0; i < 3; i++) {
      await page.click('[data-testid="trigger-slow"]');
      await page.waitForTimeout(500);
    }
    await page.waitForTimeout(2000);

    const shadowContent = await page.evaluate(() => {
      const root = document.getElementById('react-scan-root');
      return root?.shadowRoot?.innerHTML ?? '';
    });
    expect(shadowContent.length).toBeGreaterThan(100);
  });
});


================================================
FILE: e2e/outlines.spec.ts
================================================
import { test, expect, type Page } from '@playwright/test';
import { gotoFixture, getRenderCount } from './helpers';

async function clickAndCountRenders(
  page: Page,
  selector: string,
  waitMs = 1000,
): Promise<number> {
  await page.evaluate(() => {
    (window as any).__E2E_RENDER_COUNT__ = 0;
  });
  await page.click(selector);
  await page.waitForTimeout(waitMs);
  return getRenderCount(page);
}

test.describe('Render Outlines', () => {
  test.beforeEach(async ({ page }) => {
    await gotoFixture(page);
  });

  test('state update triggers render tracking', async ({ page }) => {
    const count = await clickAndCountRenders(page, '[data-testid="increment"]');
    expect(count).toBeGreaterThan(0);
  });

  test('rapid updates produce multiple tracked renders', async ({ page }) => {
    const count = await clickAndCountRenders(page, '[data-testid="trigger-rapid"]', 2000);
    expect(count).toBeGreaterThan(5);
  });

  test('outline canvas exists on the page', async ({ page }) => {
    const hasCanvas = await page.evaluate(() => {
      return document.querySelectorAll('canvas').length > 0;
    });
    expect(hasCanvas).toBe(true);
  });

  test('context change triggers render tracking', async ({ page }) => {
    const count = await clickAndCountRenders(page, '[data-testid="toggle-theme"]');
    expect(count).toBeGreaterThan(0);
  });

  test('unstable props on memo components trigger render tracking', async ({ page }) => {
    const count = await clickAndCountRenders(page, '[data-testid="trigger-unstable"]');
    expect(count).toBeGreaterThan(0);
  });

  test('render count accumulates with repeated clicks', async ({ page }) => {
    await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; });

    await page.click('[data-testid="increment"]');
    await page.waitForTimeout(300);
    const after1 = await getRenderCount(page);

    await page.click('[data-testid="increment"]');
    await page.waitForTimeout(300);
    const after2 = await getRenderCount(page);

    await page.click('[data-testid="increment"]');
    await page.waitForTimeout(300);
    const after3 = await getRenderCount(page);

    expect(after1).toBeGreaterThan(0);
    expect(after2).toBeGreaterThan(after1);
    expect(after3).toBeGreaterThan(after2);
  });
});


================================================
FILE: e2e/toolbar.spec.ts
================================================
import { test, expect } from '@playwright/test';
import { gotoFixture, isReactScanActive, hasShadowRoot } from './helpers';

test.describe('Toolbar', () => {
  test.beforeEach(async ({ page }) => {
    await gotoFixture(page);
  });

  test('React Scan initializes and attaches to the page', async ({ page }) => {
    const active = await isReactScanActive(page);
    expect(active).toBe(true);
  });

  test('React Scan internals are accessible', async ({ page }) => {
    const hasInternals = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      return (
        scan?.ReactScanInternals !== undefined &&
        scan.ReactScanInternals.options !== undefined &&
        scan.ReactScanInternals.Store !== undefined
      );
    });
    expect(hasInternals).toBe(true);
  });

  test('options are set correctly', async ({ page }) => {
    const options = await page.evaluate(() => {
      const scan = (window as any).__REACT_SCAN__;
      const opts = scan?.ReactScanInternals?.options?.value;
      if (!opts) return null;
      return {
        enabled: opts.enabled,
        dangerouslyForceRunInProduction: opts.dangerouslyForceRunInProduction,
        showToolbar: opts.showToolbar,
      };
    });
    expect(options).toEqual({
      enabled: true,
      dangerouslyForceRunInProduction: true,
      showToolbar: true,
    });
  });

  test('shadow DOM root is created', async ({ page }) => {
    await page.waitForTimeout(1000);
    expect(await hasShadowRoot(page)).toBe(true);
  });

  test('toolbar has content in shadow DOM', async ({ page }) => {
    await page.waitForTimeout(1000);
    const childCount = await page.evaluate(() => {
      const root = document.getElementById('react-scan-root');
      return root?.shadowRoot?.children.length ?? 0;
    });
    expect(childCount).toBeGreaterThan(0);
  });

  test('toolbar persists across interactions', async ({ page }) => {
    await page.click('[data-testid="increment"]');
    await page.waitForTimeout(500);

    const active = await isReactScanActive(page);
    expect(active).toBe(true);

    const options = await page.evaluate(() => {
      return (window as any).__REACT_SCAN__?.ReactScanInternals?.options?.value?.enabled;
    });
    expect(options).toBe(true);
  });
});


================================================
FILE: kitchen-sink/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>React Scan</title>
    <meta name="title" content="React Scan" />
    <meta
      name="description"
      content="React Scan automatically detects and highlights components that cause performance issues in your React app. Drop it in anywhere – script tag, npm, you name it!"
    />

    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://react-scan.million.dev" />
    <meta property="og:title" content="React Scan" />
    <meta
      property="og:description"
      content="React Scan automatically detects and highlights components that cause performance issues in your React app. Drop it in anywhere – script tag, npm, you name it!"
    />
    <meta
      property="og:image"
      content="https://react-scan.million.dev/banner.png"
    />

    <meta property="twitter:card" content="summary_large_image" />
    <meta property="twitter:url" content="https://react-scan.million.dev" />
    <meta property="twitter:title" content="React Scan" />
    <meta
      property="twitter:description"
      content="React Scan automatically detects and highlights components that cause performance issues in your React app. Drop it in anywhere – script tag, npm, you name it!"
    />
    <meta
      property="twitter:image"
      content="https://react-scan.million.dev/banner.png"
    />

    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap"
      rel="stylesheet"
    />

    <link rel="icon" href="/logo.svg" type="image/svg+xml" />

    <meta
      name="keywords"
      content="react, performance, debugging, developer tools, web development, javascript"
    />
    <meta name="author" content="Aiden Bai" />
    <meta name="theme-color" content="#8b5cf6" />
    <link rel="canonical" href="https://react-scan.million.dev" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: kitchen-sink/package.json
================================================
{
  "name": "@react-scan/kitchen-sink",
  "type": "module",
  "private": true,
  "publishConfig": {
    "access": "restricted"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-scan": "workspace:*",
    "tailwindcss": "^3.4.17"
  },
  "devDependencies": {
    "@types/react": "^19.0.8",
    "@types/react-dom": "^19.0.3",
    "@vitejs/plugin-react": "^4.3.4",
    "postcss": "^8.5.3",
    "typescript": "^5.7.3",
    "vite": "^6.3.0"
  }
}


================================================
FILE: kitchen-sink/postcss.config.mjs
================================================
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};


================================================
FILE: kitchen-sink/src/examples/e2e-fixture/index.tsx
================================================
import { useState, useContext, createContext, memo } from 'react';
import { scan, Store } from 'react-scan';

Store.isInIframe.value = false;
scan({
  enabled: true,
  dangerouslyForceRunInProduction: true,
});

const ThemeContext = createContext('light');

function Counter(): JSX.Element {
  const [count, setCount] = useState(0);
  return (
    <div data-testid="counter">
      <span data-testid="count">{count}</span>
      <button data-testid="increment" type="button" onClick={() => setCount((c) => c + 1)}>
        Increment
      </button>
    </div>
  );
}

function UnstableProps(): JSX.Element {
  const [tick, setTick] = useState(0);
  return (
    <div data-testid="unstable-section">
      <button data-testid="trigger-unstable" type="button" onClick={() => setTick((t) => t + 1)}>
        Trigger ({tick})
      </button>
      <MemoChild style={{ color: 'red' }} onClick={() => {}} label="unstable" />
    </div>
  );
}

const MemoChild = memo(function MemoChild({
  style,
  onClick,
  label,
}: {
  style: { color: string };
  onClick: () => void;
  label: string;
}): JSX.Element {
  return (
    <div data-testid="memo-child" style={style} onClick={onClick}>
      MemoChild: {label}
    </div>
  );
});

function ContextConsumer(): JSX.Element {
  const theme = useContext(ThemeContext);
  return <div data-testid="context-value">Theme: {theme}</div>;
}

function ThemeToggle(): JSX.Element {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <div data-testid="theme-section">
        <button
          data-testid="toggle-theme"
          type="button"
          onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
        >
          Toggle Theme
        </button>
        <ContextConsumer />
      </div>
    </ThemeContext.Provider>
  );
}

function SlowComponent(): JSX.Element {
  const [rendering, setRendering] = useState(false);

  const triggerSlowRender = () => {
    setRendering(true);
    const start = performance.now();
    while (performance.now() - start < 100) {
      // block for 100ms to simulate slow render
    }
    setRendering(false);
  };

  return (
    <div data-testid="slow-section">
      <button data-testid="trigger-slow" type="button" onClick={triggerSlowRender}>
        Trigger Slow Render
      </button>
      <span data-testid="slow-status">{rendering ? 'Rendering...' : 'Idle'}</span>
    </div>
  );
}

function RapidUpdater(): JSX.Element {
  const [count, setCount] = useState(0);

  const triggerRapid = () => {
    for (let i = 0; i < 50; i++) {
      setTimeout(() => setCount((c) => c + 1), i * 16);
    }
  };

  return (
    <div data-testid="rapid-section">
      <button data-testid="trigger-rapid" type="button" onClick={triggerRapid}>
        Rapid Updates
      </button>
      <span data-testid="rapid-count">{count}</span>
    </div>
  );
}

export default function E2EFixture(): JSX.Element {
  return (
    <div style={{ padding: 24, fontFamily: 'sans-serif' }}>
      <h1 data-testid="heading">React Scan E2E Fixture</h1>
      <hr />
      <section>
        <h2>Counter</h2>
        <Counter />
      </section>
      <hr />
      <section>
        <h2>Unstable Props (memo bypass)</h2>
        <UnstableProps />
      </section>
      <hr />
      <section>
        <h2>Context</h2>
        <ThemeToggle />
      </section>
      <hr />
      <section>
        <h2>Slow Render</h2>
        <SlowComponent />
      </section>
      <hr />
      <section>
        <h2>Rapid Updates</h2>
        <RapidUpdater />
      </section>
    </div>
  );
}


================================================
FILE: kitchen-sink/src/examples/sierpinski/index.tsx
================================================
/**
 *  Modified version of https://github.com/ryansolid/solid-sierpinski-triangle-demo
 **/
// import { Analytics } from '@vercel/analytics/react';
import { useEffect, useMemo, useState } from 'react';
import { scan, Store } from 'react-scan';

import './styles.css';


Store.isInIframe.value = false;
scan({
  enabled: true,
  dangerouslyForceRunInProduction: true,
});

const TARGET = 50;

const TriangleDemo = () => {
  const [elapsed, setElapsed] = useState(0);
  const [seconds, setSeconds] = useState(0);
  const scale = useMemo(() => {
    const e = (elapsed / 1000) % 10;
    return 1 + (e > 5 ? 10 - e : e) / 10;
  }, [elapsed]);

  useEffect(() => {
    const t = setInterval(() => setSeconds((s) => (s % 10) + 1), 1000);

    let f: number;
    const start = Date.now();
    const update = () => {
      setElapsed(Date.now() - start);
      f = requestAnimationFrame(update);
    };
    f = requestAnimationFrame(update);

    return () => {
      clearInterval(t);
      cancelAnimationFrame(f);
    };
  }, []);

  return (
    <div
      className="container"
      style={{
        transform: 'scaleX(' + scale / 2.1 + ') scaleY(0.7) translateZ(0.1px)',
      }}
    >
      <Triangle x={0} y={0} s={1000} seconds={seconds} />
    </div>
  );
};

interface SlowTriangleProps {
  x: number;
  y: number;
  s: number;
  seconds: number;
}

const SlowTriangle = ({ x, y, s, seconds }: SlowTriangleProps) => {
  s = s / 2;

  const slow = useMemo(() => {
    const e = performance.now() + 0.8;
    // Artificially long execution time.
    while (performance.now() < e) {}
    return seconds;
  }, [seconds]);

  return (
    <>
      <Triangle x={x} y={y - s / 2} s={s} seconds={slow} />
      <Triangle x={x - s} y={y + s / 2} s={s} seconds={slow} />
      <Triangle x={x + s} y={y + s / 2} s={s} seconds={slow} />
    </>
  );
};

interface TriangleProps {

  x: number;
  y: number;
  s: number;
  seconds: number;
}

const Triangle = ({ x, y, s, seconds }: TriangleProps) => {
  if (s <= TARGET) {
    return (
      <Dot x={x - TARGET / 2} y={y - TARGET / 2} s={TARGET} text={seconds} />
    );
  }
  return <SlowTriangle x={x} y={y} s={s} seconds={seconds} />;
};

interface DotProps {
  x: number;
  y: number;
  s: number;
  text: number;
}

const Dot = ({ x, y, s, text }: DotProps) => {
  const [hover, setHover] = useState(false);
  const onEnter = () => setHover(true);
  const onExit = () => setHover(false);

  return (
    <div
      className="dot"
      style={{
        width: s + 'px',
        height: s + 'px',
        left: x + 'px',
        top: y + 'px',
        borderRadius: s / 2 + 'px',
        lineHeight: s + 'px',
        background: hover ? '#ff0' : '#61dafb',
      }}
      onMouseEnter={onEnter}
      onMouseLeave={onExit}
    >
      {hover ? '**' + text + '**' : text}
    </div>
  );
};

export default function App(): JSX.Element {
  return (
    <>
      {/* <Analytics /> */}
      <TriangleDemo />
    </>
  );
}


================================================
FILE: kitchen-sink/src/examples/sierpinski/styles.css
================================================
body {
  background: #fff;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 15px;
  line-height: 1.7;
  margin: 0;
  padding: 30px;
}

a {
  color: #4183c4;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

code {
  background-color: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 3px;
  font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
  font-size: 12px;
  margin: 0 2px;
  padding: 0px 5px;
}

h1, h2, h3, h4 {
  font-weight: bold;
  margin: 0 0 15px;
  padding: 0;
}

h1 {
  border-bottom: 1px solid #ddd;
  font-size: 2.5em;
  font-weight: bold;
  margin: 0 0 15px;
  padding: 0;
}

h2 {
  border-bottom: 1px solid #eee;
  font-size: 2em;
}

h3 {
  font-size: 1.5em;
}

h4 {
  font-size: 1.2em;
}

p, ul {
  margin: 15px 0;
}

ul {
  padding-left: 30px;
}

.container {
  position: absolute;
  transform-origin: 0 0;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 10px;
  background: #eee;
}

.dot {
  position: absolute;
  font: normal 15px sans-serif;
  text-align: center;
  cursor: pointer;
}

================================================
FILE: kitchen-sink/src/examples/todo-list/index.tsx
================================================
import { useState } from 'react';
import { scan, Store } from 'react-scan';
import './styles.css';


Store.isInIframe.value = false;
scan({
  enabled: true,
  dangerouslyForceRunInProduction: true,
});

interface TodoItem {
  id: number;
  message: string;
  done: boolean;
}

interface TodoListItemProps {
  setList: (action: (list: TodoItem[]) => TodoItem[]) => void;
  item: TodoItem;
}

function TodoListItem({ item, setList }: TodoListItemProps): JSX.Element {
  return (
    <div className={`todo-item ${item.done ? 'complete' : 'pending'}`}>
      <div className="todo-item-content">{item.message}</div>
      <div className="todo-item-actions">
        <button
          type="button"
          className={`todo-item-toggle ${item.done ? 'complete' : 'pending'}`}
          onClick={(): void => {
            setList(list =>
              list.map(value => {
                if (value === item) {
                  return {
                    ...value,
                    done: !item.done,
                  };
                }
                return value;
              }),
            );
          }}
        >
          {item.done ? 'Completed' : 'Pending'}
        </button>
        <button
          type="button"
          className="todo-item-delete"
          onClick={(): void => {
            setList(list => list.filter(value => value.id !== item.id));
          }}
        >
          Delete
        </button>
      </div>
    </div>
  );
}

interface TodoListFormProps {
  index: number;
  setIndex: (update: number) => void;
  setList: (action: (list: TodoItem[]) => TodoItem[]) => void;
}

function TodoListForm({
  setList,
  index,
  setIndex,
}: TodoListFormProps): JSX.Element {
  const [message, setMessage] = useState('');

  return (
    <form
      className="todo-list-form"
      onSubmit={(e): void => {
        e.preventDefault();

        setList(list => [
          ...list,
          {
            done: false,
            message,
            id: index,
          },
        ]);
        setIndex(index + 1);
        setMessage('');
      }}
    >
      <input
        type="text"
        value={message}
        onInput={(e): void => {
          setMessage((e.target as HTMLInputElement).value);
        }}
      />
      <button type="submit" disabled={message === ''}>
        Add
      </button>
    </form>
  );
}

function TodoList(): JSX.Element {
  const [list, setList] = useState<TodoItem[]>([]);
  const [index, setIndex] = useState(0);
  return (
    <>
      <TodoListForm setList={setList} index={index} setIndex={setIndex} />
      <div className="todo-list">
        {list.map(item => (
          <TodoListItem key={item.id} item={item} setList={setList} />
        ))}
      </div>
    </>
  );
}

export default function App(): JSX.Element {
  return (
    <div className="app">
      <h1>Todo List</h1>
      <TodoList />
    </div>
  );
}


================================================
FILE: kitchen-sink/src/examples/todo-list/styles.css
================================================
* {
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

button {
  margin: 0.5rem;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  outline: none;
  color: white;
  border-style: none;
  cursor: pointer;
  transition: background-color 200ms;
}

button:disabled {
  background-color: rgba(156, 163, 175, 255);
  color: rgba(229, 231, 235, 255);
}

.app {
  margin: 5%;
}

.todo-list-form {
  width: 100%;

  display: flex;
  align-items: center;
  justify-content: space-between;
}

.todo-list-form input {
  width: 100%;
  margin: 0.5rem;
  padding: 0.5rem;
  border-radius: 0.5rem;
  outline: none;
  border-width: 1px;
  border-color: black;
  border-style: solid;
}

.todo-list-form button:not(:disabled) {
  background-color: rgba(99, 102, 241, 255);
}

.todo-list-form button:not(:disabled):hover {
  background-color: rgba(67, 56, 202, 255);
}

.todo-list-form button:not(:disabled):active {
  background-color: rgba(79, 70, 229, 255);
}

.todo-item-toggle.pending {
  background-color: rgba(245, 158, 11, 255);
}

.todo-item-toggle.pending:hover {
  background-color: rgba(180, 83, 9, 255);
}

.todo-item-toggle.pending:active {
  background-color: rgba(217, 119, 6, 255);
}

.todo-item-toggle.complete {
  background-color: rgba(16, 185, 129, 255);
}

.todo-item-toggle.complete:hover {
  background-color: rgba(4, 120, 87, 255);
}

.todo-item-toggle.complete:active {
  background-color: rgba(5, 150, 105, 255);
}

.todo-item-delete:not(:disabled) {
  background-color: rgba(239, 68, 68, 255);
}

.todo-item-delete:not(:disabled):hover {
  background-color: rgba(185, 28, 28, 255);
}

.todo-item-delete:not(:disabled):active {
  background-color: rgba(220, 38, 38, 255);
}

.todo-item {
  margin: 0.5rem;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;

  display: flex;
  align-items: center;
  justify-content: space-between;
  transition: background-color 200ms;
}

.todo-item.loading {
  background-color: rgba(17, 24, 39, 255);
  color: white;
}


.todo-item.complete {
  background-color: rgba(209, 250, 229, 255);
}

.todo-item.pending {
  background-color: rgba(254, 243, 199, 255);
}


================================================
FILE: kitchen-sink/src/index.css
================================================
body {
  margin: 0;
}


================================================
FILE: kitchen-sink/src/index.tsx
================================================
import 'react-scan';

import { FC, lazy } from 'react';
import { createRoot } from 'react-dom/client';
import Home from './main';

import './index.css';

const examples = import.meta.glob<false, string, { default: FC<unknown> }>(
  './examples/**/index.tsx',
  {
    eager: false,
  },
);

const root = document.getElementById('root');

if (root) {
  const embedded = new URLSearchParams(window.location.search);
  const page = embedded.get('example');
  const target = `./examples/${page}/index.tsx`;
  if (page && target in examples) {
    const App = lazy(examples[target]);
    createRoot(root).render(<App />);
  } else {
    createRoot(root).render(<Home />);
  }
}


================================================
FILE: kitchen-sink/src/main.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  overflow: hidden;
}


================================================
FILE: kitchen-sink/src/main.tsx
================================================
import { type JSX, useState } from 'react';
import './main.css';

interface Example {
  title: string;
  url: string;
}

const examples: Example[] = [
  { title: 'Sierpinski Triangle', url: '/?example=sierpinski' },
  { title: 'Todo List', url: '/?example=todo-list' },
];

export default function Home(): JSX.Element {
  const [example, setExample] = useState(0);

  return (
    <div className="flex flex-col w-screen h-screen">
      <div className="flex flex-none border-b border-gray-950">
        <h1 className="m-8 font-bold text-3xl">react-scan</h1>
      </div>
      <div className="flex flex-1">
        {/* content */}
        <div className="flex flex-none flex-col border-r border-gray-950">
          {/* sidebar */}
          {examples.map((item, index) => (
            <button
              key={item.url}
              className="px-8 py-4 border-b border-gray-950"
              type="button"
              onClick={() => setExample(index)}
            >
              {item.title}
            </button>
          ))}
        </div>
        <div className="flex-1 flex items-center justify-center">
          {/* iframe */}
          <iframe className="flex-1 h-full" src={examples[example].url} />
        </div>
      </div>
    </div>
  );
}


================================================
FILE: kitchen-sink/tailwind.config.mjs
================================================
export default {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
};


================================================
FILE: kitchen-sink/tsconfig.json
================================================
{
  "exclude": ["node_modules"],
  "include": ["src", "types"],
  "compilerOptions": {
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "types": ["vite/client"],
    "importHelpers": true,
    "declaration": true,
    "sourceMap": true,
    "rootDir": "./src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "esModuleInterop": true,
    "target": "ES2017"
  }
}


================================================
FILE: kitchen-sink/vite.config.ts
================================================
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  css: {
    modules: {
      localsConvention: 'camelCaseOnly',
    },
  },
});


================================================
FILE: package.json
================================================
{
  "name": "root",
  "private": true,
  "scripts": {
    "build": "node scripts/workspace.mjs build",
    "postbuild": "node scripts/version-warning.mjs",
    "dev": "node scripts/workspace.mjs dev",
    "pack": "node scripts/workspace.mjs pack",
    "pack:bump": "pnpm --filter scan pack:bump",
    "lint": "pnpm -r lint",
    "lint:all": "oxlint .",
    "changeset": "changeset add",
    "version": "changeset version",
    "release": "changeset publish",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.12",
    "@playwright/test": "^1.58.2",
    "@types/node": "^22.10.2",
    "autoprefixer": "^10.4.20",
    "boxen": "^8.0.1",
    "chalk": "^5.3.0",
    "oxlint": "latest",
    "postcss": "^8.5.3",
    "rimraf": "^6.0.1",
    "tailwindcss": "^3.4.17",
    "typescript": "latest",
    "vite-tsconfig-paths": "^5.1.4"
  },
  "packageManager": "pnpm@9.1.0",
  "dependencies": {
    "@vercel/speed-insights": "^1.1.0"
  },
  "pnpm": {
    "overrides": {
      "@jridgewell/gen-mapping": "0.3.2",
      "@jridgewell/sourcemap-codec": "1.4.15"
    }
  }
}


================================================
FILE: packages/extension/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.DS_Store
.idea
*.suo
*.ntvs*
*.njsproj
*.sw?
*.sln

# Config files
.webextrc
.webextrc.*


================================================
FILE: packages/extension/CHANGELOG.md
================================================
# @react-scan/extension

## 0.5.3

### Patch Changes

- fix
- Updated dependencies
  - react-scan@0.5.3

## 0.5.2

### Patch Changes

- fix
- Updated dependencies
  - react-scan@0.5.2

## 0.5.1

### Patch Changes

- fix: infinite mounting
- Updated dependencies
  - react-scan@0.5.1

## 0.5.0

### Minor Changes

- cleanup

### Patch Changes

- Updated dependencies
- Updated dependencies [9d38ffe]
  - react-scan@0.5.0


================================================
FILE: packages/extension/README.md
================================================
# React Scanner Extension

Browser extension for scanning React applications and identifying performance issues.


### Environment Variables

When developing with Brave, you need to set the `BRAVE_BINARY` environment variable. Create a `.env` file (copy from `.env.example`):

```env
# For macOS
BRAVE_BINARY="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"

# For Windows
BRAVE_BINARY="C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"

# For Linux
BRAVE_BINARY="/usr/bin/brave"
```

### Development Setup
#### For Chrome
1. Run development server:
   ```bash
   pnpm dev
   ```
3. This will automatically open Chrome with the extension loaded.

<i>If you need to inspect the extension, open `chrome://extensions` in Chrome</i>
#### For Firefox

<br />

#### For Firefox
1. Run development server:
   ```bash
   pnpm dev:firefox
   ```
2. This will automatically open Firefox with the extension loaded.

<i>If you need to inspect the extension, open `about:debugging#/runtime/this-firefox` in Firefox</i>

<br />

#### For Brave

1. Run development server:
   ```bash
   pnpm dev:brave
   ```

2. This will automatically open Brave with the extension loaded.

<i>If you need to inspect the extension, open `brave://extensions` in Brave</i>

<br />

### Building for Production

To build the extension for all browsers:

```bash
pnpm pack:all
```

This will create:
- `chrome-extension-v1.0.8.zip`
- `firefox-extension-v1.0.8.zip`
- `brave-extension-v1.0.8.zip`

in the `build` directory.


================================================
FILE: packages/extension/package.json
================================================
{
  "name": "@react-scan/extension",
  "version": "0.5.3",
  "private": true,
  "type": "module",
  "scripts": {
    "clean": "rimraf dist",
    "build": "vite build",
    "postbuild": "node ../../scripts/version-warning.mjs",
    "dev": "pnpm dev:chrome",
    "dev:chrome": "cross-env BROWSER=chrome vite",
    "dev:firefox": "cross-env BROWSER=firefox vite",
    "dev:brave": "cross-env BROWSER=brave vite",
    "mkdir": "mkdir -p build",
    "pack:chrome": "pnpm clean && pnpm build && pnpm mkdir && cd dist && zip -r \"../build/chrome-extension-v$npm_package_version.zip\" .",
    "pack:firefox": "pnpm clean && BROWSER=firefox pnpm build && pnpm mkdir && cd dist && zip -r \"../build/firefox-extension-v$npm_package_version.zip\" .",
    "pack:brave": "pnpm clean && BROWSER=brave pnpm build && pnpm mkdir && cd dist && zip -r \"../build/brave-extension-v$npm_package_version.zip\" .",
    "pack:all": "rimraf build && pnpm pack:chrome && pnpm pack:firefox && pnpm pack:brave",
    "lint": "oxlint src && pnpm typecheck",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "bippy": "0.3.8",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scan": "workspace:*",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/chrome": "^0.0.281",
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.9",
    "@types/semver": "^7.5.8",
    "@types/webextension-polyfill": "^0.12.0",
    "@vitejs/plugin-react": "^4.2.1",
    "bestzip": "^2.2.1",
    "cross-env": "^7.0.3",
    "semver": "^7.7.1",
    "vite": "^6.3.0",
    "vite-plugin-web-extension": "^4.4.3",
    "vite-tsconfig-paths": "^5.1.4",
    "webextension-polyfill": "^0.12.0"
  }
}


================================================
FILE: packages/extension/src/assets/css/no-react.css
================================================
html.freeze > body {
  pointer-events: none;
}

html.freeze {
  overflow: auto;
  overscroll-behavior-x: contain;
}

html.freeze svg {
  pointer-events: none;
}

html.freeze #react-scan-toast {
  pointer-events: auto;
}

#react-scan-backdrop {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.01);
  backdrop-filter: blur(94px) saturate(180%);
  animation-duration: 300ms;
  opacity: 0;
  pointer-events: none;
  transition: opacity 500ms;
  z-index: 2147483650;
}

#react-scan-toast {
  position: fixed;
  top: 50%;
  left: 50%;
  display: flex;
  flex-direction: column;
  padding: 12px 40px 12px 16px;
  min-width: 320px;
  max-width: 480px;
  color: #fff;
  font-size: 12px;
  font-family: Menlo, Consolas, Monaco, 'Liberation Mono', 'Lucida Console', monospace;
  line-height: 1.5;
  background: rgba(0, 0, 0, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transform: translate(-50%, -50%);
  transition: opacity 300ms ease-in-out;
  z-index: 2147483651;
}

#react-scan-toast-title {
  margin: 0 0 8px 0;
}

#react-scan-toast-message {
  display: flex;
  align-items: flex-start;
}

#react-scan-toast-message span {
  white-space: pre-line;
}

#react-scan-toast .icon {
  font-size: .75rem;
  margin-right: 8px;
}

@media (max-width: 320px) {
  #react-scan-toast {
    flex-direction: column;
    min-width: auto;
    width: calc(100% - 36px);
  }
}

@media (max-width: 720px) {
  #react-scan-toast {
    br {
      display: none;
    }
  }
}

#react-scan-toast-close-button {
  position: absolute;
  top: 10px;
  right: 8px;
  display: flex;
  align-items: center;
  width: 23px;
  height: 23px;
  padding: 4px;
  border: none;
  border-radius: 4px;
  color: #fff;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.01);
  transition: background-color 300ms ease;
}

#react-scan-toast-close-button:hover {
  background-color: rgba(255, 255, 255, 0.15);
}


================================================
FILE: packages/extension/src/background/icon.ts
================================================
import browser from 'webextension-polyfill';

export enum IconState {
  DISABLED = 'disabled',
  ENABLED = 'enabled',
}

const browserAction = browser.action || browser.browserAction;

const cachedIcons = {
  [IconState.ENABLED]: {
    path: {
      16: browser.runtime.getURL('icons/enabled/16.png'),
      32: browser.runtime.getURL('icons/enabled/32.png'),
      48: browser.runtime.getURL('icons/enabled/48.png'),
      128: browser.runtime.getURL('icons/enabled/128.png'),
    },
  },
  [IconState.DISABLED]: {
    path: {
      16: browser.runtime.getURL('icons/disabled/16.png'),
      32: browser.runtime.getURL('icons/disabled/32.png'),
      48: browser.runtime.getURL('icons/disabled/48.png'),
      128: browser.runtime.getURL('icons/disabled/128.png'),
    },
  },
};

export const updateIconForTab = async (
  tab: browser.Tabs.Tab,
  state: IconState,
  badgeText = 'on',
): Promise<void> => {
  try {
    switch (state) {
      case IconState.ENABLED:
        await browserAction.setIcon({
          tabId: tab.id,
          path: cachedIcons[IconState.ENABLED].path,
        });
        if (badgeText) {
          await browserAction.setBadgeText({ text: badgeText, tabId: tab.id });
          await browserAction.setBadgeBackgroundColor({
            color: '#A295EE',
            tabId: tab.id,
          });
        }
        break;

      default:
        await browserAction.setIcon({
          tabId: tab.id,
          path: cachedIcons[IconState.DISABLED].path,
        });
        await browserAction.setBadgeText({ text: '', tabId: tab.id });
        break;
    }
  } catch {}
};


================================================
FILE: packages/extension/src/background/index.ts
================================================
import browser from 'webextension-polyfill';
import { isInternalUrl } from '~utils/helpers';
import { IconState, updateIconForTab } from './icon';
import { BroadcastMessage } from '~types/messages';

const browserAction = browser.action || browser.browserAction;

const injectScripts = async (tabId: number) => {
  try {
    await browser.scripting.executeScript({
      target: { tabId },
      files: ['src/content/index.js', 'src/inject/index.js'],
    });

    await browser.tabs.sendMessage(tabId, {
      type: 'react-scan:page-reload',
    });
  } catch (e) {
    // oxlint-disable-next-line no-console
    console.error('Script injection error:', e);
  }
};

const isScriptsLoaded = async (tabId: number): Promise<boolean> => {
  try {
    await browser.tabs.sendMessage(tabId, { type: 'react-scan:ping' });
    return true;
  } catch {
    return false;
  }
};

const init = async (tab: browser.Tabs.Tab) => {
  if (!tab.id || !tab.url || isInternalUrl(tab.url)) {
    if (tab.id) {
      await updateIconForTab(tab, IconState.DISABLED);
    }
    return;
  }

  const isLoaded = await isScriptsLoaded(tab.id);

  if (!isLoaded) {
    await injectScripts(tab.id);
  }

  if (!isLoaded) {
    await updateIconForTab(tab, IconState.DISABLED);
  }
};

browser.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    void init(tab);
  }
});

browser.tabs.onActivated.addListener(async ({ tabId }) => {
  const tab = await browser.tabs.get(tabId);
  void init(tab);
});

browser.windows.onFocusChanged.addListener(async (windowId) => {
  if (windowId !== browser.windows.WINDOW_ID_NONE) {
    const [tab] = await browser.tabs.query({ active: true, windowId });
    if (tab) {
      void init(tab);
    }
  }
});

browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => {
  if (tab) {
    void init(tab);
  }
});

browserAction.onClicked.addListener(async (tab) => {
  if (!tab.id || !tab.url || isInternalUrl(tab.url)) {
    if (tab.id) {
      await updateIconForTab(tab, IconState.DISABLED);
    }
    return;
  }

  try {
    await browser.tabs.sendMessage(tab.id, {
      type: 'react-scan:toggle-state',
    });

    await updateIconForTab(tab, IconState.DISABLED);
  } catch {
    if (tab.id) {
      await updateIconForTab(tab, IconState.DISABLED);
    }
  }
});

browser.runtime.onMessage.addListener(
  (message: unknown, sender: browser.Runtime.MessageSender) => {
    const msg = message as BroadcastMessage;
    if (!sender.tab?.id) return;
    if (msg.type === 'react-scan:is-enabled') {
      void updateIconForTab(
        sender.tab,
        msg.data?.state ? IconState.ENABLED : IconState.DISABLED,
      );
    }
  },
);


================================================
FILE: packages/extension/src/content/index.ts
================================================
import browser from 'webextension-polyfill';
import {
  type BroadcastMessage,
  BroadcastSchema,
  type IEvents,
} from '~types/messages';
import { busDispatch, busSubscribe } from '~utils/helpers';

chrome.runtime.onMessage.addListener(
  async (message: unknown, _sender, sendResponse) => {
    const result = BroadcastSchema.safeParse(message);
    if (!result.success) {
      return false;
    }

    const data = result.data;

    if (data.type === 'react-scan:ping') {
      sendResponse({ pong: true });
      return false;
    }

    if (data.type === 'react-scan:page-reload') {
      window.location.reload();
      return false;
    }

    if (data.type === 'react-scan:toggle-state') {
      busDispatch<IEvents['react-scan:toggle-state']>(
        'react-scan:toggle-state',
        {
          topic: 'react-scan:toggle-state',
          message: undefined,
        },
      );
      return false;
    }

    return false;
  },
);

const sendMessageToBackground = ({ type, data }: BroadcastMessage) => {
  try {
    return browser.runtime.sendMessage({ type, data });
  } catch {
    return Promise.resolve();
  }
};

busSubscribe<IEvents['react-scan:send-to-background']>(
  'react-scan:send-to-background',
  (event) => {
    sendMessageToBackground(event.message);
  },
);


================================================
FILE: packages/extension/src/inject/index.ts
================================================
import * as reactScan from 'react-scan';
import { gt } from 'semver';
import type { IEvents } from '~types/messages';
import { EXTENSION_STORAGE_KEY, STORAGE_KEY } from '~utils/constants';
import {
  busDispatch,
  busSubscribe,
  canLoadReactScan,
  hasReactFiber,
  readLocalStorage,
  saveLocalStorage,
  sleep,
  storageGetItem,
  storageSetItem,
} from '~utils/helpers';
import { createNotificationUI, toggleNotification } from './notification';

const reactScanExtensionVersion = 'version' in reactScan.ReactScanInternals ? (reactScan.ReactScanInternals as any).version  : undefined;
const isTargetPageAlreadyUsedReactScan = () => {
  const currentReactScanVersion = window.__REACT_SCAN_VERSION__;

  if (
    window.__REACT_SCAN__?.ReactScanInternals?.Store?.monitor?.value &&
    !currentReactScanVersion
  ) {
    return true;
  }

  if (!reactScanExtensionVersion || !currentReactScanVersion) {
    return false;
  }

  return gt(currentReactScanVersion, reactScanExtensionVersion);
};

const getInitialOptions = async (): Promise<reactScan.Options> => {
  const storedOptions = readLocalStorage<reactScan.Options>(STORAGE_KEY);
  let isEnabled = false;

  try {
    const storedEnabled = await storageGetItem<boolean>(
      EXTENSION_STORAGE_KEY,
      'isEnabled',
    );
    isEnabled = storedEnabled ?? false;
  } catch {}

  return {
    ...storedOptions,
    enabled: isEnabled,
    showToolbar: isEnabled,
    dangerouslyForceRunInProduction: true,
  };
};

const initializeReactScan = async () => {
  const options = await getInitialOptions();

  window.__REACT_SCAN_EXTENSION__ = true;
  if (options.enabled) {
    window.hideIntro = true;
    reactScan.scan(options);
    window.reactScan = undefined;
  }
};

let timer: number | undefined;
const updateReactScanState = async (isEnabled: boolean | null) => {
  clearTimeout(timer);
  const toggledState = isEnabled === null ? true : !isEnabled;

  try {
    await storageSetItem(EXTENSION_STORAGE_KEY, 'isEnabled', toggledState);
  } catch {}

  const storedOptions = readLocalStorage<reactScan.Options>(STORAGE_KEY) ?? {};
  const updatedOptions = {
    ...storedOptions,
    enabled: toggledState,
    showToolbar: toggledState,
    dangerouslyForceRunInProduction: true,
  };

  saveLocalStorage(STORAGE_KEY, updatedOptions);

  window.location.reload();
};

void initializeReactScan();

window.addEventListener('DOMContentLoaded', async () => {
  if (!canLoadReactScan) {
    return;
  }

  let isReactAvailable = false;

  await sleep(1000);
  isReactAvailable = await hasReactFiber();

  if (!isReactAvailable) {
    createNotificationUI({
      title: 'React Not Detected',
      content:
        "React is not detected on this page.\nPlease ensure you're visiting a React application.",
    });

    busDispatch<IEvents['react-scan:send-to-background']>(
      'react-scan:send-to-background',
      {
        topic: 'react-scan:send-to-background',
        message: {
          type: 'react-scan:is-enabled',
          data: {
            state: false,
          },
        },
      },
    );

    busSubscribe<IEvents['react-scan:toggle-state']>(
      'react-scan:toggle-state',
      async () => {
        toggleNotification();
      },
    );

    return;
  }

  if (isTargetPageAlreadyUsedReactScan()) {
    createNotificationUI({
      title: 'Already Initialized',
      content: 'React Scan is already initialized on this page.',
    });

    busDispatch<IEvents['react-scan:send-to-background']>(
      'react-scan:send-to-background',
      {
        topic: 'react-scan:send-to-background',
        message: {
          type: 'react-scan:is-enabled',
          data: {
            state: false,
          },
        },
      },
    );

    busSubscribe<IEvents['react-scan:toggle-state']>(
      'react-scan:toggle-state',
      async () => {
        toggleNotification();
      },
    );

    return;
  }

  const storedOptions = readLocalStorage<reactScan.Options>(STORAGE_KEY);
  if (storedOptions !== null) {
    busDispatch<IEvents['react-scan:send-to-background']>(
      'react-scan:send-to-background',
      {
        topic: 'react-scan:send-to-background',
        message: {
          type: 'react-scan:is-enabled',
          data: {
            state: storedOptions.showToolbar,
          },
        },
      },
    );
  }

  if (!isTargetPageAlreadyUsedReactScan()) {
    window.reactScan = reactScan.setOptions;
  }

  busSubscribe<IEvents['react-scan:toggle-state']>(
    'react-scan:toggle-state',
    async () => {
      if (!isReactAvailable || isTargetPageAlreadyUsedReactScan()) {
        toggleNotification();
        return;
      }

      try {
        const isEnabled = await storageGetItem<boolean>(
          EXTENSION_STORAGE_KEY,
          'isEnabled',
        );
        await updateReactScanState(!!isEnabled);
      } catch {
        await updateReactScanState(null);
      }
    },
  );
});


================================================
FILE: packages/extension/src/inject/notification.ts
================================================
import noReactStyles from '~assets/css/no-react.css?inline';
import type { IEvents } from '~types/messages';
import { busDispatch } from '~utils/helpers';

let backdrop: HTMLDivElement | null = null;
let isAnimating = false;

const defaultTitle = 'React Not Detected';
const defaultContent =
  "React is not detected on this page. \nPlease ensure you're visiting a React application!";

export const createNotificationUI = ({
  title = defaultTitle,
  content = defaultContent,
}) => {
  busDispatch<IEvents['react-scan:send-to-background']>(
    'react-scan:send-to-background',
    {
      topic: 'react-scan:send-to-background',
      message: {
        type: 'react-scan:is-enabled',
        data: {
          state: false,
        },
      },
    },
  );

  if (backdrop) {
    return;
  }

  backdrop = document.createElement('div');
  backdrop.id = 'react-scan-backdrop';
  backdrop.style.opacity = '0';
  backdrop.style.pointerEvents = 'none';

  const toast = document.createElement('div');
  toast.id = 'react-scan-toast';
  toast.onclick = (e) => {
    e.stopPropagation();
  };

  // Create title element
  const titleElement = document.createElement('div');
  titleElement.id = 'react-scan-toast-title';

  const icon = document.createElement('span');
  icon.className = 'icon';
  icon.textContent = '⚛️';
  titleElement.appendChild(icon);

  const titleText = document.createElement('span');
  titleText.textContent = title;
  titleElement.appendChild(titleText);

  toast.appendChild(titleElement);

  // Create message element
  const messageElement = document.createElement('div');
  messageElement.id = 'react-scan-toast-message';

  const text = document.createElement('span');
  text.textContent = content;
  text.style.whiteSpace = 'pre-line'; // Preserve line breaks
  messageElement.appendChild(text);

  toast.appendChild(messageElement);

  const button = document.createElement('button');
  button.id = 'react-scan-toast-close-button';
  button.type = 'button';
  button.onclick = toggleNotification;

  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('width', '15');
  svg.setAttribute('height', '15');
  svg.setAttribute('viewBox', '0 0 24 24');
  svg.setAttribute('fill', 'none');
  svg.setAttribute('stroke', 'currentColor');
  svg.setAttribute('stroke-width', '2');
  svg.setAttribute('stroke-linecap', 'round');
  svg.setAttribute('stroke-linejoin', 'round');

  const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  line1.setAttribute('x1', '18');
  line1.setAttribute('y1', '6');
  line1.setAttribute('x2', '6');
  line1.setAttribute('y2', '18');

  const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  line2.setAttribute('x1', '6');
  line2.setAttribute('y1', '6');
  line2.setAttribute('x2', '18');
  line2.setAttribute('y2', '18');

  svg.appendChild(line1);
  svg.appendChild(line2);
  button.appendChild(svg);

  toast.appendChild(button);

  backdrop.appendChild(toast);
  backdrop.onclick = toggleNotification;

  const style = document.createElement('style');
  style.id = 'react-scan-no-react-styles';
  style.appendChild(document.createTextNode(noReactStyles));

  const fragment = document.createDocumentFragment();
  fragment.appendChild(style);
  fragment.appendChild(backdrop);

  document.documentElement.appendChild(fragment);
};

export const toggleNotification = () => {
  if (!backdrop || isAnimating) return;
  isAnimating = true;

  const handleTransitionEnd = () => {
    isAnimating = false;
    backdrop?.removeEventListener('transitionend', handleTransitionEnd);
  };

  backdrop.addEventListener('transitionend', handleTransitionEnd);

  const isVisible = backdrop.style.opacity === '1';
  backdrop.style.opacity = isVisible ? '0' : '1';
  backdrop.style.pointerEvents = isVisible ? 'none' : 'auto';
  document.documentElement.classList.toggle('freeze', !isVisible);
};


================================================
FILE: packages/extension/src/inject/react-scan.ts
================================================
// Bippy has a side-effect that installs the hook.
import 'bippy';


================================================
FILE: packages/extension/src/manifest.chrome.json
================================================
{
  "manifest_version": 3,
  "name": "React Scan",
  "version": "0.0.0",
  "description": "Scan React apps for performance problems",
  "icons": {
    "16": "icons/disabled/16.png",
    "32": "icons/disabled/32.png",
    "48": "icons/disabled/48.png",
    "128": "icons/disabled/128.png"
  },
  "action": {
    "default_icon": {
      "16": "icons/disabled/16.png",
      "32": "icons/disabled/32.png",
      "48": "icons/disabled/48.png",
      "128": "icons/disabled/128.png"
    }
  },
  "background": {
    "service_worker": "src/background/index.ts"
  },
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  },
  "permissions": ["activeTab", "tabs", "scripting"],
  "host_permissions": ["<all_urls>"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["src/inject/react-scan.ts"],
      "run_at": "document_start",
      "world": "MAIN"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["src/inject/index.ts"],
      "run_at": "document_start",
      "world": "MAIN"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["src/content/index.ts"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["icons/*"],
      "matches": ["<all_urls>"]
    }
  ]
}


================================================
FILE: packages/extension/src/manifest.firefox.json
================================================
{
  "manifest_version": 2,
  "name": "React Scan",
  "version": "0.0.0",
  "description": "Scan React apps for performance problems",
  "icons": {
    "16": "icons/disabled/16.png",
    "32": "icons/disabled/32.png",
    "48": "icons/disabled/48.png",
    "128": "icons/disabled/128.png"
  },
  "browser_specific_settings": {
    "gecko": {
      "id": "react-scan@million.dev",
      "strict_min_version": "57.0"
    }
  },
  "browser_action": {
    "default_icon": {
      "16": "icons/disabled/16.png",
      "32": "icons/disabled/32.png",
      "48": "icons/disabled/48.png",
      "128": "icons/disabled/128.png"
    }
  },
  "background": {
    "scripts": ["src/background/index.ts"]
  },
  "permissions": ["activeTab", "tabs", "scripting", "<all_urls>"],
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["src/inject/react-scan.ts"],
      "run_at": "document_start"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["src/inject/index.ts"],
      "run_at": "document_start"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["src/content/index.ts"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": ["icons/*"]
}


================================================
FILE: packages/extension/src/types/global.d.ts
================================================
import type * as reactScan from 'react-scan';

declare global {
  interface Window {
    __REACT_SCAN__?: {
      ReactScanInternals: {
        version: string;
        Store: {
          monitor: {
            value: boolean;
          };
        };
      };
    };
    __REACT_SCAN_EXTENSION__?: boolean;
    __REACT_SCAN_VERSION__?: string;
    __REACT_DEVTOOLS_GLOBAL_HOOK__?: {
      checkDCE: (fn: unknown) => void;
      supportsFiber: boolean;
      supportsFlight: boolean;
      renderers: Map<number, ReactRenderer>;
      hasUnsupportedRendererAttached: boolean;
      onCommitFiberRoot: (
        rendererID: number,
        root: FiberRoot,
        priority: void | number,
      ) => void;
      onCommitFiberUnmount: (rendererID: number, fiber: Fiber) => void;
      onPostCommitFiberRoot: (rendererID: number, root: FiberRoot) => void;
      inject: (renderer: ReactRenderer) => number;
      _instrumentationSource?: string;
      _instrumentationIsActive?: boolean;
    };
    hideIntro: boolean;
    reactScan: typeof reactScan.setOptions | undefined;
  }
}


================================================
FILE: packages/extension/src/types/messages.ts
================================================
import { z } from 'zod';

export const BroadcastSchema = z.object({
  type: z.enum([
    'react-scan:ping',
    'react-scan:is-enabled',
    'react-scan:toggle-state',
    'react-scan:page-reload',
  ]),
  data: z.any().optional(),
});

export type BroadcastMessage = z.infer<typeof BroadcastSchema>;

export interface IEvents {
  'react-scan:toggle-state': {
    topic: 'react-scan:toggle-state';
    message: undefined;
  };
  'react-scan:send-to-background': {
    topic: 'react-scan:send-to-background';
    message: BroadcastMessage;
  };
}


================================================
FILE: packages/extension/src/utils/constants.ts
================================================
export const STORAGE_KEY = 'react-scan-options';
export const EXTENSION_STORAGE_KEY = 'react-scan-extension';


================================================
FILE: packages/extension/src/utils/helpers.ts
================================================
export const isIframe = window !== window.top;
export const isPopup = window.opener !== null;
export const canLoadReactScan = !isIframe && !isPopup;

export const IS_CLIENT = typeof window !== 'undefined';

export const isInternalUrl = (url: string): boolean => {
  if (!url) return false;

  const allowedProtocols = ['http:', 'https:', 'file:'];
  return !allowedProtocols.includes(new URL(url).protocol);
};

interface ReactRootContainer {
  _reactRootContainer?: {
    _internalRoot?: {
      current?: {
        child: unknown;
      };
    };
  };
  __reactContainer$?: unknown;
}

const ReactDetection = {
  limits: {
    MAX_DEPTH: 10,
    MAX_ELEMENTS: 30,
    ELEMENTS_PER_LEVEL: 5
  },
  nonVisualTags: new Set([
    // Document level
    'HTML', 'HEAD', 'META', 'TITLE', 'BASE',
    // Scripts and styles
    'SCRIPT', 'STYLE', 'LINK', 'NOSCRIPT',
    // Media and embeds
    'SOURCE', 'TRACK', 'EMBED', 'OBJECT', 'PARAM',
    // Special elements
    'TEMPLATE', 'PORTAL', 'SLOT',
    // Others
    'AREA', 'XML', 'DOCTYPE', 'COMMENT'
  ]),
  reactMarkers: {
    root: '_reactRootContainer',
    fiber: '__reactFiber',
    instance: '__reactInternalInstance$',
    container: '__reactContainer$'
  }
} as const;

const childrenCache = new WeakMap<Element, Element[]>();

export const hasReactFiber = (): boolean => {
  const rootElement = document.body;
  let elementsChecked = 0;

  const getChildren = (element: Element): Element[] => {
    let children = childrenCache.get(element);
    if (!children) {
      const childNodes = element.children;
      children = [];
      for (let i = 0; i < childNodes.length; i++) {
        const child = childNodes[i];
        if (!ReactDetection.nonVisualTags.has(child.tagName)) {
          children.push(child);
        }
      }
      childrenCache.set(element, children);
    }
    return children;
  };

  const checkElement = (element: Element, depth: number): boolean => {
    if (elementsChecked >= ReactDetection.limits.MAX_ELEMENTS) return false;
    elementsChecked++;

    const props = Object.getOwnPropertyNames(element);

    if (ReactDetection.reactMarkers.root in element) {
      const elementWithRoot = element as unknown as ReactRootContainer;
      const rootContainer = elementWithRoot._reactRootContainer;

      const hasLegacyRoot = rootContainer?._internalRoot?.current?.child != null;
      const hasContainerRoot = Object.keys(elementWithRoot).some(key =>
        key.startsWith(ReactDetection.reactMarkers.container)
      );

      return hasLegacyRoot || hasContainerRoot;
    }

    for (const key of props) {
      if (
        key.startsWith(ReactDetection.reactMarkers.fiber) ||
        key.startsWith(ReactDetection.reactMarkers.instance)
      ) {
        return true;
      }
    }

    if (depth < ReactDetection.limits.MAX_DEPTH) {
      const children = getChildren(element);
      const maxCheck = Math.min(children.length, ReactDetection.limits.ELEMENTS_PER_LEVEL);

      for (let i = 0; i < maxCheck; i++) {
        if (checkElement(children[i], depth + 1)) {
          return true;
        }
      }
    }

    return false;
  };

  return checkElement(rootElement, 0);
};

export const readLocalStorage = <T>(storageKey: string): T | null => {
  if (!IS_CLIENT) return null;

  try {
    const stored = localStorage.getItem(storageKey);
    return stored ? JSON.parse(stored) : null;
  } catch {
    return null;
  }
};

export const saveLocalStorage = <T>(storageKey: string, state: T): void => {
  if (!IS_CLIENT) return;

  try {
    window.localStorage.setItem(storageKey, JSON.stringify(state));
  } catch {}
};

export const removeLocalStorage = (storageKey: string): void => {
  if (!IS_CLIENT) return;

  try {
    window.localStorage.removeItem(storageKey);
  } catch {}
};

export const debounce = <T extends (enabled: boolean | null) => Promise<void>>(
  fn: T,
  wait: number,
  options: { leading?: boolean; trailing?: boolean } = {},
) => {
  let timeoutId: number | undefined;
  let lastArg: boolean | null | undefined;
  let isLeadingInvoked = false;

  const debounced = (enabled: boolean | null) => {
    lastArg = enabled;

    if (options.leading && !isLeadingInvoked) {
      isLeadingInvoked = true;
      fn(enabled);
      return;
    }

    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }

    if (options.trailing !== false) {
      timeoutId = setTimeout(() => {
        isLeadingInvoked = false;
        timeoutId = undefined;
        if (lastArg !== undefined) {
          fn(lastArg);
        }
      }, wait);
    }
  };

  debounced.cancel = () => {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
      timeoutId = undefined;
      isLeadingInvoked = false;
      lastArg = undefined;
    }
  };

  return debounced;
};

type EventCallback<T = unknown> = (data: T) => void;
const eventBus = new Map<string, Set<EventCallback>>();

export const busSubscribe = <T = unknown>(
  event: string,
  callback: EventCallback<T>,
): (() => void) => {
  if (!eventBus.has(event)) {
    eventBus.set(event, new Set());
  }
  eventBus.get(event)!.add(callback as EventCallback);

  return () => {
    const callbacks = eventBus.get(event);
    if (callbacks) {
      callbacks.delete(callback as EventCallback);
      if (callbacks.size === 0) {
        eventBus.delete(event);
      }
    }
  };
};

export const busDispatch = <T = unknown>(event: string, data: T): void => {
  const callbacks = eventBus.get(event);
  if (callbacks) {
    callbacks.forEach((callback) => callback(data));
  }
};

export const sleep = (ms: number): Promise<void> => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};

export const storageGetItem = async <T>(
  storageKey: string,
  key: string,
): Promise<T | null> => {
  try {
    const result = await chrome.storage.local.get(storageKey);
    const data = result[storageKey];
    return data?.[key] ?? null;
  } catch {
    return null;
  }
};

export const storageSetItem = async <T>(
  storageKey: string,
  key: string,
  value: T,
): Promise<void> => {
  try {
    const result = await chrome.storage.local.get(storageKey);
    const data = result[storageKey] || {};
    data[key] = value;
    await chrome.storage.local.set({ [storageKey]: data });
  } catch {
  }
};


================================================
FILE: packages/extension/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: packages/extension/tsconfig.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "paths": {
      "react-scan": [
        "../scan/dist"
      ],
      "~utils/*": [
        "./src/utils/*"
      ],
      "~types/*": [
        "./src/types/*"
      ],
      "~assets/*": [
        "./src/assets/*"
      ]
    },
    "types": [
      "chrome"
    ]
  },
  "include": [
    "src"
  ],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: packages/extension/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: packages/extension/vite.config.ts
================================================
import react from '@vitejs/plugin-react';
import { type UserConfig, defineConfig, loadEnv } from 'vite';
import webExtension, { readJsonFile } from 'vite-plugin-web-extension';
import tsconfigPaths from 'vite-tsconfig-paths';

// Browser types
const BROWSER_TYPES = {
  CHROME: 'chrome',
  FIREFOX: 'firefox',
  BRAVE: 'brave',
} as const;

type BrowserType = (typeof BROWSER_TYPES)[keyof typeof BROWSER_TYPES];

export default defineConfig(({ mode }): UserConfig => {
  const env = loadEnv(mode, process.cwd(), '');
  const browser = (env.BROWSER || BROWSER_TYPES.CHROME) as BrowserType;

  const isBrave = browser === BROWSER_TYPES.BRAVE;

  // Validate Brave binary
  if (env.NODE_ENV === 'development' && isBrave && !env.BRAVE_BINARY) {
    // oxlint-disable-next-line no-console
    console.error(`
    ⚛️  React Scan
    ==============
    🚫 Error: BRAVE_BINARY environment variable is missing

    This is required for Brave browser development.
    Please check .env.example and set up your .env file with the correct path:

    📍 For macOS:
       BRAVE_BINARY="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"

    📍 For Windows:
       BRAVE_BINARY="C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"

    📍 For Linux:
       BRAVE_BINARY="/usr/bin/brave"
    ===============
    `);
    process.exit(0);
  }

  // Get browser binary based on type
  const getBrowserBinary = () => {
    switch (browser) {
      case BROWSER_TYPES.FIREFOX:
        return env.FIREFOX_BINARY;
      case BROWSER_TYPES.BRAVE:
        return env.BRAVE_BINARY || env.CHROME_BINARY;
      case BROWSER_TYPES.CHROME:
        return env.CHROME_BINARY;
      default:
        return env.CHROME_BINARY;
    }
  };

  // Generate manifest with package info
  const generateManifest = () => {
    const manifestPath =
      browser === BROWSER_TYPES.FIREFOX
        ? 'src/manifest.firefox.json'
        : 'src/manifest.chrome.json';

    const manifest = readJsonFile(manifestPath);
    const pkg = readJsonFile('package.json');

    return {
      name: pkg.name,
      description: pkg.description,
      ...manifest,
      version: pkg.version,
    };
  };

  // Vite configuration
  return {
    build: {
      minify: 'esbuild' as const,
    },
    esbuild: {
      keepNames: true,
      minifyIdentifiers: false,
    },
    plugins: [
      react(),
      tsconfigPaths(),
      webExtension({
        manifest: generateManifest,
        // Use Chrome config for Brave
        webExtConfig: {
          target: isBrave
            ? 'chromium'
            : browser === 'firefox'
              ? 'firefox-desktop'
              : 'chromium',
          chromiumBinary: getBrowserBinary(),
          firefoxBinary: env.FIREFOX_BINARY,
          startUrl: ['https://github.com/aidenybai/react-scan'],
        },
      }),
    ],
    optimizeDeps: {
      exclude: ['react-scan'],
    },
  };
});


================================================
FILE: packages/scan/.gitignore
================================================
src/web/assets/css/styles.css


================================================
FILE: packages/scan/CHANGELOG.md
================================================
# react-scan

## 0.5.3

### Patch Changes

- fix

## 0.5.2

### Patch Changes

- fix

## 0.5.1

### Patch Changes

- fix: infinite mounting

## 0.5.0

### Minor Changes

- cleanup
- 9d38ffe: Remove monitoring module, replace Playwright CLI with interactive init command, clean up dead code

  - Removed the entire monitoring system (`packages/scan/src/core/monitor/`) and all related exports, types, and build entries
  - Replaced the Playwright-based proxy CLI (`npx react-scan <url>`) with an interactive `npx react-scan init` command that auto-detects your framework and sets up React Scan
  - Removed unused code: old outline system, LRU cache, lazy refs, commented-out code blocks, and unused exports
  - Consolidated duplicate utilities (safeGetValue, RenderPhase types)
  - Simplified README to focus on the new init command
  - Added CLI quick-start command to the website homepage


================================================
FILE: packages/scan/README.md
================================================
# <img src="https://github.com/aidenybai/react-scan/blob/main/.github/assets/logo.svg" width="30" height="30" align="center" /> React Scan

React Scan automatically detects performance issues in your React app.

Previously, tools like:

- [`<Profiler />`](https://react.dev/reference/react/Profiler) required lots of manual changes
- [Why Did You Render?](https://github.com/welldone-software/why-did-you-render) lacked simple visual cues
- [React Devtools](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html) didn't have a simple, portable, and programmatic API

React Scan attempts to solve these problems:

- It requires no code changes – just drop it in
- It highlights exactly the components you need to optimize
- Use it via script tag, npm, CLI, you name it!

Trusted by engineering teams at:

Airbnb&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://polaris.shopify.com/"><img src="https://raw.githubusercontent.com/aidenybai/react-scan/refs/heads/main/.github/assets/shopify-logo.png" height="30" align="center" /></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://www.faire.com/"><img src="https://raw.githubusercontent.com/aidenybai/react-scan/refs/heads/main/.github/assets/faire-logo.svg" height="20" align="center" /></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://perplexity.com/"><img src="https://raw.githubusercontent.com/aidenybai/react-scan/refs/heads/main/.github/assets/perplexity-logo.png" height="30" align="center" /></a>

### [**Try it out! →**](https://react-scan.million.dev)

![React Scan in action](https://raw.githubusercontent.com/aidenybai/react-scan/refs/heads/main/.github/assets/demo.gif)

> [!IMPORTANT]
> Want to monitor issues in production? Check out [React Scan Monitoring](https://react-scan.com/monitoring)!

## Install

### Package managers

```bash
npm i react-scan
```

```bash
pnpm add react-scan
```

```bash
yarn add react-scan
```

### CDN

```html
<!-- import this BEFORE any scripts -->
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
```

## Usage

- [NextJS App Router](https://github.com/aidenybai/react-scan/blob/main/docs/installation/next-js-app-router.md)
- [NextJS Page Router](https://github.com/aidenybai/react-scan/blob/main/docs/installation/next-js-page-router.md)
- [Create React App](https://github.com/aidenybai/react-scan/blob/main/docs/installation/create-react-app.md)
- [Vite](https://github.com/aidenybai/react-scan/blob/main/docs/installation/vite.md)
- [Parcel](https://github.com/aidenybai/react-scan/blob/main/docs/installation/parcel.md)
- [Remix](https://github.com/aidenybai/react-scan/blob/main/docs/installation/remix.md)
- [React Router](https://github.com/aidenybai/react-scan/blob/main/docs/installation/react-router.md)
- [Astro](https://github.com/aidenybai/react-scan/blob/main/docs/installation/astro.md)
- [TanStack Start](https://github.com/aidenybai/react-scan/blob/main/docs/installation/tanstack-start.md)

### CLI

If you don't have a local version of the site or you want to test a React app remotely, you can use the CLI. This will spin up an isolated browser instance which you can interact or use React Scan with.

```bash
npx react-scan@latest http://localhost:3000
# you can technically scan ANY website on the web:
# npx react-scan@latest https://react.dev
```

You can add it to your existing dev process as well. Here's an example for Next.js:

```json
{
  "scripts": {
    "dev": "next dev",
    "scan": "next dev & npx react-scan@latest localhost:3000"
  }
}
```

### Browser Extension

If you want to install the extension, follow the guide [here](https://github.com/aidenybai/react-scan/blob/main/BROWSER_EXTENSION_GUIDE.md).

### React Native

See [discussion](https://github.com/aidenybai/react-scan/pull/23)

## API Reference

<details>
<summary><code>Options</code></summary>

<br />

```tsx
export interface Options {
  /**
   * Enable/disable scanning
   *
   * Please use the recommended way:
   * enabled: process.env.NODE_ENV === 'development',
   *
   * @default true
   */
  enabled?: boolean;

  /**
   * Force React Scan to run in production (not recommended)
   *
   * @default false
   */
  dangerouslyForceRunInProduction?: boolean;
  /**
   * Log renders to the console
   *
   * WARNING: This can add significant overhead when the app re-renders frequently
   *
   * @default false
   */
  log?: boolean;

  /**
   * Show toolbar bar
   *
   * If you set this to true, and set {@link enabled} to false, the toolbar will still show, but scanning will be disabled.
   *
   * @default true
   */
  showToolbar?: boolean;

  /**
   * Animation speed
   *
   * @default "fast"
   */
  animationSpeed?: "slow" | "fast" | "off";

  /**
   * Track unnecessary renders, and mark their outlines gray when detected
   *
   * An unnecessary render is defined as the component re-rendering with no change to the component's
   * corresponding dom subtree
   *
   *  @default false
   *  @warning tracking unnecessary renders can add meaningful overhead to react-scan
   */
  trackUnnecessaryRenders?: boolean;

  onCommitStart?: () => void;
  onRender?: (fiber: Fiber, renders: Array<Render>) => void;
  onCommitFinish?: () => void;
  onPaintStart?: (outlines: Array<Outline>) => void;
  onPaintFinish?: (outlines: Array<Outline>) => void;
}
```

</details>

- `scan(options: Options)`: Imperative API to start scanning
- `useScan(options: Options)`: Hook API to start scanning
- `getReport()`: Get a report of all the renders
- `setOptions(options: Options): void`: Set options at runtime
- `getOptions()`: Get the current options
- `onRender(Component, onRender: (fiber: Fiber, render: Render) => void)`: Hook into a specific component's renders

## Why React Scan?

React can be tricky to optimize.

The issue is that component props are compared by reference, not value. This is intentional – this way rendering can be cheap to run.

However, this makes it easy to accidentally cause unnecessary renders, making the app slow. Even in production apps, with hundreds of engineers, can't fully optimize their apps (see [GitHub](https://github.com/aidenybai/react-scan/blob/main/.github/assets/github.mp4), [Twitter](https://github.com/aidenybai/react-scan/blob/main/.github/assets/twitter.mp4), and [Instagram](https://github.com/aidenybai/react-scan/blob/main/.github/assets/instagram.mp4)).

This often comes down to props that update in reference, like callbacks or object values. For example, the `onClick` function and `style` object are re-created on every render, causing `ExpensiveComponent` to slow down the app:

```jsx
<ExpensiveComponent onClick={() => alert("hi")} style={{ color: "purple" }} />
```

React Scan helps you identify these issues by automatically detecting and highlighting renders that cause performance issues. Now, instead of guessing, you can see exactly which components you need to fix.

> Want monitor issues in production? Check out [React Scan Monitoring](https://react-scan.com/monitoring)!

### FAQ

**Q: Why this instead of React Devtools?**

React Devtools aims to be a general purpose tool for React. However, I deal with React performance issues every day, and React Devtools doesn't fix my problems well. There's a lot of noise (no obvious distinction between unnecessary and necessary renders), and there's no programmatic API. If it sounds like you have the same problems, then React Scan may be a better choice.

Also, some personal complaints about React Devtools' highlight feature:

- React Devtools "batches" paints, so if a component renders too fast, it will lag behind and only show 1 every second or so
- When you scroll/resize the boxes don't update position
- No count of how many renders there are
- I don't know what the bad/slow renders are without inspecting
- The menu is hidden away so it's annoying to turn on/off, user experience should be specifically tuned for debugging performance, instead of hidden behind a profiler/component tree
- No programmatic API
- It's stuck in a chrome extension, I want to run it anywhere on the web
- It looks subjectively ugly (lines look fuzzy, feels sluggish)
- I'm more ambitious with react-scan

## Resources & Contributing Back

Want to try it out? Check the [our demo](https://react-scan.million.dev).

Looking to contribute back? Check the [Contributing Guide](https://github.com/aidenybai/react-scan/blob/main/CONTRIBUTING.md) out.

Want to talk to the community? Hop in our [Discord](https://discord.gg/X9yFbcV2rF) and share your ideas and what you've build with React Scan.

Find a bug? Head over to our [issue tracker](https://github.com/aidenybai/react-scan/issues) and we'll do our best to help. We love pull requests, too!

We expect all contributors to abide by the terms of our [Code of Conduct](https://github.com/aidenybai/react-scan/blob/main/.github/CODE_OF_CONDUCT.md).

[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-scan/blob/main/CONTRIBUTING.md)

## Acknowledgments

React Scan takes inspiration from the following projects:

- [React Devtools](https://react.dev/learn/react-developer-tools) for the initial idea of [highlighting renders](https://medium.com/dev-proto/highlight-react-components-updates-1b2832f2ce48). We chose to diverge from this to provide a [better developer experience](https://x.com/aidenybai/status/1857122670929969551)
- [Million Lint](https://million.dev) for scanning and linting approaches
- [Why Did You Render?](https://github.com/welldone-software/why-did-you-render) for the concept of hijacking internals to detect unnecessary renders caused by "unstable" props

## License

React Scan is [MIT-licensed](LICENSE) open-source software by Aiden Bai, [Million Software, Inc.](https://million.dev), and [contributors](https://github.com/aidenybai/react-scan/graphs/contributors).


================================================
FILE: packages/scan/auto.d.ts
================================================
export * from './dist/auto';


================================================
FILE: packages/scan/bin/cli.js
================================================
#! /usr/bin/env node
require('../dist/cli');


================================================
FILE: packages/scan/global.d.ts
================================================
declare module '*.css' {
  const content: string;
  export default content;
}

declare module '*.astro' {
  const Component: unknown;
  export default Component;
}


================================================
FILE: packages/scan/package.json
================================================
{
  "name": "react-scan",
  "version": "0.5.3",
  "description": "Scan your React app for renders",
  "keywords": [
    "react",
    "react-scan",
    "react scan",
    "render",
    "performance"
  ],
  "homepage": "https://react-scan.million.dev",
  "bugs": {
    "url": "https://github.com/aidenybai/react-scan/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/aidenybai/react-scan.git"
  },
  "license": "MIT",
  "author": {
    "name": "Aiden Bai",
    "email": "aiden@million.dev",
    "url": "https://million.dev"
  },
  "scripts": {
    "build": "npm run build:css && NODE_ENV=production tsup",
    "postbuild": "node ../../scripts/version-warning.mjs",
    "build:copy": "npm run build:css && NODE_ENV=production tsup && cat dist/auto.global.js | pbcopy",
    "dev:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css --watch",
    "dev:tsup": "NODE_ENV=development tsup --watch",
    "dev": "pnpm run --parallel \"/^dev:(css|tsup)/\"",
    "build:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css",
    "pack": "npm version patch && pnpm build && npm pack",
    "pack:bump": "bun scripts/bump-version.js && nr pack && echo $(pwd)/react-scan-$(node -p \"require('./package.json').version\").tgz | pbcopy",
    "publint": "publint",
    "test": "vitest",
    "lint": "oxlint src && pnpm typecheck",
    "typecheck": "tsc --noEmit"
  },
  "exports": {
    "./package.json": "./package.json",
    ".": {
      "production": {
        "import": {
          "types": "./dist/index.d.mts",
          "react-server": "./dist/rsc-shim.mjs",
          "default": "./dist/index.mjs"
        },
        "require": {
          "types": "./dist/index.d.mts",
          "react-server": "./dist/rsc-shim.js",
          "default": "./dist/index.mjs"
        }
      },
      "development": {
        "import": {
          "types": "./dist/index.d.mts",
          "react-server": "./dist/rsc-shim.mjs",
          "default": "./dist/index.mjs"
        },
        "require": {
          "types": "./dist/index.d.ts",
          "react-server": "./dist/rsc-shim.js",
          "default": "./dist/index.js"
        }
      },
      "default": {
        "import": {
          "types": "./dist/index.d.mts",
          "react-server": "./dist/rsc-shim.mjs",
          "default": "./dist/index.mjs"
        },
        "require": {
          "types": "./dist/index.d.ts",
          "react-server": "./dist/rsc-shim.js",
          "default": "./dist/index.js"
        }
      }
    },
    "./all-environments": {
      "types": "./dist/core/all-environments.d.ts",
      "import": "./dist/core/all-environments.mjs",
      "require": "./dist/core/all-environments.js"
    },
    "./install-hook": {
      "types": "./dist/install-hook.d.ts",
      "import": "./dist/install-hook.mjs",
      "require": "./dist/install-hook.js"
    },
    "./auto": {
      "production": {
        "import": {
          "types": "./dist/rsc-shim.d.mts",
          "react-server": "./dist/rsc-shim.mjs",
          "default": "./dist/rsc-shim.mjs"
        },
        "require": {
          "types": "./dist/rsc-shim.d.ts",
          "react-server": "./dist/rsc-shim.js",
          "default": "./dist/rsc-shim.js"
        }
      },
      "development": {
        "import": {
          "types": "./dist/auto.d.mts",
          "react-server": "./dist/rsc-shim.mjs",
          "default": "./dist/auto.mjs"
        },
        "require": {
          "types": "./dist/auto.d.ts",
          "react-server": "./dist/rsc-shim.js",
          "default": "./dist/auto.js"
        }
      }
    },
    "./dist/*": "./dist/*.js",
    "./dist/*.js": "./dist/*.js",
    "./dist/*.mjs": "./dist/*.mjs",
    "./react-component-name/vite": {
      "types": "./dist/react-component-name/vite.d.ts",
      "import": "./dist/react-component-name/vite.mjs",
      "require": "./dist/react-component-name/vite.js"
    },
    "./react-component-name/webpack": {
      "types": "./dist/react-component-name/webpack.d.ts",
      "import": "./dist/react-component-name/webpack.mjs",
      "require": "./dist/react-component-name/webpack.js"
    },
    "./react-component-name/esbuild": {
      "types": "./dist/react-component-name/esbuild.d.ts",
      "import": "./dist/react-component-name/esbuild.mjs",
      "require": "./dist/react-component-name/esbuild.js"
    },
    "./react-component-name/rspack": {
      "types": "./dist/react-component-name/rspack.d.ts",
      "import": "./dist/react-component-name/rspack.mjs",
      "require": "./dist/react-component-name/rspack.js"
    },
    "./react-component-name/rolldown": {
      "types": "./dist/react-component-name/rolldown.d.ts",
      "import": "./dist/react-component-name/rolldown.mjs",
      "require": "./dist/react-component-name/rolldown.js"
    },
    "./react-component-name/rollup": {
      "types": "./dist/react-component-name/rollup.d.ts",
      "import": "./dist/react-component-name/rollup.mjs",
      "require": "./dist/react-component-name/rollup.js"
    },
    "./react-component-name/astro": {
      "types": "./dist/react-component-name/astro.d.ts",
      "import": "./dist/react-component-name/astro.mjs",
      "require": "./dist/react-component-name/astro.js"
    },
    "./react-component-name/loader": {
      "types": "./dist/react-component-name/loader.d.ts",
      "import": "./dist/react-component-name/loader.mjs",
      "require": "./dist/react-component-name/loader.js"
    }
  },
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "browser": "dist/auto.global.js",
  "types": "dist/index.d.ts",
  "typesVersions": {
    "*": {
      "react-component-name/vite": [
        "./dist/react-component-name/vite.d.ts"
      ],
      "react-component-name/webpack": [
        "./dist/react-component-name/webpack.d.ts"
      ],
      "react-component-name/esbuild": [
        "./dist/react-component-name/esbuild.d.ts"
      ],
      "react-component-name/rspack": [
        "./dist/react-component-name/rspack.d.ts"
      ],
      "react-component-name/rolldown": [
        "./dist/react-component-name/rolldown.d.ts"
      ],
      "react-component-name/rollup": [
        "./dist/react-component-name/rollup.d.ts"
      ],
      "react-component-name/astro": [
        "./dist/react-component-name/astro.d.ts"
      ],
      "react-component-name/loader": [
        "./dist/react-component-name/loader.d.ts"
      ]
    }
  },
  "bin": "bin/cli.js",
  "files": [
    "dist",
    "bin",
    "package.json",
    "README.md",
    "LICENSE",
    "auto.d.ts"
  ],
  "dependencies": {
    "@babel/core": "^7.26.0",
    "@babel/generator": "^7.26.2",
    "@babel/types": "^7.26.0",
    "@preact/signals": "^1.3.1",
    "@rollup/pluginutils": "^5.1.3",
    "@types/node": "^20.17.9",
    "bippy": "^0.5.30",
    "commander": "^14.0.0",
    "esbuild": "^0.25.0",
    "estree-walker": "^3.0.3",
    "picocolors": "^1.1.1",
    "preact": "^10.25.1",
    "prompts": "^2.4.2"
  },
  "devDependencies": {
    "@esbuild-plugins/tsconfig-paths": "^0.1.2",
    "@remix-run/react": "*",
    "@types/babel__core": "^7.20.5",
    "@types/prompts": "^2.4.9",
    "@types/react": "^18.0.0",
    "@types/react-router": "^5.1.0",
    "clsx": "^2.1.1",
    "es-module-lexer": "^1.5.4",
    "next": "*",
    "postcss-cli": "^11.0.0",
    "publint": "^0.2.12",
    "react": "*",
    "react-dom": "*",
    "react-router": "^5.0.0",
    "react-router-dom": "^5.0.0 || ^6.0.0 || ^7.0.0",
    "tailwind-merge": "^2.5.5",
    "terser": "^5.36.0",
    "tsup": "^8.0.0",
    "vitest": "^3.0.0"
  },
  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  },
  "optionalDependencies": {
    "unplugin": "2.1.0"
  },
  "publishConfig": {
    "access": "public"
  }
}


================================================
FILE: packages/scan/postcss.config.mjs
================================================
import autoprefixer from 'autoprefixer';
import tailwindcss from 'tailwindcss';
import remToPx from './postcss.rem2px.mjs';

export default {
  plugins: [remToPx({ baseValue: 16 }), tailwindcss, autoprefixer],
};


================================================
FILE: packages/scan/postcss.rem2px.mjs
================================================
const remToPx = (options = {}) => {
  const baseValue = options.baseValue || 16;

  // Improved regex that handles all rem cases including negatives
  const remRegex = /(?<![-\w])(-)?((?:\d*\.)?\d+)rem\b/g;

  const convertRemToPx = (value) => {
    // Handle all cases: calc(), min(), max(), clamp(), and regular values
    return value.replace(remRegex, (_match, negative, num) => {
      const pixels = Number.parseFloat(num) * baseValue;
      return `${negative ? '-' : ''}${pixels}px`;
    });
  };

  return {
    postcssPlugin: 'postcss-rem-to-px',
    prepare() {
      return {
        Once(root) {
          root.walkDecls((decl) => {
            if (decl.value?.includes('rem')) {
              decl.value = convertRemToPx(decl.value);
            }
          });
        },
        Declaration(decl) {
          if (decl.value?.includes('rem')) {
            decl.value = convertRemToPx(decl.value);
          }
        },
        AtRule: {
          media: (atRule) => {
            if (atRule.params?.includes('rem')) {
              atRule.params = convertRemToPx(atRule.params);
            }
          },
        },
      };
    },
  };
};

remToPx.postcss = true;

export default remToPx;


================================================
FILE: packages/scan/scripts/bump-version.js
================================================
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

// Read the current version from scan package.json
const scanPackagePath = path.join(__dirname, '../package.json');
const scanPackage = JSON.parse(fs.readFileSync(scanPackagePath, 'utf8'));

// Bump patch version
const version = scanPackage.version.split('.');
version[2] = Number.parseInt(version[2]) + 1;
const newVersion = version.join('.');

// Update the version in package.json
scanPackage.version = newVersion;

// Write back to package.json
fs.writeFileSync(scanPackagePath, `${JSON.stringify(scanPackage, null, 2)}\n`);

// Get the tar file path
const tarFileName = `react-scan-${newVersion}.tgz`;
const tarFilePath = path.join(__dirname, '..', tarFileName);

// Copy to clipboard
execSync(`echo "${tarFilePath}" | pbcopy`);

// oxlint-disable-next-line no-console
console.log(`Bumped version to ${newVersion}`);
// oxlint-disable-next-line no-console
console.log(`Tar file path copied to clipboard: ${tarFilePath}`);


================================================
FILE: packages/scan/src/auto.ts
================================================
import './polyfills';
// Prioritize bippy side-effect
import 'bippy';

import { IS_CLIENT } from '~web/utils/constants';
import { scan } from './index';

if (IS_CLIENT) {
  scan();
  window.reactScan = scan;
}

export * from './core';


================================================
FILE: packages/scan/src/cli-utils.mts
================================================
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
type Framework = 'next' | 'vite' | 'tanstack' | 'webpack' | 'unknown';
type NextRouterType = 'app' | 'pages' | 'unknown';

interface ProjectInfo {
  packageManager: PackageManager;
  framework: Framework;
  nextRouterType: NextRouterType;
  projectRoot: string;
  hasReactScan: boolean;
}

interface TransformResult {
  success: boolean;
  filePath: string;
  message: string;
  originalContent?: string;
  newContent?: string;
  noChanges?: boolean;
}

interface DiffLine {
  type: 'added' | 'removed' | 'unchanged';
  content: string;
}

const FRAMEWORK_NAMES: Record<Framework, string> = {
  next: 'Next.js',
  vite: 'Vite',
  tanstack: 'TanStack Start',
  webpack: 'Webpack',
  unknown: 'Unknown',
};

const INSTALL_COMMANDS: Record<PackageManager, string> = {
  npm: 'npm install -D',
  yarn: 'yarn add -D',
  pnpm: 'pnpm add -D',
  bun: 'bun add -D',
};

// --- Templates ---

const REACT_SCAN_SCRIPT_TAG = '<script src="https://unpkg.com/react-scan/dist/auto.global.js" crossorigin="anonymous"></script>';

const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
          <script src="https://unpkg.com/react-scan/dist/auto.global.js" crossOrigin="anonymous" />
        )}`;

const NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
          <script src="https://unpkg.com/react-scan/dist/auto.global.js" crossOrigin="anonymous" />
        )}`;

const VITE_SCRIPT = `<script src="https://unpkg.com/react-scan/dist/auto.global.js" crossorigin="anonymous"></script>`;

const WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
  import("react-scan");
}`;

// --- Detection ---

const detectPackageManager = (projectRoot: string): PackageManager => {
  if (existsSync(join(projectRoot, 'bun.lockb')) || existsSync(join(projectRoot, 'bun.lock'))) return 'bun';
  if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
  if (existsSync(join(projectRoot, 'yarn.lock'))) return 'yarn';
  return 'npm';
};

const detectFramework = (projectRoot: string): Framework => {
  const packageJsonPath = join(projectRoot, 'package.json');
  if (!existsSync(packageJsonPath)) return 'unknown';

  try {
    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
    const allDeps = {
      ...packageJson.dependencies,
      ...packageJson.devDependencies,
    };

    if (allDeps['next']) return 'next';
    if (allDeps['@tanstack/react-start']) return 'tanstack';
    if (allDeps['vite']) return 'vite';
    if (allDeps['webpack'] || allDeps['react-scripts']) return 'webpack';

    return 'unknown';
  } catch {
    return 'unknown';
  }
};

const detectNextRouterType = (projectRoot: string): NextRouterType => {
  if (existsSync(join(projectRoot, 'app')) || existsSync(join(projectRoot, 'src', 'app'))) return 'app';
  if (existsSync(join(projectRoot, 'pages')) || existsSync(join(projectRoot, 'src', 'pages'))) return 'pages';
  return 'unknown';
};

const detectProject = (cwd: string): ProjectInfo => {
  const packageManager = detectPackageManager(cwd);
  const framework = detectFramework(cwd);
  const nextRouterType = framework === 'next' ? detectNextRouterType(cwd) : 'unknown';

  const packageJsonPath = join(cwd, 'package.json');
  let hasReactScan = false;
  if (existsSync(packageJsonPath)) {
    try {
      const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
      const allDeps = {
        ...packageJson.dependencies,
        ...packageJson.devDependencies,
      };
      hasReactScan = Boolean(allDeps['react-scan']);
    } catch { /* */ }
  }

  return {
    packageManager,
    framework,
    nextRouterType,
    projectRoot: cwd,
    hasReactScan,
  };
};

// --- File Finding ---

const findLayoutFile = (
  projectRoot: string,
  routerType: NextRouterType,
): string | null => {
  if (routerType === 'app') {
    const candidates = [
      join(projectRoot, 'app', 'layout.tsx'),
      join(projectRoot, 'app', 'layout.jsx'),
      join(projectRoot, 'app', 'layout.js'),
      join(projectRoot, 'src', 'app', 'layout.tsx'),
      join(projectRoot, 'src', 'app', 'layout.jsx'),
      join(projectRoot, 'src', 'app', 'layout.js'),
    ];
    return candidates.find(existsSync) ?? null;
  }

  if (routerType === 'pages') {
    const candidates = [
      join(projectRoot, 'pages', '_document.tsx'),
      join(projectRoot, 'pages', '_document.jsx'),
      join(projectRoot, 'pages', '_document.js'),
      join(projectRoot, 'src', 'pages', '_document.tsx'),
      join(projectRoot, 'src', 'pages', '_document.jsx'),
      join(projectRoot, 'src', 'pages', '_document.js'),
    ];
    return candidates.find(existsSync) ?? null;
  }

  return null;
};

const findIndexHtml = (projectRoot: string): string | null => {
  const candidates = [
    join(projectRoot, 'index.html'),
    join(projectRoot, 'public', 'index.html'),
    join(projectRoot, 'src', 'index.html'),
  ];
  return candidates.find(existsSync) ?? null;
};

const findEntryFile = (projectRoot: string): string | null => {
  const candidates = [
    join(projectRoot, 'src', 'index.tsx'),
    join(projectRoot, 'src', 'index.ts'),
    join(projectRoot, 'src', 'index.jsx'),
    join(projectRoot, 'src', 'index.js'),
    join(projectRoot, 'src', 'main.tsx'),
    join(projectRoot, 'src', 'main.ts'),
    join(projectRoot, 'src', 'main.jsx'),
    join(projectRoot, 'src', 'main.js'),
  ];
  return candidates.find(existsSync) ?? null;
};

const hasReactScanCode = (content: string): boolean => {
  return content.includes('react-scan') || content.includes('react_scan');
};

// --- Transform ---

const transformNextAppRouter = (
  projectRoot: string,
  routerType: NextRouterType,
): TransformResult => {
  const layoutPath = findLayoutFile(projectRoot, routerType);
  if (!layoutPath) {
    return {
      success: false,
      filePath: '',
      message: 'Could not find app/layout.tsx',
    };
  }

  const originalContent = readFileSync(layoutPath, 'utf-8');

  if (hasReactScanCode(originalContent)) {
    return {
      success: true,
      filePath: layoutPath,
      message: 'React Scan is already installed.',
      noChanges: true,
    };
  }

  let newContent = originalContent;

  const headOpenMatch = newContent.match(/<head[^>]*>/);
  if (headOpenMatch) {
    const injection = `\n        ${NEXT_APP_ROUTER_SCRIPT}\n`;
    newContent = newContent.replace(
      headOpenMatch[0],
      `${headOpenMatch[0]}${injection}`,
    );
  } else {
    const bodyMatch = newContent.match(/<body[\s\S]*?>/);
    if (bodyMatch) {
      const injection = `\n        ${NEXT_APP_ROUTER_SCRIPT}`;
      newContent = newContent.replace(
        bodyMatch[0],
        `${bodyMatch[0]}${injection}`,
      );
    }
  }

  return {
    success: true,
    filePath: layoutPath,
    message: 'Success',
    originalContent,
    newContent,
  };
};

const transformNextPagesRouter = (
  projectRoot: string,
  routerType: NextRouterType,
): TransformResult => {
  const documentPath = findLayoutFile(projectRoot, routerType);
  if (!documentPath) {
    return {
      success: false,
      filePath: '',
      message: 'Could not find pages/_document.tsx',
    };
  }

  const originalContent = readFileSync(documentPath, 'utf-8');

  if (hasReactScanCode(originalContent)) {
    return {
      success: true,
      filePath: documentPath,
      message: 'React Scan is already installed.',
      noChanges: true,
    };
  }

  let newContent = originalContent;
  const injection = `\n        ${NEXT_PAGES_ROUTER_SCRIPT}`;

  const headMatch = newContent.match(/<Head>([\s\S]*?)<\/Head>/);
  if (headMatch) {
    newContent = newContent.replace('<Head>', `<Head>${injection}`);
  } else {
    const selfClosingHeadMatch = newContent.match(/<Head\s*\/>/);
    if (selfClosingHeadMatch) {
      newContent = newContent.replace(
        selfClosingHeadMatch[0],
        `<Head>${injection}\n      </Head>`,
      );
    }
  }

  if (newContent === originalContent) {
    return {
      success: false,
      filePath: documentPath,
      message:
        'Could not find <Head> component in _document file to inject React Scan script.',
    };
  }

  return {
    success: true,
    filePath: documentPath,
    message: 'Success',
    originalContent,
    newContent,
  };
};

const transformVite = (projectRoot: string): TransformResult => {
  const indexHtml = findIndexHtml(projectRoot);
  if (!indexHtml) {
    return {
      success: false,
      filePath: '',
      message: 'Could not find index.html',
    };
  }

  const originalContent = readFileSync(indexHtml, 'utf-8');

  if (hasReactScanCode(originalContent)) {
    return {
      success: true,
      filePath: indexHtml,
      message: 'React Scan is already installed.',
      noChanges: true,
    };
  }

  const headOpenMatch = originalContent.match(/<head[^>]*>/);
  if (!headOpenMatch) {
    return {
      success: false,
      filePath: indexHtml,
      message: 'Could not find <head> tag in index.html',
    };
  }

  const newContent = originalContent.replace(
    headOpenMatch[0],
    `${headOpenMatch[0]}\n    ${VITE_SCRIPT}`,
  );

  return {
    success: true,
    filePath: indexHtml,
    message: 'Success',
    originalContent,
    newContent,
  };
};

const transformWebpack = (projectRoot: string): TransformResult => {
  const indexHtml = findIndexHtml(projectRoot);
  if (indexHtml) {
    const originalContent = readFileSync(indexHtml, 'utf-8');
    if (hasReactScanCode(originalContent)) {
      return {
        success: true,
        filePath: indexHtml,
        message: 'React Scan is already installed.',
        noChanges: true,
      };
    }

    const headOpenMatch = originalContent.match(/<head[^>]*>/);
    if (!headOpenMatch) {
      return {
        success: false,
        filePath: indexHtml,
        message: 'Could not find <head> tag in index.html',
      };
    }

    const newContent = originalContent.replace(
      headOpenMatch[0],
      `${headOpenMatch[0]}\n    ${REACT_SCAN_SCRIPT_TAG}`,
    );

    return {
      success: true,
      filePath: indexHtml,
      message: 'Success',
      originalContent,
      newContent,
    };
  }

  const entryFile = findEntryFile(projectRoot);
  if (!entryFile) {
    return {
      success: false,
      filePath: '',
      message: 'Could not find entry file or index.html',
    };
  }

  const originalContent = readFileSync(entryFile, 'utf-8');
  if (hasReactScanCode(originalContent)) {
    return {
      success: true,
      filePath: entryFile,
      message: 'React Scan is already installed.',
      noChanges: true,
    };
  }

  const newContent = `${WEBPACK_IMPORT}\n\n${originalContent}`;

  return {
    success: true,
    filePath: entryFile,
    message: 'Success',
    originalContent,
    newContent,
  };
};

const previewTransform = (
  projectRoot: string,
  framework: Framework,
  nextRouterType: NextRouterType,
): TransformResult => {
  switch (framework) {
    case 'next':
      return nextRouterType === 'pages'
        ? transformNextPagesRouter(projectRoot, nextRouterType)
        : transformNextAppRouter(projectRoot, nextRouterType);
    case 'vite':
      return transformVite(projectRoot);
    case 'webpack':
      return transformWebpack(projectRoot);
    case 'tanstack':
    case 'unknown':
    default:
      return {
        success: false,
        filePath: '',
        message: `Framework "${framework}" is not yet supported by automatic setup. Visit https://github.com/aidenybai/react-scan#install for manual setup.`,
      };
  }
};

// --- Diff ---

const generateDiff = (original: string, updated: string): DiffLine[] => {
  const originalLines = original.split('\n');
  const newLines = updated.split('\n');
  const diff: DiffLine[] = [];

  let originalIdx = 0;
  let newIdx = 0;

  while (originalIdx < originalLines.length || newIdx < newLines.length) {
    const originalLine = originalLines[originalIdx];
    const newLine = newLines[newIdx];

    if (originalLine === newLine) {
      diff.push({ type: 'unchanged', content: originalLine });
      originalIdx++;
      newIdx++;
    } else if (originalLine === undefined) {
      diff.push({ type: 'added', content: newLine });
      newIdx++;
    } else if (newLine === undefined) {
      diff.push({ type: 'removed', content: originalLine });
      originalIdx++;
    } else {
      const originalInNew = newLines.indexOf(originalLine, newIdx);
      const newInOriginal = originalLines.indexOf(newLine, originalIdx);

      if (originalInNew !== -1 && (newInOriginal === -1 || originalInNew - newIdx < newInOriginal - originalIdx)) {
        while (newIdx < originalInNew) {
          diff.push({ type: 'added', content: newLines[newIdx] });
          newIdx++;
        }
      } else if (newInOriginal !== -1) {
        while (originalIdx < newInOriginal) {
          diff.push({ type: 'removed', content: originalLines[originalIdx] });
          originalIdx++;
        }
      } else {
        diff.push({ type: 'removed', content: originalLine });
        diff.push({ type: 'added', content: newLine });
        originalIdx++;
        newIdx++;
      }
    }
  }

  return diff;
};

export {
  type DiffLine,
  type Framework,
  type NextRouterType,
  type PackageManager,
  type ProjectInfo,
  type TransformResult,
  FRAMEWORK_NAMES,
  INSTALL_COMMANDS,
  NEXT_APP_ROUTER_SCRIPT,
  NEXT_PAGES_ROUTER_SCRIPT,
  REACT_SCAN_SCRIPT_TAG,
  VITE_SCRIPT,
  WEBPACK_IMPORT,
  detectFramework,
  detectNextRouterType,
  detectPackageManager,
  detectProject,
  findEntryFile,
  findIndexHtml,
  findLayoutFile,
  generateDiff,
  hasReactScanCode,
  previewTransform,
  transformNextAppRouter,
  transformNextPagesRouter,
  transformVite,
  transformWebpack,
};


================================================
FILE: packages/scan/src/cli-utils.test.mts
================================================
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
  NEXT_APP_ROUTER_SCRIPT,
  NEXT_PAGES_ROUTER_SCRIPT,
  REACT_SCAN_SCRIPT_TAG,
  VITE_SCRIPT,
  WEBPACK_IMPORT,
  detectFramework,
  detectNextRouterType,
  detectPackageManager,
  detectProject,
  findEntryFile,
  findIndexHtml,
  findLayoutFile,
  generateDiff,
  hasReactScanCode,
  previewTransform,
  transformNextAppRouter,
  transformNextPagesRouter,
  transformVite,
  transformWebpack,
} from './cli-utils.mjs';

let tempDirectory: string;

beforeEach(() => {
  tempDirectory = mkdtempSync(join(tmpdir(), 'react-scan-cli-test-'));
});

afterEach(() => {
  rmSync(tempDirectory, { recursive: true, force: true });
});

const writePackageJson = (
  directory: string,
  dependencies: Record<string, string> = {},
  devDependencies: Record<string, string> = {},
) => {
  writeFileSync(
    join(directory, 'package.json'),
    JSON.stringify({ dependencies, devDependencies }),
  );
};

// --- detectPackageManager ---

describe('detectPackageManager', () => {
  it('returns bun when bun.lockb exists', () => {
    writeFileSync(join(tempDirectory, 'bun.lockb'), '');
    expect(detectPackageManager(tempDirectory)).toBe('bun');
  });

  it('returns bun when bun.lock exists', () => {
    writeFileSync(join(tempDirectory, 'bun.lock'), '');
    expect(detectPackageManager(tempDirectory)).toBe('bun');
  });

  it('returns pnpm when pnpm-lock.yaml exists', () => {
    writeFileSync(join(tempDirectory, 'pnpm-lock.yaml'), '');
    expect(detectPackageManager(tempDirectory)).toBe('pnpm');
  });

  it('returns yarn when yarn.lock exists', () => {
    writeFileSync(join(tempDirectory, 'yarn.lock'), '');
    expect(detectPackageManager(tempDirectory)).toBe('yarn');
  });

  it('defaults to npm when no lock file exists', () => {
    expect(detectPackageManager(tempDirectory)).toBe('npm');
  });

  it('prefers bun over pnpm when both lock files exist', () => {
    writeFileSync(join(tempDirectory, 'bun.lockb'), '');
    writeFileSync(join(tempDirectory, 'pnpm-lock.yaml'), '');
    expect(detectPackageManager(tempDirectory)).toBe('bun');
  });

  it('prefers pnpm over yarn when both lock files exist', () => {
    writeFileSync(join(tempDirectory, 'pnpm-lock.yaml'), '');
    writeFileSync(join(tempDirectory, 'yarn.lock'), '');
    expect(detectPackageManager(tempDirectory)).toBe('pnpm');
  });
});

// --- detectFramework ---

describe('detectFramework', () => {
  it('detects Next.js from dependencies', () => {
    writePackageJson(tempDirectory, { next: '^14.0.0' });
    expect(detectFramework(tempDirectory)).toBe('next');
  });

  it('detects Next.js from devDependencies', () => {
    writePackageJson(tempDirectory, {}, { next: '^14.0.0' });
    expect(detectFramework(tempDirectory)).toBe('next');
  });

  it('detects Vite from dependencies', () => {
    writePackageJson(tempDirectory, {}, { vite: '^5.0.0' });
    expect(detectFramework(tempDirectory)).toBe('vite');
  });

  it('detects TanStack Start from dependencies', () => {
    writePackageJson(tempDirectory, { '@tanstack/react-start': '^1.0.0' });
    expect(detectFramework(tempDirectory)).toBe('tanstack');
  });

  it('detects Webpack from dependencies', () => {
    writePackageJson(tempDirectory, {}, { webpack: '^5.0.0' });
    expect(detectFramework(tempDirectory)).toBe('webpack');
  });

  it('detects Webpack via react-scripts', () => {
    writePackageJson(tempDirectory, { 'react-scripts': '^5.0.0' });
    expect(detectFramework(tempDirectory)).toBe('webpack');
  });

  it('returns unknown when no framework is detected', () => {
    writePackageJson(tempDirectory, { react: '^18.0.0' });
    expect(detectFramework(tempDirectory)).toBe('unknown');
  });

  it('returns unknown when no package.json exists', () => {
    expect(detectFramework(tempDirectory)).toBe('unknown');
  });

  it('returns unknown when package.json is malformed', () => {
    writeFileSync(join(tempDirectory, 'package.json'), 'not-json');
    expect(detectFramework(tempDirectory)).toBe('unknown');
  });

  it('prefers Next.js over Vite when both are present', () => {
    writePackageJson(tempDirectory, { next: '^14.0.0' }, { vite: '^5.0.0' });
    expect(detectFramework(tempDirectory)).toBe('next');
  });
});

// --- detectNextRouterType ---

describe('detectNextRouterType', () => {
  it('detects app router from root app directory', () => {
    mkdirSync(join(tempDirectory, 'app'));
    expect(detectNextRouterType(tempDirectory)).toBe('app');
  });

  it('detects app router from src/app directory', () => {
    mkdirSync(join(tempDirectory, 'src', 'app'), { recursive: true });
    expect(detectNextRouterType(tempDirectory)).toBe('app');
  });

  it('detects pages router from root pages directory', () => {
    mkdirSync(join(tempDirectory, 'pages'));
    expect(detectNextRouterType(tempDirectory)).toBe('pages');
  });

  it('detects pages router from src/pages directory', () => {
    mkdirSync(join(tempDirectory, 'src', 'pages'), { recursive: true });
    expect(detectNextRouterType(tempDirectory)).toBe('pages');
  });

  it('returns unknown when no router directories exist', () => {
    expect(detectNextRouterType(tempDirectory)).toBe('unknown');
  });

  it('prefers app router when both app and pages directories exist', () => {
    mkdirSync(join(tempDirectory, 'app'));
    mkdirSync(join(tempDirectory, 'pages'));
    expect(detectNextRouterType(tempDirectory)).toBe('app');
  });
});

// --- detectProject ---

describe('detectProject', () => {
  it('detects a Next.js app router project with pnpm', () => {
    writePackageJson(tempDirectory, { next: '^14.0.0', react: '^18.0.0' });
    writeFileSync(join(tempDirectory, 'pnpm-lock.yaml'), '');
    mkdirSync(join(tempDirectory, 'app'));

    const project = detectProject(tempDirectory);
    expect(project.packageManager).toBe('pnpm');
    expect(project.framework).toBe('next');
    expect(project.nextRouterType).toBe('app');
    expect(project.projectRoot).toBe(tempDirectory);
    expect(project.hasReactScan).toBe(false);
  });

  it('detects hasReactScan from dependencies', () => {
    writePackageJson(tempDirectory, { 'react-scan': '^0.4.0', vite: '^5.0.0' });
    const project = detectProject(tempDirectory);
    expect(project.hasReactScan).toBe(true);
  });

  it('detects hasReactScan from devDependencies', () => {
    writePackageJson(tempDirectory, { vite: '^5.0.0' }, { 'react-scan': '^0.4.0' });
    const project = detectProject(tempDirectory);
    expect(project.hasReactScan).toBe(true);
  });

  it('sets nextRouterType to unknown for non-Next.js frameworks', () => {
    writePackageJson(tempDirectory, {}, { vite: '^5.0.0' });
    mkdirSync(join(tempDirectory, 'app'));
    const project = detectProject(tempDirectory);
    expect(project.nextRouterType).toBe('unknown');
  });
});

// --- hasReactScanCode ---

describe('hasReactScanCode', () => {
  it('detects react-scan in content', () => {
    expect(hasReactScanCode('import("react-scan")')).toBe(true);
  });

  it('detects react_scan in content', () => {
    expect(hasReactScanCode('window.react_scan = true')).toBe(true);
  });

  it('returns false when not present', () => {
    expect(hasReactScanCode('import React from "react"')).toBe(false);
  });

  it('detects react-scan in script tag', () => {
    expect(hasReactScanCode('<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>')).toBe(true);
  });
});

// --- findLayoutFile ---

describe('findLayoutFile', () => {
  it('finds app/layout.tsx for app router', () => {
    mkdirSync(join(tempDirectory, 'app'));
    const layoutPath = join(tempDirectory, 'app', 'layout.tsx');
    writeFileSync(layoutPath, '');
    expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath);
  });

  it('finds src/app/layout.tsx for app router', () => {
    mkdirSync(join(tempDirectory, 'src', 'app'), { recursive: true });
    const layoutPath = join(tempDirectory, 'src', 'app', 'layout.tsx');
    writeFileSync(layoutPath, '');
    expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath);
  });

  it('finds app/layout.jsx for app router', () => {
    mkdirSync(join(tempDirectory, 'app'));
    const layoutPath = join(tempDirectory, 'app', 'layout.jsx');
    writeFileSync(layoutPath, '');
    expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath);
  });

  it('finds pages/_document.tsx for pages router', () => {
    mkdirSync(join(tempDirectory, 'pages'));
    const documentPath = join(tempDirectory, 'pages', '_document.tsx');
    writeFileSync(documentPath, '');
    expect(findLayoutFile(tempDirectory, 'pages')).toBe(documentPath);
  });

  it('finds src/pages/_document.tsx for pages router', () => {
    mkdirSync(join(tempDirectory, 'src', 'pages'), { recursive: true });
    const documentPath = join(tempDirectory, 'src', 'pages', '_document.tsx');
    writeFileSync(documentPath, '');
    expect(findLayoutFile(tempDirectory, 'pages')).toBe(documentPath);
  });

  it('returns null when no layout file exists for app router', () => {
    expect(findLayoutFile(tempDirectory, 'app')).toBeNull();
  });

  it('returns null when no document file exists for pages router', () => {
    expect(findLayoutFile(tempDirectory, 'pages')).toBeNull();
  });

  it('returns null for unknown router type', () => {
    expect(findLayoutFile(tempDirectory, 'unknown')).toBeNull();
  });
});

// --- findIndexHtml ---

describe('findIndexHtml', () => {
  it('finds root index.html', () => {
    const indexPath = join(tempDirectory, 'index.html');
    writeFileSync(indexPath, '');
    expect(findIndexHtml(tempDirectory)).toBe(indexPath);
  });

  it('finds public/index.html', () => {
    mkdirSync(join(tempDirectory, 'public'));
    const indexPath = join(tempDirectory, 'public', 'index.html');
    writeFileSync(indexPath, '');
    expect(findIndexHtml(tempDirectory)).toBe(indexPath);
  });

  it('finds src/index.html', () => {
    mkdirSync(join(tempDirectory, 'src'));
    const indexPath = join(tempDirectory, 'src', 'index.html');
    writeFileSync(indexPath, '');
    expect(findIndexHtml(tempDirectory)).toBe(indexPath);
  });

  it('prefers root index.html over public/index.html', () => {
    const rootPath = join(tempDirectory, 'index.html');
    writeFileSync(rootPath, '');
    mkdirSync(join(tempDirectory, 'public'));
    writeFileSync(join(tempDirectory, 'public', 'index.html'), '');
    expect(findIndexHtml(tempDirectory)).toBe(rootPath);
  });

  it('returns null when no index.html exists', () => {
    expect(findIndexHtml(tempDirectory)).toBeNull();
  });
});

// --- findEntryFile ---

describe('findEntryFile', () => {
  it('finds src/index.tsx', () => {
    mkdirSync(join(tempDirectory, 'src'));
    const entryPath = join(tempDirectory, 'src', 'index.tsx');
    writeFileSync(entryPath, '');
    expect(findEntryFile(tempDirectory)).toBe(entryPath);
  });

  it('finds src/main.tsx', () => {
    mkdirSync(join(tempDirectory, 'src'));
    const entryPath = join(tempDirectory, 'src', 'main.tsx');
    writeFileSync(entryPath, '');
    expect(findEntryFile(tempDirectory)).toBe(entryPath);
  });

  it('finds src/index.js', () => {
    mkdirSync(join(tempDirectory, 'src'));
    const entryPath = join(tempDirectory, 'src', 'index.js');
    writeFileSync(entryPath, '');
    expect(findEntryFile(tempDirectory)).toBe(entryPath);
  });

  it('prefers src/index.tsx over src/main.tsx', () => {
    mkdirSync(join(tempDirectory, 'src'));
    const indexPath = join(tempDirectory, 'src', 'index.tsx');
    writeFileSync(indexPath, '');
    writeFileSync(join(tempDirectory, 'src', 'main.tsx'), '');
    expect(findEntryFile(tempDirectory)).toBe(indexPath);
  });

  it('returns null when no entry file exists', () => {
    expect(findEntryFile(tempDirectory)).toBeNull();
  });
});

// --- transformNextAppRouter ---

describe('transformNextAppRouter', () => {
  const LAYOUT_WITH_BODY = `export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}`;

  const LAYOUT_WITH_HEAD = `export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head><title>App</title></head>
      <body>{children}</body>
    </html>
  );
}`;

  it('returns failure when no layout file exists', () => {
    const result = transformNextAppRouter(tempDirectory, 'app');
    expect(result.success).toBe(false);
    expect(result.message).toContain('Could not find');
  });

  it('injects script after body tag when no head tag exists', () => {
    mkdirSync(join(tempDirectory, 'app'));
    writeFileSync(join(tempDirectory, 'app', 'layout.tsx'), LAYOUT_WITH_BODY);

    const result = transformNextAppRouter(tempDirectory, 'app');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
    expect(result.newContent).toContain('<body>');
  });

  it('reports already installed when react-scan is in content', () => {
    mkdirSync(join(tempDirectory, 'app'));
    writeFileSync(
      join(tempDirectory, 'app', 'layout.tsx'),
      'import "react-scan";\n' + LAYOUT_WITH_BODY,
    );

    const result = transformNextAppRouter(tempDirectory, 'app');
    expect(result.success).toBe(true);
    expect(result.noChanges).toBe(true);
    expect(result.message).toContain('already installed');
  });

  it('preserves original content', () => {
    mkdirSync(join(tempDirectory, 'app'));
    writeFileSync(join(tempDirectory, 'app', 'layout.tsx'), LAYOUT_WITH_BODY);

    const result = transformNextAppRouter(tempDirectory, 'app');
    expect(result.originalContent).toBe(LAYOUT_WITH_BODY);
  });
});

// --- transformNextPagesRouter ---

describe('transformNextPagesRouter', () => {
  const DOCUMENT_WITH_HEAD = `import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html>
      <Head></Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}`;

  it('returns failure when no _document file exists', () => {
    const result = transformNextPagesRouter(tempDirectory, 'pages');
    expect(result.success).toBe(false);
    expect(result.message).toContain('Could not find');
  });

  it('injects script inside Head tag', () => {
    mkdirSync(join(tempDirectory, 'pages'));
    writeFileSync(join(tempDirectory, 'pages', '_document.tsx'), DOCUMENT_WITH_HEAD);

    const result = transformNextPagesRouter(tempDirectory, 'pages');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
    expect(result.newContent).toContain('<Head>');
  });

  it('reports already installed when react-scan is in content', () => {
    mkdirSync(join(tempDirectory, 'pages'));
    writeFileSync(
      join(tempDirectory, 'pages', '_document.tsx'),
      DOCUMENT_WITH_HEAD.replace('<Head>', '<Head><script src="react-scan" />'),
    );

    const result = transformNextPagesRouter(tempDirectory, 'pages');
    expect(result.success).toBe(true);
    expect(result.noChanges).toBe(true);
  });
});

// --- transformVite ---

describe('transformVite', () => {
  const VITE_INDEX_HTML = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>`;

  it('returns failure when no index.html exists', () => {
    const result = transformVite(tempDirectory);
    expect(result.success).toBe(false);
    expect(result.message).toContain('Could not find index.html');
  });

  it('injects script inside head tag', () => {
    writeFileSync(join(tempDirectory, 'index.html'), VITE_INDEX_HTML);

    const result = transformVite(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.newContent).toContain(VITE_SCRIPT);
    expect(result.newContent).toContain('<head>');
  });

  it('reports already installed when react-scan is in content', () => {
    writeFileSync(
      join(tempDirectory, 'index.html'),
      VITE_INDEX_HTML.replace('<head>', `<head>\n    ${VITE_SCRIPT}`),
    );

    const result = transformVite(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.noChanges).toBe(true);
  });

  it('preserves rest of the html', () => {
    writeFileSync(join(tempDirectory, 'index.html'), VITE_INDEX_HTML);

    const result = transformVite(tempDirectory);
    expect(result.newContent).toContain('<div id="root"></div>');
    expect(result.newContent).toContain('src="/src/main.tsx"');
  });
});

// --- transformWebpack ---

describe('transformWebpack', () => {
  const WEBPACK_INDEX_HTML = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>`;

  const WEBPACK_ENTRY = `import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(<App />);`;

  it('injects script tag into index.html when it exists', () => {
    mkdirSync(join(tempDirectory, 'public'));
    writeFileSync(join(tempDirectory, 'public', 'index.html'), WEBPACK_INDEX_HTML);

    const result = transformWebpack(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.newContent).toContain(REACT_SCAN_SCRIPT_TAG);
  });

  it('falls back to entry file import when no index.html exists', () => {
    mkdirSync(join(tempDirectory, 'src'));
    writeFileSync(join(tempDirectory, 'src', 'index.tsx'), WEBPACK_ENTRY);

    const result = transformWebpack(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.newContent).toContain(WEBPACK_IMPORT);
    expect(result.newContent).toContain(WEBPACK_ENTRY);
  });

  it('returns failure when no index.html or entry file exists', () => {
    const result = transformWebpack(tempDirectory);
    expect(result.success).toBe(false);
    expect(result.message).toContain('Could not find');
  });

  it('reports already installed via index.html', () => {
    mkdirSync(join(tempDirectory, 'public'));
    writeFileSync(
      join(tempDirectory, 'public', 'index.html'),
      WEBPACK_INDEX_HTML.replace('<head>', `<head>\n    ${REACT_SCAN_SCRIPT_TAG}`),
    );

    const result = transformWebpack(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.noChanges).toBe(true);
  });

  it('reports already installed via entry file', () => {
    mkdirSync(join(tempDirectory, 'src'));
    writeFileSync(
      join(tempDirectory, 'src', 'index.tsx'),
      `import("react-scan");\n${WEBPACK_ENTRY}`,
    );

    const result = transformWebpack(tempDirectory);
    expect(result.success).toBe(true);
    expect(result.noChanges).toBe(true);
  });
});

// --- previewTransform ---

describe('previewTransform', () => {
  it('routes to Next.js app router transform', () => {
    mkdirSync(join(tempDirectory, 'app'));
    writeFileSync(
      join(tempDirectory, 'app', 'layout.tsx'),
      '<html><body></body></html>',
    );

    const result = previewTransform(tempDirectory, 'next', 'app');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
  });

  it('routes to Next.js pages router transform', () => {
    mkdirSync(join(tempDirectory, 'pages'));
    writeFileSync(
      join(tempDirectory, 'pages', '_document.tsx'),
      '<Html><Head></Head><body></body></Html>',
    );

    const result = previewTransform(tempDirectory, 'next', 'pages');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
  });

  it('routes to Vite transform', () => {
    writeFileSync(
      join(tempDirectory, 'index.html'),
      '<html><head></head><body></body></html>',
    );

    const result = previewTransform(tempDirectory, 'vite', 'unknown');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
  });

  it('routes to Webpack transform', () => {
    mkdirSync(join(tempDirectory, 'public'));
    writeFileSync(
      join(tempDirectory, 'public', 'index.html'),
      '<html><head></head><body></body></html>',
    );

    const result = previewTransform(tempDirectory, 'webpack', 'unknown');
    expect(result.success).toBe(true);
    expect(result.newContent).toContain('react-scan');
  });

  it('returns failure for tanstack framework', () => {
    const result = previewTransform(tempDirectory, 'tanstack', 'unknown');
    expect(result.success).toBe(false);
    expect(result.message).toContain('not yet supported');
  });

  it('returns failure for unknown framework', () => {
    const result = previewTransform(tempDirectory, 'unknown', 'unknown');
    expect(result.success).toBe(false);
    expect(result.message).toContain('not yet supported');
  });
});

// --- generateDiff ---

describe('generateDiff', () => {
  it('returns empty diff for identical strings', () => {
    const diff = generateDiff('hello\nworld', 'hello\nworld');
    expect(diff).toEqual([
      { type: 'unchanged', content: 'hello' },
      { type: 'unchanged', content: 'world' },
    ]);
  });

  it('detects added lines', () => {
    const diff = generateDiff('line1\nline3', 'line1\nline2\nline3');
    const addedLines = diff.filter((diffLine) => diffLine.type === 'added');
    expect(addedLines.length).toBeGreaterThan(0);
    expect(addedLines.some((diffLine) => diffLine.content === 'line2')).toBe(true);
  });

  it('detects removed lines', () => {
    const diff = generateDiff('line1\nline2\nline3', 'line1\nline3');
    const removedLines = diff.filter((diffLine) => diffLine.type === 'removed');
    expect(removedLines.length).toBeGreaterThan(0);
    expect(removedLines.some((diffLine) => diffLine.content === 'line2')).toBe(true);
  });

  it('detects replaced lines', () => {
    const diff = generateDiff('hello', 'goodbye');
    expect(diff).toEqual([
      { type: 'removed', content: 'hello' },
      { type: 'added', content: 'goodbye' },
    ]);
  });

  it('handles empty original', () => {
    const diff = generateDiff('', 'new line');
    expect(diff).toEqual([
      { type: 'removed', content: '' },
      { type: 'added', content: 'new line' },
    ]);
  });

  it('handles empty updated', () => {
    const diff = generateDiff('old line', '');
    expect(diff).toEqual([
      { type: 'removed', content: 'old line' },
      { type: 'added', content: '' },
    ]);
  });

  it('handles multi-line additions in the middle', () => {
    const original = '<head>\n</head>';
    const updated = '<head>\n  <script src="react-scan"></script>\n</head>';
    const diff = generateDiff(original, updated);

    const addedLines = diff.filter((diffLine) => diffLine.type === 'added');
    expect(addedLines.length).toBe(1);
    expect(addedLines[0].content).toContain('react-scan');
  });
});


================================================
FILE: packages/scan/src/cli.mts
================================================
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { Command } from 'commander';
import pc from 'picocolors';
import prompts from 'prompts';
import {
  type DiffLine,
  type PackageManager,
  FRAMEWORK_NAMES,
  INSTALL_COMMANDS,
  detectProject,
  generateDiff,
  previewTransform,
} from './cli-utils.mjs';

const VERSION = process.env.NPM_PACKAGE_VERSION ?? '0.0.0';

// --- Diff ---

const printDiff = (filePath: string, original: string, updated: string): void => {
  const diff = generateDiff(original, updated);
  const contextLines = 3;
  const changedIndices = diff
    .map((line: DiffLine, i: number) => (line.type !== 'unchanged' ? i : -1))
    .filter((i: number) => i !== -1);

  if (changedIndices.length === 0) {
    console.log(pc.dim('  No changes'));
    return;
  }

  console.log(`\n${pc.bold(`File: ${filePath}`)}`);
  console.log(pc.dim('─'.repeat(60)));

  let lastPrintedIdx = -1;

  for (const changedIdx of changedIndices) {
    const start = Math.max(0, changedIdx - contextLines);
    const end = Math.min(diff.length - 1, changedIdx + contextLines);

    if (start > lastPrintedIdx + 1 && lastPrintedIdx !== -1) {
      console.log(pc.dim('  ...'));
    }

    for (let i = Math.max(start, lastPrintedIdx + 1); i <= end; i++) {
      const line = diff[i];
      if (line.type === 'added') {
        console.log(pc.green(`+ ${line.content}`));
      } else if (line.type === 'removed') {
        console.log(pc.red(`- ${line.content}`));
      } else {
        console.log(pc.dim(`  ${line.content}`));
      }
      lastPrintedIdx = i;
    }
  }

  console.log(pc.dim('─'.repeat(60)));
};

// --- Install ---

const installPackages = (
  packages: string[],
  packageManager: PackageManager,
  projectRoot: string,
): void => {
  if (packages.length === 0) return;

  const command = `${INSTALL_COMMANDS[packageManager]} ${packages.join(' ')}`;
  console.log(pc.dim(`  Running: ${command}\n`));

  execSync(command, {
    cwd: projectRoot,
    stdio: 'inherit',
  });
};

// --- Main ---

const program = new Command()
  .name('react-scan')
  .description('React Scan CLI')
  .version(VERSION);

program
  .command('init')
  .description('Set up React Scan in your project')
  .option('-y, --yes', 'skip confirmation prompts', false)
  .option('-c, --cwd <cwd>', 'working directory', process.cwd())
  .option('--skip-install', 'skip package installation', false)
  .action(async (opts) => {
    console.log(`\n${pc.magenta('[·]')} ${pc.bold('React Scan')} ${pc.dim(`v${VERSION}`)}\n`);

    try {
      const cwd = resolve(opts.cwd);

      if (!existsSync(cwd)) {
        console.error(pc.red(`Directory does not exist: ${cwd}`));
        process.exit(1);
      }

      if (!existsSync(join(cwd, 'package.json'))) {
        console.error(pc.red('No package.json found. Run this command from a project root.'));
        process.exit(1);
      }

      console.log(pc.dim('  Detecting project...\n'));

      const project = detectProject(cwd);

      if (project.framework === 'unknown') {
        console.error(pc.red('  Could not detect a supported framework.'));
        console.log(pc.dim('  React Scan supports Next.js, Vite, and Webpack projects.'));
        console.log(pc.dim('  Visit https://github.com/aidenybai/react-scan#install for manual setup.\n'));
        process.exit(1);
      }

      console.log(`  Framework:       ${pc.cyan(FRAMEWORK_NAMES[project.framework])}`);
      if (project.framework === 'next') {
        console.log(`  Router:          ${pc.cyan(project.nextRouterType === 'app' ? 'App Router' : 'Pages Router')}`);
      }
      console.log(`  Package manager: ${pc.cyan(project.packageManager)}`);
      console.log();

      if (project.hasReactScan) {
        console.log(pc.green('  React Scan is already installed in package.json.'));
        console.log(pc.dim('  Checking if code setup is needed...\n'));
      }

      const result = previewTransform(cwd, project.framework, project.nextRouterType);

      if (!result.success) {
        console.error(pc.red(`  ${result.message}\n`));
        process.exit(1);
      }

      const hasCodeChanges = !result.noChanges && result.originalContent && result.newContent;

      if (hasCodeChanges) {
        printDiff(
          relative(cwd, result.filePath),
          result.originalContent!,
          result.newContent!,
        );

        console.log();
        console.log(pc.yellow('  Auto-detection may not be 100% accurate.'));
        console.log(pc.yellow('  Please verify the changes before committing.\n'));

        if (!opts.yes) {
          const { proceed } = await prompts({
            type: 'confirm',
            name: 'proceed',
            message: 'Apply these changes?',
            initial: true,
          });

          if (!proceed) {
            console.log(pc.dim('\n  Changes cancelled.\n'));
            process.exit(0);
          }
        }
      }

      if (!opts.skipInstall && !project.hasReactScan) {
        console.log(pc.dim('\n  Installing react-scan...\n'));
        installPackages(['react-scan'], project.packageManager, cwd);
        console.log();
      }

      if (hasCodeChanges) {
        writeFileSync(result.filePath, result.newContent!, 'utf-8');
        console.log(pc.green(`  Updated ${relative(cwd, result.filePath)}`));
      }

      if (!hasCodeChanges && project.hasReactScan) {
        console.log(pc.green('  React Scan is already set up in your project.\n'));
        process.exit(0);
      }

      console.log();
      console.log(`${pc.green('  Success!')} React Scan has been installed.`);
      console.log(pc.dim('  You may now start your development server.\n'));
    } catch (error) {
      console.error(pc.red(`\n  Error: ${error instanceof Error ? error.message : String(error)}\n`));
      process.exit(1);
    }
  });

program.parse();


================================================
FILE: packages/scan/src/core/all-environments.ts
================================================
import { ReactScanInternals, scan as innerScan } from '.';

export const scan = /*#__PURE__*/ (...params: Parameters<typeof innerScan>) => {
  if (typeof window !== 'undefined') {
    ReactScanInternals.runInAllEnvironments = true;
    innerScan(...params);
  }
};


================================================
FILE: packages/scan/src/core/fast-serialize.test.ts
================================================
import { describe, expect, it } from 'vitest';
import { fastSerialize } from '~core/instrumentation';

describe('fastSerialize', () => {
  it('serializes null', () => {
    expect(fastSerialize(null)).toBe('null');
  });

  it('serializes undefined', () => {
    expect(fastSerialize(undefined)).toBe('undefined');
  });

  it('serializes strings', () => {
    expect(fastSerialize('hello')).toBe('hello');
    expect(fastSerialize('')).toBe('');
  });

  it('serializes numbers', () => {
    expect(fastSerialize(42)).toBe('42');
    expect(fastSerialize(0)).toBe('0');
    expect(fastSerialize(Number.NaN)).toBe('NaN');
  });

  it('serializes booleans', () => {
    expect(fastSerialize(true)).toBe('true');
    expect(fastSerialize(false)).toBe('false');
  });

  it('serializes functions', () => {
    const testFunc = (_x: 2) => 3;
    expect(fastSerialize(testFunc)).toBe('(_x) => 3');
  });

  it('serializes arrays', () => {
    expect(fastSerialize([])).toBe('[]');
    expect(fastSerialize([1, 2, 3])).toBe('[3]');
  });

  it('serializes plain objects', () => {
    expect(fastSerialize({})).toBe('{}');
    expect(fastSerialize({ a: 1, b: 2 })).toBe('{2}');
  });

  it('serializes deeply nested objects with depth limit', () => {
    const nested = { a: { b: { c: 1 } } };
    expect(fastSerialize(nested, 0)).toBe('{1}');
    expect(fastSerialize(nested, -1)).toBe('…');
  });

  it('serializes objects with custom constructors', () => {
    class CustomClass {}
    const instance = new CustomClass();
    expect(fastSerialize(instance)).toBe('CustomClass{…}');
  });

  it('serializes unknown objects gracefully', () => {
    const date = new Date();
    const serialized = fastSerialize(date);
    expect(serialized.includes('Date')).toBe(true);
  });
});


================================================
FILE: packages/scan/src/core/index.ts
================================================
import { type Signal, signal } from '@preact/signals';
import {
  type Fiber,
  detectReactBuildType,
  getRDTHook,
  getType,
  isInstrumentationActive,
} from 'bippy';
import type { ComponentType } from 'preact';
import type { ReactNode } from 'preact/compat';
import type { RenderData } from 'src/core/utils';
import { initReactScanInstrumentation } from 'src/new-outlines';
import styles from '~web/assets/css/styles.css';
import { createToolbar } from '~web/toolbar';
import { IS_CLIENT } from '~web/utils/constants';
import { readLocalStorage, saveLocalStorage } from '~web/utils/helpers';
import type { States } from '~web/views/inspector/utils';
import type {
  ChangeReason,
  Render,
  createInstrumentation,
} from './instrumentation';
import { startTimingTracking } from './notifications/event-tracking';
import { createHighlightCanvas } from './notifications/outline-overlay';
import packageJson from '../../package.json';

let rootContainer: HTMLDivElement | null = null;
let shadowRoot: ShadowRoot | null = null;

interface RootContainer {
  rootContainer: HTMLDivElement;
  shadowRoot: ShadowRoot;
}

const initRootContainer = (): RootContainer => {
  if (rootContainer && shadowRoot) {
    return { rootContainer, shadowRoot };
  }

  rootContainer = document.createElement('div');
  rootContainer.id = 'react-scan-root';

  shadowRoot = rootContainer.attachShadow({ mode: 'open' });

  const cssStyles = document.createElement('style');
  cssStyles.textContent = styles;

  shadowRoot.appendChild(cssStyles);

  document.documentElement.appendChild(rootContainer);

  return { rootContainer, shadowRoot };
};

export interface Options {
  /**
   * Enable/disable scanning
   *
   * Please use the recommended way:
   * enabled: process.env.NODE_ENV === 'development',
   *
   * @default true
   */
  enabled?: boolean;

  /**
   * Force React Scan to run in production (not recommended)
   *
   * @default false
   */
  dangerouslyForceRunInProduction?: boolean;
  /**
   * Log renders to the console
   *
   * WARNING: This can add significant overhead when the app re-renders frequently
   *
   * @default false
   */
  log?: boolean;

  /**
   * Show toolbar bar
   *
   * If you set this to true, and set {@link enabled} to false, the toolbar will still show, but scanning will be disabled.
   *
   * @default true
   */
  showToolbar?: boolean;

  /**
   * Animation speed
   *
   * @default "fast"
   */
  animationSpeed?: 'slow' | 'fast' | 'off';

  /**
   * Track unnecessary renders, and mark their outlines gray when detected
   *
   * An unnecessary render is defined as the component re-rendering with no change to the component's
   * corresponding dom subtree
   *
   *  @default false
   *  @warning tracking unnecessary renders can add meaningful overhead to react-scan
   */
  trackUnnecessaryRenders?: boolean;

  /**
   * Should the FPS meter show in the toolbar
   *
   *  @default true
   */
  showFPS?: boolean;

  /**
   * Should the number of slowdown notifications be shown in the toolbar
   *
   *  @default true
   */
  showNotificationCount?: boolean;

  /**
   * Allow React Scan to run inside iframes
   *
   * @default false
   */
  allowInIframe?: boolean;

  /**
   * Should react scan log internal errors to the console.
   *
   * Useful if react scan is not behaving expected and you want to provide information to maintainers when submitting an issue https://github.com/aidenybai/react-scan/issues
   *
   *  @default false
   */
  _debug?: 'verbose' | false;

  onCommitStart?: () => void;
  onRender?: (fiber: Fiber, renders: Array<Render>) => void;
  onCommitFinish?: () => void;
}

export interface StoreType {
  inspectState: Signal<States>;
  wasDetailsOpen: Signal<boolean>;
  lastReportTime: Signal<number>;
  isInIframe: Signal<boolean>;
  fiberRoots: WeakSet<Fiber>;
  reportData: Map<number, RenderData>;
  legacyReportData: Map<string, RenderData>;
  changesListeners: Map<number, Array<ChangesListener>>;
  interactionListeningForRenders:
    | ((fiber: Fiber, renders: Array<Render>) => void)
    | null;
}

export type OutlineKey = `${string}-${string}`;

export interface Internals {
  instrumentation: ReturnType<typeof createInstrumentation> | null;
  componentAllowList: WeakMap<ComponentType<unknown>, Options> | null;
  options: Signal<Options>;
  onRender: ((fiber: Fiber, renders: Array<Render>) => void) | null;
  Store: StoreType;
  version: string;
  runInAllEnvironments: boolean;
}

export type FunctionalComponentStateChange = {
  type: ChangeReason.FunctionalState;
  value: unknown;
  prevValue?: unknown;
  count?: number | undefined;
  name: string;
};
export type ClassComponentStateChange = {
  type: ChangeReason.ClassState;
  value: unknown;
  prevValue?: unknown;
  count?: number | undefined;
  name: 'state';
};

export type StateChange =
  | FunctionalComponentStateChange
  | ClassComponentStateChange;
export type PropsChange = {
  type: ChangeReason.Props;
  name: string;
  value: unknown;
  prevValue?: unknown;
  count?: number | undefined;
};
export type ContextChange = {
  type: ChangeReason.Context;
  name: string;
  value: unknown;
  prevValue?: unknown;
  count?: number | undefined;
  contextType: number;
};

export type Change = StateChange | PropsChange | ContextChange;

export type ChangesPayload = {
  propsChanges: Array<PropsChange>;
  stateChanges: Array<
    FunctionalComponentStateChange | ClassComponentStateChange
  >;
  contextChanges: Array<ContextChange>;
};
export type ChangesListener = (changes: ChangesPayload) => void;

export const Store: StoreType = {
  wasDetailsOpen: signal(true),
  isInIframe: signal(IS_CLIENT && window.self !== window.top),
  inspectState: signal<States>({
    kind: 'uninitialized',
  }),
  fiberRoots: new Set<Fiber>(),
  reportData: new Map<number, RenderData>(),
  legacyReportData: new Map<string, RenderData>(),
  lastReportTime: signal(0),
  interactionListeningForRenders: null,
  changesListeners: new Map(),
};

export const ReactScanInternals: Internals = {
  instrumentation: null,
  componentAllowList: null,
  options: signal({
    enabled: true,
    log: false,
    showToolbar: true,
    animationSpeed: 'fast',
    dangerouslyForceRunInProduction: false,
    showFPS: true,
    showNotificationCount: true,
    allowInIframe: false,
  }),
  runInAllEnvironments: false,
  onRender: null,
  Store,
  version: packageJson.version,
};

if (IS_CLIENT && window.__REACT_SCAN_EXTENSION__) {
  window.__REACT_SCAN_VERSION__ = ReactScanInternals.version;
}

export type LocalStorageOptions = Omit<
  Options,
  'onCommitStart' | 'onRender' | 'onCommitFinish'
>;

const applyLocalStorageOptions = (options: Options): LocalStorageOptions => {
  const {
    onCommitStart,
    onRender,
    onCommitFinish,
    ...rest
  } = options;
  return rest;
};

const validateOptions = (options: Partial<Options>): Partial<Options> => {
  const errors: Array<string> = [];
  const validOptions: Partial<Options> = {};

  for (const key in options) {
    const value = options[key as keyof Options];
    switch (key) {
      case 'enabled':
      case 'log':
      case 'showToolbar':
      case 'showNotificationCount':
      case 'dangerouslyForceRunInProduction':
      case 'showFPS':
      case 'allowInIframe':
        if (typeof value !== 'boolean') {
          errors.push(`- ${key} must be a boolean. Got "${value}"`);
        } else {
          validOptions[key] = value;
        }
        break;
      case 'animationSpeed':
        if (!['slow', 'fast', 'off'].includes(value as string)) {
          errors.push(
            `- Invalid animation speed "${value}". Using default "fast"`,
          );
        } else {
          validOptions[key] = value as 'slow' | 'fast' | 'off';
        }
        break;
      case 'onCommitStart':
        if (typeof value !== 'function') {
          errors.push(`- ${key} must be a function. Got "${value}"`);
        } else {
          validOptions.onCommitStart = value as () => void;
        }
        break;
      case 'onCommitFinish':
        if (typeof value !== 'function') {
          errors.push(`- ${key} must be a function. Got "${value}"`);
        } else {
          validOptions.onCommitFinish = value as () => void;
        }
        break;
      case 'onRender':
        if (typeof value !== 'function') {
          errors.push(`- ${key} must be a function. Got "${value}"`);
        } else {
          validOptions.onRender = value as (
            fiber: Fiber,
            renders: Array<Render>,
          ) => void;
        }
        break;
      default:
        errors.push(`- Unknown option "${key}"`);
    }
  }

  if (errors.length > 0) {
    // oxlint-disable-next-line no-console
    console.warn(`[React Scan] Invalid options:\n${errors.join('\n')}`);
  }

  return validOptions;
};

export const getReport = (type?: ComponentType<unknown>) => {
  if (type) {
    for (const reportData of Array.from(Store.legacyReportData.values())) {
      if (reportData.type === type) {
        return reportData;
      }
    }
    return null;
  }
  return Store.legacyReportData;
};

export const setOptions = (userOptions: Partial<Options>) => {
  try {
    const validOptions = validateOptions(userOptions);

    if (Object.keys(validOptions).length === 0) {
      return;
    }

    const shouldInitToolbar =
      'showToolbar' in validOptions && validOptions.showToolbar !== undefined;

    const newOptions = {
      ...ReactScanInternals.options.value,
      ...validOptions,
    };

    const { instrumentation } = ReactScanInternals;
    if (instrumentation && 'enabled' in validOptions) {
      instrumentation.isPaused.value = validOptions.enabled === false;
    }

    ReactScanInternals.options.value = newOptions;

    // temp hack since defaults override stored local storage values
    // we actually don't care about any other local storage option other than enabled, we should not be syncing those to local storage
    try {
      const existing = readLocalStorage<undefined | Record<string, unknown>>(
        'react-scan-options',
      )?.enabled;

      if (typeof existing === 'boolean') {
        newOptions.enabled = existing;
      }
    } catch (e) {
      if (ReactScanInternals.options.value._debug === 'verbose') {
        // oxlint-disable-next-line no-console
        console.error(
          '[React Scan Internal Error]',
          'Failed to create notifications outline canvas',
          e,
        );
      }
      /** */
    }

    saveLocalStorage<LocalStorageOptions>(
      'react-scan-options',
      applyLocalStorageOptions(newOptions),
    );

    if (shouldInitToolbar) {
      initToolbar(!!newOptions.showToolbar);
    }

    return newOptions;
  } catch (e) {
    if (ReactScanInternals.options.value._debug === 'verbose') {
      // oxlint-disable-next-line no-console
      console.error(
        '[React Scan Internal Error]',
        'Failed to create notifications outline canvas',
        e,
      );
    }
    /** */
  }
};

export const getOptions = () => ReactScanInternals.options;

// we only need to run this check once and will read the value in hot path
let isProduction: boolean | null = null;
let rdtHook: ReturnType<typeof getRDTHook>;
export const getIsProduction = () => {
  if (isProduction !== null) {
    return isProduction;
  }
  rdtHook ??= getRDTHook();
  for (const renderer of rdtHook.renderers.values()) {
    const buildType = detectReactBuildType(renderer);
    if (buildType === 'production') {
      isProduction = true;
    }
  }
  return isProduction;
};

export const start = () => {
  try {
    if (!IS_CLIENT) {
      return;
    }

    if (
      !ReactScanInternals.runInAllEnvironments &&
      getIsProduction() &&
      !ReactScanInternals.options.value.dangerouslyForceRunInProduction
    ) {
      return;
    }

    const localStorageOptions =
      readLocalStorage<LocalStorageOptions>('react-scan-options');

    if (localStorageOptions) {
      const validLocalOptions = validateOptions(localStorageOptions);

      if (Object.keys(validLocalOptions).length > 0) {
        ReactScanInternals.options.value = {
          ...ReactScanInternals.options.value,
          ...validLocalOptions,
        };
      }
    }

    const options = getOptions();

    initReactScanInstrumentation(() => {
      initToolbar(!!options.value.showToolbar);
    });

    if (IS_CLIENT) {
      setTimeout(() => {
        if (isInstrumentationActive()) return;
        // oxlint-disable-next-line no-console
        console.error(
          '[React Scan] Failed to load. Must import React Scan before React runs.',
        );
      }, 5000);
    }
  } catch (e) {
    if (ReactScanInternals.options.value._debug === 'verbose') {
      // oxlint-disable-next-line no-console
      console.error(
        '[React Scan Internal Error]',
        'Failed to create notifications outline canvas',
        e,
      );
    }
  }
};

const initToolbar = (showToolbar: boolean) => {
  window.reactScanCleanupListeners?.();

  const cleanupTimingTracking = startTimingTracking();
  const cleanupOutlineCanvas = createNotificationsOutlineCanvas();

  window.reactScanCleanupListeners = () => {
    cleanupTimingTracking();
    cleanupOutlineCanvas?.();
  };

  const windowToolbarContainer = window.__REACT_SCAN_TOOLBAR_CONTAINER__;

  if (!showToolbar) {
    windowToolbarContainer?.remove();
    return;
  }

  windowToolbarContainer?.remove();
  const { shadowRoot } = initRootContainer();
  createToolbar(shadowRoot);
};

const createNotificationsOutlineCanvas = () => {
  try {
    const highlightRoot = document.documentElement;
    return createHighlightCanvas(highlightRoot);
  } catch (e) {
    if (ReactScanInternals.options.value._debug === 'verbose') {
      // oxlint-disable-next-line no-console
      console.error(
        '[React Scan Internal Error]',
        'Failed to create notifications outline canvas',
        e,
      );
    }
  }
};

export const scan = (options: Options = {}) => {
  setOptions(options);
  const isInIframe = Store.isInIframe.value;

  if (
    isInIframe &&
    !ReactScanInternals.options.value.allowInIframe &&
    !ReactScanInternals.runInAllEnvironments
  ) {
    return;
  }

  if (options.enabled === false && options.showToolbar !== true) {
    return;
  }

  start();
};

export const useScan = (options: Options = {}) => {
  setOptions(options);
  start();
};

export const onRender = (
  type: unknown,
  _onRender: (fiber: Fiber, renders: Array<Render>) => void,
) => {
  const prevOnRender = ReactScanInternals.onRender;
  ReactScanInternals.onRender = (fiber, renders) => {
    prevOnRender?.(fiber, renders);
    if (getType(fiber.type) === type) {
      _onRender(fiber, renders);
    }
  };
};

export const ignoredProps = new WeakSet<
  Exclude<ReactNode, undefined | null | string | number | boolean | bigint>
>();

export const ignoreScan = (node: ReactNode) => {
  if (node && typeof node === 'object') {
    ignoredProps.add(node);
  }
};


================================================
FILE: packages/scan/src/core/instrumentation.ts
================================================
import { type Signal, signal } from '@preact/signals';
import {
  ClassComponentTag,
  type Fiber,
  type FiberRoot,
  ForwardRefTag,
  FunctionComponentTag,
  MemoComponentTag,
  type MemoizedState,
  SimpleMemoComponentTag,
  didFiberCommit,
  getDisplayName,
  getFiberId,
  getMutatedHostFibers,
  getTimings,
  getType,
  hasMemoCache,
  instrument,
  traverseContexts,
  traverseProps,
  traverseRenderedFibers,
} from 'bippy';
import { isValidElement } from 'preact';
import { isEqual } from '~core/utils';
import {
  collectContextChanges,
  collectPropsChanges,
  collectStateChanges,
} from '~web/views/inspector/timeline/utils';
import {
  type Change,
  type ContextChange,
  ReactScanInternals,
  type StateChange,
} from './index';

export enum RenderPhase {
  Mount = 0b001,
  Update = 0b010,
  Unmount = 0b100,
}

export const RENDER_PHASE_STRING_TO_ENUM = {
  mount: RenderPhase.Mount,
  update: RenderPhase.Update,
  unmount: RenderPhase.Unmount,
} as const;

export interface AggregatedChange {
  type: number;
  unstable: boolean;
}

export interface AggregatedRender {
  name: string;
  frame: number | null;
  phase: number;
  time: number | null;
  aggregatedCount: number;
  forget: boolean;
  changes: AggregatedChange;
  unnecessary: boolean | null;
  didCommit: boolean;
  fps: number;
  computedKey: import('./index').OutlineKey | null;
  computedCurrent: DOMRect | null;
}

let fps = 0;
let lastTime = performance.now();
let frameCount = 0;
let initedFps = false;

const updateFPS = () => {
  frameCount++;
  const now = performance.now();
  if (now - lastTime >= 1000) {
    fps = frameCount;
    frameCount = 0;
    lastTime = now;
  }
  requestAnimationFrame(updateFPS);
};

export const getFPS = () => {
  if (!initedFps) {
    initedFps = true;
    updateFPS();
    fps = 60;
  }

  return fps;
};

export const isElementVisible = (el: Element) => {
  const style = window.getComputedStyle(el);
  return (
    style.display !== 'none' &&
    style.visibility !== 'hidden' &&
    style.contentVisibility !== 'hidden' &&
    style.opacity !== '0'
  );
};

export const isValueUnstable = (prevValue: unknown, nextValue: unknown) => {
  const prevValueString = fastSerialize(prevValue);
  const nextValueString = fastSerialize(nextValue);
  return (
    prevValueString === nextValueString &&
    unstableTypes.includes(typeof prevValue) &&
    unstableTypes.includes(typeof nextValue)
  );
};

export const isElementInViewport = (
  el: Element,
  rect = el.getBoundingClientRect(),
) => {
  const isVisible =
    rect.bottom > 0 &&
    rect.right > 0 &&
    rect.top < window.innerHeight &&
    rect.left < window.innerWidth;

  return isVisible && rect.width && rect.height;
};

export const enum ChangeReason {
  Props = 0b001,
  FunctionalState = 0b010,
  ClassState = 0b011,
  Context = 0b100,
}

export interface AggregatedChange {
  type: number; // union of AggregatedChangeReason
  unstable: boolean;
}

export interface Render {
  phase: RenderPhase;
  componentName: string | null;
  time: number | null;
  count: number;
  forget: boolean;
  changes: Array<Change>;
  unnecessary: boolean | null;
  didCommit: boolean;
  fps: number;
}

const unstableTypes = ['function', 'object'];

const cache = new WeakMap<object, string>();

export function fastSerialize(value: unknown, depth = 0): string {
  if (depth < 0) return '…';

  switch (typeof value) {
    case 'function':
      return value.toString();
    case 'string':
      return value;
    case 'number':
    case 'boolean':
    case 'undefined':
      return String(value);
    case 'object':
      break;
    default:
      return String(value);
  }

  if (value === null) return 'null';

  if (cache.has(value)) {
    const cached = cache.get(value);
    if (cached !== undefined) {
      return cached;
    }
  }

  if (Array.isArray(value)) {
    const str = value.length ? `[${value.length}]` : '[]';
    cache.set(value, str);
    return str;
  }

  if (isValidElement(value)) {
    const type = getDisplayName(value.type) ?? '';
    const propCount = value.props ? Object.keys(value.props).length : 0;
    const str = `<${type} ${propCount}>`;
    cache.set(value, str);
    return str;
  }

  if (Object.getPrototypeOf(value) === Object.prototype) {
    const keys = Object.keys(value);
    const str = keys.length ? `{${keys.length}}` : '{}';
    cache.set(value, str);
    return str;
  }

  const ctor =
    value && typeof value === 'object' ? value.constructor : undefined;
  if (ctor && typeof ctor === 'function' && ctor.name) {
    const str = `${ctor.name}{…}`;
    cache.set(value, str);
    return str;
  }

  const tagString = Object.prototype.toString.call(value).slice(8, -1);
  const str = `${tagString}{…}`;
  cache.set(value, str);
  return str;
}

export const getStateChanges = (fiber: Fiber): StateChange[] => {
  if (!fiber) return [];
  const changes: StateChange[] = [];

  if (
    fiber.tag === FunctionComponentTag ||
    fiber.tag === ForwardRefTag ||
    fiber.tag === SimpleMemoComponentTag ||
    fiber.tag === MemoComponentTag
  ) {
    let memoizedState: MemoizedState | null = fiber.memoizedState;
    let prevState: MemoizedState | null | undefined =
      fiber.alternate?.memoizedState;
Download .txt
gitextract_xag52wf6/

├── .changeset/
│   ├── README.md
│   └── config.json
├── .github/
│   ├── CODE_OF_CONDUCT.md
│   └── workflows/
│       ├── build-extension.yml
│       └── pkg-pr-new.yaml
├── .gitignore
├── .npmrc
├── .oxlintrc.json
├── .vscode/
│   └── settings.json
├── AGENTS.md
├── BROWSER_EXTENSION_GUIDE.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│   ├── generate-certs.sh
│   └── serve-scan.sh
├── docs/
│   └── installation/
│       ├── astro.md
│       ├── cdn.md
│       ├── create-react-app.md
│       ├── next-js-app-router.md
│       ├── next-js-page-router.md
│       ├── parcel.md
│       ├── react-router.md
│       ├── remix.md
│       ├── rsbuild.md
│       ├── tanstack-start.md
│       └── vite.md
├── e2e/
│   ├── helpers.ts
│   ├── inspector.spec.ts
│   ├── notifications.spec.ts
│   ├── outlines.spec.ts
│   └── toolbar.spec.ts
├── kitchen-sink/
│   ├── index.html
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── src/
│   │   ├── examples/
│   │   │   ├── e2e-fixture/
│   │   │   │   └── index.tsx
│   │   │   ├── sierpinski/
│   │   │   │   ├── index.tsx
│   │   │   │   └── styles.css
│   │   │   └── todo-list/
│   │   │       ├── index.tsx
│   │   │       └── styles.css
│   │   ├── index.css
│   │   ├── index.tsx
│   │   ├── main.css
│   │   └── main.tsx
│   ├── tailwind.config.mjs
│   ├── tsconfig.json
│   └── vite.config.ts
├── package.json
├── packages/
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── assets/
│   │   │   │   └── css/
│   │   │   │       └── no-react.css
│   │   │   ├── background/
│   │   │   │   ├── icon.ts
│   │   │   │   └── index.ts
│   │   │   ├── content/
│   │   │   │   └── index.ts
│   │   │   ├── inject/
│   │   │   │   ├── index.ts
│   │   │   │   ├── notification.ts
│   │   │   │   └── react-scan.ts
│   │   │   ├── manifest.chrome.json
│   │   │   ├── manifest.firefox.json
│   │   │   ├── types/
│   │   │   │   ├── global.d.ts
│   │   │   │   └── messages.ts
│   │   │   ├── utils/
│   │   │   │   ├── constants.ts
│   │   │   │   └── helpers.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── scan/
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── auto.d.ts
│   │   ├── bin/
│   │   │   └── cli.js
│   │   ├── global.d.ts
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── postcss.rem2px.mjs
│   │   ├── scripts/
│   │   │   └── bump-version.js
│   │   ├── src/
│   │   │   ├── auto.ts
│   │   │   ├── cli-utils.mts
│   │   │   ├── cli-utils.test.mts
│   │   │   ├── cli.mts
│   │   │   ├── core/
│   │   │   │   ├── all-environments.ts
│   │   │   │   ├── fast-serialize.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── instrumentation.ts
│   │   │   │   ├── notifications/
│   │   │   │   │   ├── event-tracking.ts
│   │   │   │   │   ├── interaction-store.ts
│   │   │   │   │   ├── outline-overlay.ts
│   │   │   │   │   ├── performance-store.ts
│   │   │   │   │   ├── performance-utils.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── types.ts
│   │   │   │   └── utils.ts
│   │   │   ├── index.ts
│   │   │   ├── install-hook.ts
│   │   │   ├── monitoring/
│   │   │   │   └── next.ts
│   │   │   ├── new-outlines/
│   │   │   │   ├── canvas.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── offscreen-canvas.worker.ts
│   │   │   │   └── types.ts
│   │   │   ├── polyfills.ts
│   │   │   ├── react-component-name/
│   │   │   │   ├── __tests__/
│   │   │   │   │   ├── arrow-function.test.ts
│   │   │   │   │   ├── complex-patterns.test.ts
│   │   │   │   │   ├── function-declarations.test.ts
│   │   │   │   │   ├── general-cases.test.ts
│   │   │   │   │   ├── react-patterns.test.ts
│   │   │   │   │   ├── ts-patterns.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── astro.ts
│   │   │   │   ├── babel/
│   │   │   │   │   ├── get-descriptive-name.ts
│   │   │   │   │   ├── get-root-statement-path.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── is-componentish-name.ts
│   │   │   │   │   ├── is-nested-expression.ts
│   │   │   │   │   ├── is-path-valid.ts
│   │   │   │   │   ├── is-statement-top-level.ts
│   │   │   │   │   ├── path-references-import.ts
│   │   │   │   │   └── unwrap.ts
│   │   │   │   ├── core/
│   │   │   │   │   └── options.ts
│   │   │   │   ├── esbuild.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── loader.ts
│   │   │   │   ├── rolldown.ts
│   │   │   │   ├── rollup.ts
│   │   │   │   ├── rspack.ts
│   │   │   │   ├── tsconfig.json
│   │   │   │   ├── vite.ts
│   │   │   │   └── webpack.ts
│   │   │   ├── types.d.ts
│   │   │   ├── types.ts
│   │   │   ├── web/
│   │   │   │   ├── assets/
│   │   │   │   │   └── css/
│   │   │   │   │       └── styles.tailwind.css
│   │   │   │   ├── components/
│   │   │   │   │   ├── copy-to-clipboard/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── icon/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── slider/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── sticky-section/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── svg-sprite/
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   └── toggle/
│   │   │   │   │       └── index.tsx
│   │   │   │   ├── constants.ts
│   │   │   │   ├── hooks/
│   │   │   │   │   ├── use-delayed-value.ts
│   │   │   │   │   ├── use-merged-refs.ts
│   │   │   │   │   └── use-virtual-list.ts
│   │   │   │   ├── state.ts
│   │   │   │   ├── toolbar.tsx
│   │   │   │   ├── utils/
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── create-store.ts
│   │   │   │   │   ├── geiger.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── log.ts
│   │   │   │   │   ├── pin.ts
│   │   │   │   │   └── preact/
│   │   │   │   │       └── constant.ts
│   │   │   │   ├── views/
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── inspector/
│   │   │   │   │   │   ├── components-tree/
│   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   └── state.ts
│   │   │   │   │   │   ├── diff-value.tsx
│   │   │   │   │   │   ├── flash-overlay.ts
│   │   │   │   │   │   ├── header.tsx
│   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   ├── logging.ts
│   │   │   │   │   │   ├── overlay/
│   │   │   │   │   │   │   └── index.tsx
│   │   │   │   │   │   ├── properties.tsx
│   │   │   │   │   │   ├── states.ts
│   │   │   │   │   │   ├── timeline/
│   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   └── utils.ts
│   │   │   │   │   │   ├── utils.ts
│   │   │   │   │   │   ├── what-changed.tsx
│   │   │   │   │   │   └── whats-changed/
│   │   │   │   │   │       └── use-change-store.ts
│   │   │   │   │   ├── notifications/
│   │   │   │   │   │   ├── collapsed-event.tsx
│   │   │   │   │   │   ├── data.ts
│   │   │   │   │   │   ├── details-routes.tsx
│   │   │   │   │   │   ├── icons.tsx
│   │   │   │   │   │   ├── notification-header.tsx
│   │   │   │   │   │   ├── notification-tabs.tsx
│   │   │   │   │   │   ├── notifications.tsx
│   │   │   │   │   │   ├── optimize.tsx
│   │   │   │   │   │   ├── other-visualization.tsx
│   │   │   │   │   │   ├── popover.tsx
│   │   │   │   │   │   ├── render-bar-chart.tsx
│   │   │   │   │   │   ├── render-explanation.tsx
│   │   │   │   │   │   └── slowdown-history.tsx
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   └── header.tsx
│   │   │   │   │   └── toolbar/
│   │   │   │   │       └── index.tsx
│   │   │   │   └── widget/
│   │   │   │       ├── fps-meter.tsx
│   │   │   │       ├── header.tsx
│   │   │   │       ├── helpers.ts
│   │   │   │       ├── index.tsx
│   │   │   │       ├── resize-handle.tsx
│   │   │   │       └── types.ts
│   │   │   └── worker-shim.ts
│   │   ├── tailwind.config.mjs
│   │   ├── tsconfig.json
│   │   ├── tsup.config.ts
│   │   ├── vite.config.mts
│   │   └── worker-plugin.ts
│   ├── vite-plugin-react-scan/
│   │   ├── .npmignore
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── global.d.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── website/
│       ├── .gitignore
│       ├── .oxlintrc.json
│       ├── AGENTS.md
│       ├── README.md
│       ├── app/
│       │   ├── api/
│       │   │   └── waitlist/
│       │   │       └── route.ts
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   ├── react-scan.ts
│       │   └── replay/
│       │       └── page.tsx
│       ├── components/
│       │   ├── cli.tsx
│       │   ├── code.tsx
│       │   ├── companies.tsx
│       │   ├── counter.tsx
│       │   ├── footer.tsx
│       │   ├── header.tsx
│       │   ├── icons/
│       │   │   ├── icon-discord.tsx
│       │   │   ├── icon-github.tsx
│       │   │   └── types.ts
│       │   ├── install-guide.tsx
│       │   ├── test-data-types.tsx
│       │   └── todo-demo.tsx
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── public/
│       │   └── auto.global.js
│       ├── tailwind.config.ts
│       └── tsconfig.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── scripts/
│   ├── build-worker.ts
│   ├── bump-version.js
│   ├── version-warning.mjs
│   └── workspace.mjs
└── tsconfig.json
Download .txt
SYMBOL INDEX (524 symbols across 97 files)

FILE: e2e/helpers.ts
  constant FIXTURE_URL (line 3) | const FIXTURE_URL = '/?example=e2e-fixture';
  function gotoFixture (line 5) | async function gotoFixture(page: Page): Promise<void> {
  function getRenderCount (line 36) | async function getRenderCount(page: Page): Promise<number> {
  function waitForRenders (line 40) | async function waitForRenders(
  function isReactScanActive (line 70) | async function isReactScanActive(page: Page): Promise<boolean> {
  function hasShadowRoot (line 76) | async function hasShadowRoot(page: Page): Promise<boolean> {

FILE: e2e/outlines.spec.ts
  function clickAndCountRenders (line 4) | async function clickAndCountRenders(

FILE: kitchen-sink/src/examples/e2e-fixture/index.tsx
  function Counter (line 12) | function Counter(): JSX.Element {
  function UnstableProps (line 24) | function UnstableProps(): JSX.Element {
  function ContextConsumer (line 52) | function ContextConsumer(): JSX.Element {
  function ThemeToggle (line 57) | function ThemeToggle(): JSX.Element {
  function SlowComponent (line 75) | function SlowComponent(): JSX.Element {
  function RapidUpdater (line 97) | function RapidUpdater(): JSX.Element {
  function E2EFixture (line 116) | function E2EFixture(): JSX.Element {

FILE: kitchen-sink/src/examples/sierpinski/index.tsx
  constant TARGET (line 17) | const TARGET = 50;
  type SlowTriangleProps (line 56) | interface SlowTriangleProps {
  type TriangleProps (line 82) | interface TriangleProps {
  type DotProps (line 99) | interface DotProps {
  function App (line 131) | function App(): JSX.Element {

FILE: kitchen-sink/src/examples/todo-list/index.tsx
  type TodoItem (line 12) | interface TodoItem {
  type TodoListItemProps (line 18) | interface TodoListItemProps {
  function TodoListItem (line 23) | function TodoListItem({ item, setList }: TodoListItemProps): JSX.Element {
  type TodoListFormProps (line 61) | interface TodoListFormProps {
  function TodoListForm (line 67) | function TodoListForm({
  function TodoList (line 106) | function TodoList(): JSX.Element {
  function App (line 121) | function App(): JSX.Element {

FILE: kitchen-sink/src/main.tsx
  type Example (line 4) | interface Example {
  function Home (line 14) | function Home(): JSX.Element {

FILE: packages/extension/src/background/icon.ts
  type IconState (line 3) | enum IconState {

FILE: packages/extension/src/types/global.d.ts
  type Window (line 4) | interface Window {

FILE: packages/extension/src/types/messages.ts
  type BroadcastMessage (line 13) | type BroadcastMessage = z.infer<typeof BroadcastSchema>;
  type IEvents (line 15) | interface IEvents {

FILE: packages/extension/src/utils/constants.ts
  constant STORAGE_KEY (line 1) | const STORAGE_KEY = 'react-scan-options';
  constant EXTENSION_STORAGE_KEY (line 2) | const EXTENSION_STORAGE_KEY = 'react-scan-extension';

FILE: packages/extension/src/utils/helpers.ts
  constant IS_CLIENT (line 5) | const IS_CLIENT = typeof window !== 'undefined';
  type ReactRootContainer (line 14) | interface ReactRootContainer {
  type EventCallback (line 189) | type EventCallback<T = unknown> = (data: T) => void;

FILE: packages/extension/vite.config.ts
  constant BROWSER_TYPES (line 7) | const BROWSER_TYPES = {
  type BrowserType (line 13) | type BrowserType = (typeof BROWSER_TYPES)[keyof typeof BROWSER_TYPES];

FILE: packages/scan/postcss.rem2px.mjs
  method prepare (line 17) | prepare() {

FILE: packages/scan/src/core/fast-serialize.test.ts
  class CustomClass (line 51) | class CustomClass {}

FILE: packages/scan/src/core/index.ts
  type RootContainer (line 30) | interface RootContainer {
  type Options (line 55) | interface Options {
  type StoreType (line 143) | interface StoreType {
  type OutlineKey (line 157) | type OutlineKey = `${string}-${string}`;
  type Internals (line 159) | interface Internals {
  type FunctionalComponentStateChange (line 169) | type FunctionalComponentStateChange = {
  type ClassComponentStateChange (line 176) | type ClassComponentStateChange = {
  type StateChange (line 184) | type StateChange =
  type PropsChange (line 187) | type PropsChange = {
  type ContextChange (line 194) | type ContextChange = {
  type Change (line 203) | type Change = StateChange | PropsChange | ContextChange;
  type ChangesPayload (line 205) | type ChangesPayload = {
  type ChangesListener (line 212) | type ChangesListener = (changes: ChangesPayload) => void;
  type LocalStorageOptions (line 251) | type LocalStorageOptions = Omit<

FILE: packages/scan/src/core/instrumentation.ts
  type RenderPhase (line 37) | enum RenderPhase {
  constant RENDER_PHASE_STRING_TO_ENUM (line 43) | const RENDER_PHASE_STRING_TO_ENUM = {
  type AggregatedChange (line 49) | interface AggregatedChange {
  type AggregatedRender (line 54) | interface AggregatedRender {
  type ChangeReason (line 128) | const enum ChangeReason {
  type AggregatedChange (line 135) | interface AggregatedChange {
  type Render (line 140) | interface Render {
  function fastSerialize (line 156) | function fastSerialize(value: unknown, depth = 0): string {
  type ContextFiber (line 269) | interface ContextFiber {
  function getContextChangesTraversal (line 286) | function getContextChangesTraversal(
  type OnRenderHandler (line 329) | type OnRenderHandler = (fiber: Fiber, renders: Array<Render>) => void;
  type OnCommitStartHandler (line 330) | type OnCommitStartHandler = () => void;
  type OnCommitFinishHandler (line 331) | type OnCommitFinishHandler = () => void;
  type OnErrorHandler (line 332) | type OnErrorHandler = (error: unknown) => void;
  type IsValidFiberHandler (line 333) | type IsValidFiberHandler = (fiber: Fiber) => boolean;
  type OnActiveHandler (line 334) | type OnActiveHandler = () => void;
  type InstrumentationConfig (line 336) | interface InstrumentationConfig {
  type InstrumentationInstance (line 350) | interface InstrumentationInstance {
  type Instrumentation (line 356) | interface Instrumentation {
  type IsRenderUnnecessaryState (line 366) | interface IsRenderUnnecessaryState {
  function isRenderUnnecessaryTraversal (line 370) | function isRenderUnnecessaryTraversal(
  constant TRACK_UNNECESSARY_RENDERS (line 423) | const TRACK_UNNECESSARY_RENDERS = false;
  type RenderData (line 425) | interface RenderData {
  type OldRenderData (line 432) | interface OldRenderData {
  constant RENDER_DEBOUNCE_MS (line 443) | const RENDER_DEBOUNCE_MS = 16;
  function getFiberIdentifier (line 447) | function getFiberIdentifier(fiber: Fiber) {
  function getRenderData (line 451) | function getRenderData(fiber: Fiber) {
  function setRenderData (line 462) | function setRenderData(fiber: Fiber, value: RenderData) {
  method onCommitFiberRoot (line 527) | onCommitFiberRoot(_rendererID, root) {
  method onPostCommitFiberRoot (line 653) | onPostCommitFiberRoot() {

FILE: packages/scan/src/core/notifications/event-tracking.ts
  type FinalInteraction (line 22) | type FinalInteraction = {
  type NewInteractionStoreState (line 47) | type NewInteractionStoreState = {
  type InteractionEvent (line 75) | type InteractionEvent = {
  type LongRenderPipeline (line 88) | type LongRenderPipeline = {
  type SlowdownEvent (line 101) | type SlowdownEvent = (InteractionEvent | LongRenderPipeline) & {
  type ToolbarEventStoreState (line 105) | type ToolbarEventStoreState = {
  type DebugEvent (line 116) | type DebugEvent = {
  constant EVENT_STORE_CAPACITY (line 152) | const EVENT_STORE_CAPACITY = 200;
  constant HIGH_SEVERITY_FPS_DROP_TIME (line 324) | const HIGH_SEVERITY_FPS_DROP_TIME = 150;
  function startLongPipelineTracking (line 328) | function startLongPipelineTracking() {

FILE: packages/scan/src/core/notifications/interaction-store.ts
  type Subscriber (line 4) | type Subscriber<T> = (data: T) => void;
  class Store (line 6) | class Store<T> {
    method constructor (line 10) | constructor(initialValue: T) {
    method subscribe (line 14) | subscribe(subscriber: Subscriber<T>): () => void {
    method setState (line 24) | setState(data: T) {
    method getCurrentState (line 29) | getCurrentState(): T {
  constant MAX_INTERACTION_BATCH (line 33) | const MAX_INTERACTION_BATCH = 150;

FILE: packages/scan/src/core/notifications/outline-overlay.ts
  type TransitionHighlightState (line 9) | type TransitionHighlightState = {
  type HighlightState (line 22) | type HighlightState =
  constant FADE_SPEED (line 47) | const FADE_SPEED = 1.8;
  constant MAX_DELTA (line 48) | const MAX_DELTA = 0.05;
  constant DEFAULT_DELTA (line 49) | const DEFAULT_DELTA = 1 / 60;
  function cleanup (line 215) | function cleanup() {

FILE: packages/scan/src/core/notifications/performance-store.ts
  type UnSubscribe (line 4) | type UnSubscribe = () => void;
  type Callback (line 5) | type Callback<T> = (item: T) => void;
  type Updater (line 6) | type Updater<T> = (state: BoundedArray<T>) => BoundedArray<T>;
  type ChanelName (line 7) | type ChanelName = string;
  type PerformanceEntryChannelsType (line 9) | type PerformanceEntryChannelsType<T> = {
  constant MAX_CHANNEL_SIZE (line 29) | const MAX_CHANNEL_SIZE = 50;
  class PerformanceEntryChannels (line 33) | class PerformanceEntryChannels<T> implements PerformanceEntryChannelsTyp...
    method publish (line 35) | publish(item: T, to: ChanelName, createIfNoChannel = true) {
    method getAvailableChannels (line 53) | getAvailableChannels() {
    method subscribe (line 56) | subscribe(to: ChanelName, cb: Callback<T>, dropFirst: boolean = false) {
    method updateChannelState (line 86) | updateChannelState(
    method getChannelState (line 111) | getChannelState(channel: ChanelName) {

FILE: packages/scan/src/core/notifications/performance-utils.ts
  type Node (line 16) | type Node = Map<
  constant THROW_INVARIANTS (line 64) | const THROW_INVARIANTS = false;
  class BoundedArray (line 74) | class BoundedArray<T> extends Array<T> {
    method constructor (line 75) | constructor(private capacity: number = 25) {
    method push (line 79) | push(...items: T[]): number {
    method fromArray (line 87) | static fromArray<T>(array: Array<T>, capacity: number) {

FILE: packages/scan/src/core/notifications/performance.ts
  type PathFilters (line 30) | interface PathFilters {
  constant DEFAULT_PATH_FILTERS (line 39) | const DEFAULT_PATH_FILTERS: PathFilters = {
  constant PATH_FILTER_PATTERNS (line 48) | const PATH_FILTER_PATTERNS = {
  type FiberType (line 103) | interface FiberType {
  type FiberRenders (line 177) | type FiberRenders = Record<
  type InteractionStartStage (line 202) | type InteractionStartStage = {
  type JSEndStage (line 218) | type JSEndStage = Omit<InteractionStartStage, 'kind'> & {
  type RAFStage (line 223) | type RAFStage = Omit<JSEndStage, 'kind'> & {
  type TimeoutStage (line 228) | type TimeoutStage = Omit<RAFStage, 'kind'> & {
  type PerformanceEntryChannelEvent (line 234) | type PerformanceEntryChannelEvent =
  type CompletedInteraction (line 245) | type CompletedInteraction = {
  type UnInitializedStage (line 252) | type UnInitializedStage = {
  type CurrentInteraction (line 259) | type CurrentInteraction = {
  type Task (line 440) | type Task = {
  constant MAX_INTERACTION_TASKS (line 449) | const MAX_INTERACTION_TASKS = 25;
  type ShouldContinue (line 508) | type ShouldContinue = boolean;
  type LastInteractionRef (line 576) | type LastInteractionRef = {

FILE: packages/scan/src/core/notifications/types.ts
  type PerformanceInteractionEntry (line 1) | interface PerformanceInteractionEntry extends PerformanceEntry {
  type PerformanceInteraction (line 11) | interface PerformanceInteraction {

FILE: packages/scan/src/core/utils.ts
  function descending (line 42) | function descending(a: number, b: number): number {
  type ComponentData (line 46) | interface ComponentData {
  function getComponentGroupNames (line 52) | function getComponentGroupNames(group: ComponentData[]): string {
  function getComponentGroupTotalTime (line 65) | function getComponentGroupTotalTime(group: ComponentData[]): number {
  function componentGroupHasForget (line 75) | function componentGroupHasForget(group: ComponentData[]): boolean {
  type RenderData (line 166) | interface RenderData {
  function isEqual (line 175) | function isEqual(a: unknown, b: unknown): boolean {

FILE: packages/scan/src/new-outlines/canvas.ts
  constant OUTLINE_ARRAY_SIZE (line 3) | const OUTLINE_ARRAY_SIZE = 7;
  constant MONO_FONT (line 4) | const MONO_FONT =
  constant INTERPOLATION_SPEED (line 7) | const INTERPOLATION_SPEED = 0.2;
  constant SNAP_THRESHOLD (line 8) | const SNAP_THRESHOLD = 0.5;
  constant MAX_PARTS_LENGTH (line 15) | const MAX_PARTS_LENGTH = 4;
  constant MAX_LABEL_LENGTH (line 16) | const MAX_LABEL_LENGTH = 40;
  constant TOTAL_FRAMES (line 17) | const TOTAL_FRAMES = 45;
  constant PRIMARY_COLOR (line 19) | const PRIMARY_COLOR = '115,97,230';
  function sortEntry (line 21) | function sortEntry(prev: [number, string[]], next: [number, string[]]): ...
  function getSortedEntries (line 25) | function getSortedEntries(
  function getLabelTextPart (line 32) | function getLabelTextPart([count, names]: [number, string[]]): string {

FILE: packages/scan/src/new-outlines/index.ts
  type IntersectionState (line 101) | interface IntersectionState {
  function onIntersect (line 108) | function onIntersect(
  constant IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED (line 289) | const IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED =
  method onPostCommitFiberRoot (line 620) | onPostCommitFiberRoot() {

FILE: packages/scan/src/new-outlines/types.ts
  type OutlineData (line 1) | interface OutlineData {
  type InlineOutlineData (line 12) | type InlineOutlineData = [
  type ActiveOutline (line 22) | interface ActiveOutline {
  type BlueprintOutline (line 38) | interface BlueprintOutline {

FILE: packages/scan/src/react-component-name/__tests__/utils.ts
  type TransformFn (line 3) | type TransformFn = (

FILE: packages/scan/src/react-component-name/babel/get-descriptive-name.ts
  function getDescriptiveName (line 3) | function getDescriptiveName(

FILE: packages/scan/src/react-component-name/babel/get-root-statement-path.ts
  function getRootStatementPath (line 4) | function getRootStatementPath(path: babel.NodePath): babel.NodePath {

FILE: packages/scan/src/react-component-name/babel/index.ts
  function getAssignedDisplayNames (line 8) | function getAssignedDisplayNames(path: NodePath<t.Program>): Set<string> {
  function isValidFunction (line 33) | function isValidFunction(
  function assignDisplayName (line 39) | function assignDisplayName(
  constant REACT_CLASS (line 75) | const REACT_CLASS = ['Component', 'PureComponent'];
  function isNamespaceExport (line 77) | function isNamespaceExport(
  function isReactClassComponent (line 99) | function isReactClassComponent(path: NodePath<t.Class>): boolean {
  function isStyledComponent (line 115) | function isStyledComponent(
  constant REACT_FACTORY (line 174) | const REACT_FACTORY = [
  function isReactComponent (line 181) | function isReactComponent(
  method Program (line 236) | Program(path) {

FILE: packages/scan/src/react-component-name/babel/is-componentish-name.ts
  function isComponentishName (line 7) | function isComponentishName(name: string, flags: Options['flags']) {

FILE: packages/scan/src/react-component-name/babel/is-nested-expression.ts
  type NestedExpression (line 2) | type NestedExpression =

FILE: packages/scan/src/react-component-name/babel/is-path-valid.ts
  type TypeFilter (line 4) | type TypeFilter<V extends t.Node> = (node: t.Node) => node is V;

FILE: packages/scan/src/react-component-name/babel/is-statement-top-level.ts
  function isStatementTopLevel (line 4) | function isStatementTopLevel(

FILE: packages/scan/src/react-component-name/babel/unwrap.ts
  type TrueTypeFilter (line 6) | type TrueTypeFilter<U extends t.Node> = (node: t.Node) => node is U;
  type TypeCheck (line 7) | type TypeCheck<K> = K extends TrueTypeFilter<infer U> ? U : never;
  type NodeTypeFilter (line 9) | type NodeTypeFilter = (node: t.Node) => boolean;
  type PathTypeFilter (line 27) | type PathTypeFilter<V extends t.Node> = (node: t.Node) => node is V;

FILE: packages/scan/src/react-component-name/core/options.ts
  type Options (line 3) | interface Options {
  type Overwrite (line 15) | type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
  type OptionsResolved (line 17) | type OptionsResolved = Overwrite<
  function resolveOptions (line 22) | function resolveOptions(options: Options): OptionsResolved {

FILE: packages/scan/src/react-component-name/index.ts
  constant DEFAULT_INCLUDE (line 49) | const DEFAULT_INCLUDE = '**/*.{mtsx,mjsx,tsx,jsx}';
  constant DEFAULT_EXCLUDE (line 50) | const DEFAULT_EXCLUDE = '**/node_modules/**';
  method transform (line 70) | async transform(code, id) {

FILE: packages/scan/src/react-component-name/loader.ts
  type LoaderContext (line 4) | interface LoaderContext {
  function ReactComponentNameLoader (line 14) | async function ReactComponentNameLoader(

FILE: packages/scan/src/types.ts
  type ReactScanInternals (line 3) | type ReactScanInternals = typeof import('./core/index')['ReactScanIntern...
  type Scan (line 4) | type Scan = typeof import('./index')['scan'];
  type ExtendedReactRenderer (line 6) | interface ExtendedReactRenderer {
  type TTimer (line 36) | type TTimer = NodeJS.Timeout;
  type Window (line 38) | interface Window {

FILE: packages/scan/src/web/components/copy-to-clipboard/index.tsx
  type CopyToClipboardProps (line 6) | interface CopyToClipboardProps {

FILE: packages/scan/src/web/components/icon/index.tsx
  type SVGIconProps (line 4) | interface SVGIconProps {

FILE: packages/scan/src/web/components/slider/index.tsx
  type SliderProps (line 4) | interface SliderProps {

FILE: packages/scan/src/web/components/sticky-section/index.tsx
  type StickyRenderProps (line 5) | interface StickyRenderProps {
  type StickyProps (line 11) | interface StickyProps {

FILE: packages/scan/src/web/components/toggle/index.tsx
  type ToggleProps (line 4) | interface ToggleProps extends JSX.HTMLAttributes<HTMLInputElement> {

FILE: packages/scan/src/web/constants.ts
  constant SAFE_AREA (line 1) | const SAFE_AREA = 24;
  constant MIN_SIZE (line 2) | const MIN_SIZE = {
  constant MIN_CONTAINER_WIDTH (line 8) | const MIN_CONTAINER_WIDTH = 240;
  constant LOCALSTORAGE_KEY (line 10) | const LOCALSTORAGE_KEY = "react-scan-widget-settings-v2";
  constant LOCALSTORAGE_COLLAPSED_KEY (line 11) | const LOCALSTORAGE_COLLAPSED_KEY = "react-scan-widget-collapsed-v1";
  constant LOCALSTORAGE_LAST_VIEW_KEY (line 12) | const LOCALSTORAGE_LAST_VIEW_KEY = "react-scan-widget-last-view-v1";

FILE: packages/scan/src/web/hooks/use-merged-refs.ts
  type PossibleRef (line 4) | type PossibleRef<T> = Ref<T> | undefined;

FILE: packages/scan/src/web/hooks/use-virtual-list.ts
  type VirtualItem (line 9) | interface VirtualItem {

FILE: packages/scan/src/web/state.ts
  type SlowDowns (line 85) | interface SlowDowns {
  type WidgetStates (line 90) | type WidgetStates =

FILE: packages/scan/src/web/toolbar.tsx
  class ToolbarErrorBoundary (line 7) | class ToolbarErrorBoundary extends Component {
    method getDerivedStateFromError (line 10) | static getDerivedStateFromError(error: Error) {
    method render (line 18) | render() {

FILE: packages/scan/src/web/utils/constants.ts
  constant IS_CLIENT (line 1) | const IS_CLIENT = typeof window !== 'undefined';

FILE: packages/scan/src/web/utils/create-store.ts
  type SetStateInternal (line 8) | type SetStateInternal<T> = {
  type StoreApi (line 16) | interface StoreApi<T> {
  type ExtractState (line 29) | type ExtractState<S> = S extends { getState: () => infer T } ? T : never;
  type Get (line 31) | type Get<T, K, F> = K extends keyof T ? T[K] : F;
  type Mutate (line 33) | type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
  type StateCreator (line 41) | type StateCreator<
  type StoreMutators (line 53) | interface StoreMutators<S, A> {}
  type StoreMutatorIdentifier (line 54) | type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>;
  type CreateStore (line 56) | type CreateStore = {
  type CreateStoreImpl (line 66) | type CreateStoreImpl = <
  type TState (line 74) | type TState = ReturnType<typeof createState>;
  type Listener (line 75) | type Listener = (state: TState, prevState: TState) => void;

FILE: packages/scan/src/web/utils/helpers.ts
  type WrapperBadge (line 82) | interface WrapperBadge {
  type ExtendedDisplayName (line 88) | interface ExtendedDisplayName {

FILE: packages/scan/src/web/utils/preact/constant.ts
  function CONSTANT_UPDATE (line 8) | function CONSTANT_UPDATE() {
  function constant (line 12) | function constant<P extends Attributes>(

FILE: packages/scan/src/web/views/index.tsx
  type ContentViewProps (line 78) | interface ContentViewProps {

FILE: packages/scan/src/web/views/inspector/components-tree/index.tsx
  type TreeNodeItemProps (line 92) | interface TreeNodeItemProps {
  constant VALID_TYPES (line 102) | const VALID_TYPES = ['memo', 'forwardRef', 'lazy', 'suspense'];

FILE: packages/scan/src/web/views/inspector/components-tree/state.ts
  type TreeNode (line 5) | interface TreeNode {
  type FlattenedNode (line 14) | interface FlattenedNode extends TreeNode {
  type TreeItem (line 31) | interface TreeItem {

FILE: packages/scan/src/web/views/inspector/flash-overlay.ts
  type FlashEntry (line 1) | interface FlashEntry {
  method create (line 28) | create(container: HTMLElement) {
  method cleanup (line 89) | cleanup(container: HTMLElement) {
  method cleanupAll (line 107) | cleanupAll() {

FILE: packages/scan/src/web/views/inspector/index.tsx
  class InspectorErrorBoundary (line 38) | class InspectorErrorBoundary extends Component {
    method getDerivedStateFromError (line 44) | static getDerivedStateFromError(e: Error) {
    method render (line 53) | render() {

FILE: packages/scan/src/web/views/inspector/logging.ts
  function safeStringify (line 4) | function safeStringify(value: unknown): string {

FILE: packages/scan/src/web/views/inspector/overlay/index.tsx
  type DrawKind (line 17) | type DrawKind = 'locked' | 'inspecting';
  type Rect (line 19) | interface Rect {
  type LockIconRect (line 26) | interface LockIconRect {
  constant ANIMATION_CONFIG (line 33) | const ANIMATION_CONFIG = {
  constant OVERLAY_DPR (line 42) | const OVERLAY_DPR = IS_CLIENT

FILE: packages/scan/src/web/views/inspector/properties.tsx
  type ValueMetadata (line 32) | interface ValueMetadata {
  type PropertyElementProps (line 42) | interface PropertyElementProps {
  type PropertySectionProps (line 53) | interface PropertySectionProps {
  type EditableValueProps (line 61) | interface EditableValueProps {

FILE: packages/scan/src/web/views/inspector/states.ts
  type MinimalFiberInfo (line 7) | interface MinimalFiberInfo {
  type TimelineUpdate (line 16) | interface TimelineUpdate {
  type TimelineState (line 25) | interface TimelineState {
  constant TIMELINE_MAX_UPDATES (line 37) | const TIMELINE_MAX_UPDATES = 1000;

FILE: packages/scan/src/web/views/inspector/timeline/index.tsx
  type TimelineProps (line 10) | interface TimelineProps {

FILE: packages/scan/src/web/views/inspector/timeline/utils.ts
  type ChangeTrackingInfo (line 14) | interface ChangeTrackingInfo {
  type ChangeKey (line 21) | type ChangeKey = string | number;
  constant STATE_NAME_REGEX (line 28) | const STATE_NAME_REGEX = /\[(?<name>\w+),\s*set\w+\]/g;
  constant PROPS_ORDER_REGEX (line 29) | const PROPS_ORDER_REGEX = /\(\s*{\s*(?<props>[^}]+)\s*}\s*\)/;
  type SectionData (line 95) | interface SectionData {
  type InspectorData (line 101) | interface InspectorData {
  type InspectorDataResult (line 154) | interface InspectorDataResult {
  type BaseChange (line 159) | interface BaseChange {
  type PropChange (line 165) | interface PropChange extends BaseChange {
  type StateChange (line 169) | interface StateChange extends BaseChange {
  type ContextChange (line 173) | interface ContextChange extends BaseChange {
  type CollectorResult (line 178) | interface CollectorResult<T extends BaseChange = BaseChange> {
  type ContextInfo (line 385) | interface ContextInfo {

FILE: packages/scan/src/web/views/inspector/utils.ts
  type StateItem (line 20) | interface StateItem {
  type States (line 26) | type States =
  type ReactRootContainer (line 43) | interface ReactRootContainer {
  type ReactInternalProps (line 53) | interface ReactInternalProps {
  type OverrideMethods (line 302) | interface OverrideMethods {
  type InspectableElement (line 470) | interface InspectableElement {
  type DiffResult (line 564) | type DiffResult = {
  type DiffChange (line 575) | type DiffChange = {
  type InspectableValue (line 582) | type InspectableValue =
  type AggregatedChanges (line 601) | type AggregatedChanges = {
  function hackyJsFormatter (line 1173) | function hackyJsFormatter(code: string) {
  type TimelineSliderValues (line 1536) | interface TimelineSliderValues {
  type ExtendedMemoizedState (line 1575) | interface ExtendedMemoizedState extends MemoizedState {

FILE: packages/scan/src/web/views/inspector/what-changed.tsx
  type Setter (line 28) | type Setter<T> = Dispatch<StateUpdater<T>>;
  type SectionProps (line 262) | interface SectionProps {

FILE: packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts
  constant CHANGES_QUEUE_INTERVAL (line 12) | const CHANGES_QUEUE_INTERVAL = 50;
  type SectionData (line 14) | interface SectionData {
  type InspectorData (line 19) | interface InspectorData {
  type InspectorState (line 24) | interface InspectorState extends InspectorData {
  type AggregatedChanges (line 35) | type AggregatedChanges = {
  type AllAggregatedChanges (line 44) | type AllAggregatedChanges = {

FILE: packages/scan/src/web/views/notifications/collapsed-event.tsx
  type CollapsedDroppedFrame (line 12) | type CollapsedDroppedFrame = {
  type CollapsedKeyboardInput (line 18) | type CollapsedKeyboardInput = {

FILE: packages/scan/src/web/views/notifications/data.ts
  type GroupedFiberRender (line 8) | type GroupedFiberRender = {
  type DroppedFramesTiming (line 62) | type DroppedFramesTiming = {
  type InteractionTiming (line 67) | type InteractionTiming = {
  type InteractionEvent (line 117) | type InteractionEvent = {
  type DroppedFramesEvent (line 128) | type DroppedFramesEvent = {
  type NotificationEvent (line 138) | type NotificationEvent = InteractionEvent | DroppedFramesEvent;
  type NotificationsState (line 140) | type NotificationsState = {
  constant NOTIFICATIONS_BORDER (line 208) | const NOTIFICATIONS_BORDER = '#27272A';

FILE: packages/scan/src/web/views/notifications/other-visualization.tsx
  type BaseTimeDataItem (line 14) | type BaseTimeDataItem = {
  type TimeData (line 26) | type TimeData = Array<BaseTimeDataItem>;
  type OverviewInput (line 303) | type OverviewInput =

FILE: packages/scan/src/web/views/notifications/popover.tsx
  type PopoverState (line 13) | type PopoverState = 'closed' | 'opening' | 'open' | 'closing';

FILE: packages/scan/src/web/views/notifications/render-bar-chart.tsx
  type Bars (line 53) | type Bars = Array<
  constant NO_PURGE (line 60) | const NO_PURGE = ['hover:bg-[#0f0f0f]'];

FILE: packages/scan/src/web/views/notifications/slowdown-history.tsx
  type CollapsedKeyboardInput (line 202) | type CollapsedKeyboardInput = {
  type HistoryEvent (line 208) | type HistoryEvent =

FILE: packages/scan/src/web/widget/helpers.ts
  class WindowDimensions (line 4) | class WindowDimensions {
    method constructor (line 8) | constructor(
    method rightEdge (line 16) | rightEdge(width: number): number {
    method bottomEdge (line 20) | bottomEdge(height: number): number {
    method isFullWidth (line 24) | isFullWidth(width: number): boolean {
    method isFullHeight (line 28) | isFullHeight(height: number): boolean {

FILE: packages/scan/src/web/widget/index.tsx
  constant COLLAPSED_SIZE (line 38) | const COLLAPSED_SIZE = {

FILE: packages/scan/src/web/widget/types.ts
  type Position (line 1) | interface Position {
  type Size (line 6) | interface Size {
  type Corner (line 11) | type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
  type CollapsedPosition (line 13) | type CollapsedPosition = {
  type ResizeHandleProps (line 18) | interface ResizeHandleProps {
  type WidgetDimensions (line 22) | interface WidgetDimensions {
  type ComponentsTreeConfig (line 30) | interface ComponentsTreeConfig {
  type WidgetConfig (line 34) | interface WidgetConfig {
  type WidgetSettings (line 41) | interface WidgetSettings {

FILE: packages/scan/src/worker-shim.ts
  function createInlineWorker (line 2) | function createInlineWorker(code: string) {

FILE: packages/scan/tsup.config.ts
  constant DIST_PATH (line 9) | const DIST_PATH = './dist';
  method onSuccess (line 136) | async onSuccess() {

FILE: packages/scan/worker-plugin.ts
  method setup (line 10) | setup(build) {

FILE: packages/vite-plugin-react-scan/src/index.ts
  function resolveModuleFileContent (line 9) | async function resolveModuleFileContent(moduleName: string, startDir: st...
  type Logger (line 24) | interface Logger {
  type ReactScanPluginOptions (line 44) | interface ReactScanPluginOptions {
  constant PLUGIN_NAME (line 69) | const PLUGIN_NAME = 'vite-plugin-react-scan';
  constant DEFAULT_SCAN_OPTIONS (line 71) | const DEFAULT_SCAN_OPTIONS: Partial<Options> = {};
  constant JSX_EXTENSIONS (line 94) | const JSX_EXTENSIONS = ['.jsx', '.tsx'] as const;
  constant REACT_SCAN_IDENTIFIER (line 95) | const REACT_SCAN_IDENTIFIER = 'react-scan';
  method config (line 154) | config(config) {
  method configResolved (line 205) | configResolved(resolvedConfig) {
  method transformIndexHtml (line 225) | transformIndexHtml(html) {
  method resolveId (line 274) | resolveId(id) {
  method generateBundle (line 282) | async generateBundle() {
  method buildEnd (line 313) | buildEnd() {

FILE: packages/website/app/api/waitlist/route.ts
  function POST (line 20) | async function POST(request: Request) {

FILE: packages/website/app/layout.tsx
  function RootLayout (line 45) | function RootLayout({ children }: { children: React.ReactNode }) {

FILE: packages/website/app/page.tsx
  function Home (line 9) | function Home() {

FILE: packages/website/app/replay/page.tsx
  function ReplayPage (line 5) | function ReplayPage() {

FILE: packages/website/components/cli.tsx
  function CLI (line 37) | function CLI({ command }: { command: string }) {

FILE: packages/website/components/code.tsx
  function Code (line 1) | function Code({

FILE: packages/website/components/companies.tsx
  constant LOGO_FILTER (line 3) | const LOGO_FILTER = 'brightness(0) invert(1) drop-shadow(0 0 0 white) dr...
  type CompanyLogo (line 5) | interface CompanyLogo {
  constant LOGOS (line 11) | const LOGOS: CompanyLogo[] = [
  function Companies (line 17) | function Companies() {

FILE: packages/website/components/counter.tsx
  type CounterContextType (line 3) | interface CounterContextType {

FILE: packages/website/components/footer.tsx
  function Footer (line 1) | function Footer() {

FILE: packages/website/components/header.tsx
  function Header (line 7) | function Header() {

FILE: packages/website/components/icons/types.ts
  type IconProps (line 1) | interface IconProps {

FILE: packages/website/components/install-guide.tsx
  constant COPY_FEEDBACK_DURATION_MS (line 16) | const COPY_FEEDBACK_DURATION_MS = 2000;
  type InstallTab (line 24) | interface InstallTab {
  constant INSTALL_TABS (line 32) | const INSTALL_TABS: InstallTab[] = [
  function InstallGuide (line 177) | function InstallGuide() {

FILE: packages/website/components/todo-demo.tsx
  type Todo (line 5) | interface Todo {
  function TodoInput (line 11) | function TodoInput({
  function AddButton (line 37) | function AddButton({ onClick }: { onClick: () => void }) {
  function TodoList (line 48) | function TodoList({ items, onDelete }: {
  function TodoItem (line 65) | function TodoItem({ todo, onDelete }: {
  function TodoDemo (line 87) | function TodoDemo({ closeAction }: { closeAction: () => void }) {

FILE: packages/website/public/auto.global.js
  method inject (line 71) | inject(renderer) {
  method get (line 93) | get() {
  method set (line 96) | set(newHook) {
  method scheduleRefresh (line 154) | scheduleRefresh() {
  function traverseFiber (line 326) | function traverseFiber(fiber, selector, ascending = false) {
  function d (line 553) | function d(n3, l5) {
  function g (line 557) | function g(n3) {
  function _ (line 560) | function _(l5, u5, t4) {
  function m (line 566) | function m(n3, t4, i5, r5, o4) {
  function k (line 570) | function k(n3) {
  function x (line 573) | function x(n3, l5) {
  function S (line 576) | function S(n3, l5) {
  function C (line 581) | function C(n3) {
  function M (line 591) | function M(n3) {
  function $ (line 594) | function $() {
  function I (line 598) | function I(n3, l5, u5, t4, i5, r5, o4, e4, f5, c4, s5) {
  function P (line 603) | function P(n3, l5, u5, t4, i5) {
  function A (line 609) | function A(n3, l5, u5) {
  function H (line 621) | function H(n3, l5) {
  function L (line 626) | function L(n3, l5, u5, t4) {
  function T (line 641) | function T(n3, l5, u5) {
  function j (line 644) | function j(n3, l5, u5, t4, i5) {
  function F (line 662) | function F(n3) {
  function O (line 672) | function O(n3, u5, t4, i5, r5, o4, e4, f5, c4, s5) {
  function z (line 708) | function z(n3, u5, t4) {
  function N (line 720) | function N(n3) {
  function V (line 723) | function V(u5, t4, i5, r5, o4, e4, f5, c4, s5) {
  function q (line 751) | function q(n3, u5, t4) {
  function B (line 761) | function B(n3, u5, t4) {
  function D (line 774) | function D(n3, l5, u5) {
  function E (line 777) | function E(u5, t4, i5) {
  function K (line 781) | function K(n3) {
  function p2 (line 836) | function p2(n3, t4) {
  function d2 (line 841) | function d2(n3) {
  function h2 (line 844) | function h2(n3, u5, i5) {
  function y2 (line 878) | function y2(n3, u5) {
  function _2 (line 882) | function _2(n3, u5) {
  function A2 (line 886) | function A2(n3) {
  function T2 (line 891) | function T2(n3, r5) {
  function q2 (line 895) | function q2(n3, t4) {
  function x2 (line 900) | function x2(n3) {
  function j2 (line 904) | function j2() {
  function w2 (line 951) | function w2(n3) {
  function z2 (line 957) | function z2(n3) {
  function B2 (line 961) | function B2(n3) {
  function C2 (line 965) | function C2(n3, t4) {
  function D2 (line 970) | function D2(n3, t4) {
  function t3 (line 976) | function t3() {
  function n2 (line 1004) | function n2(i5) {
  function e3 (line 1017) | function e3(i5) {
  function u3 (line 1041) | function u3(i5, t4) {
  function d3 (line 1134) | function d3(i5, t4) {
  function c3 (line 1137) | function c3(i5) {
  function a3 (line 1141) | function a3(i5) {
  function l3 (line 1153) | function l3(i5) {
  function y3 (line 1168) | function y3(i5, t4) {
  function w3 (line 1240) | function w3(i5, t4) {
  function _3 (line 1243) | function _3(i5) {
  function b (line 1263) | function b(i5) {
  function g2 (line 1269) | function g2(i5) {
  function p3 (line 1277) | function p3(i5) {
  function E2 (line 1320) | function E2(i5) {
  function l4 (line 1333) | function l4(i5, n3) {
  function d4 (line 1337) | function d4(i5) {
  function h4 (line 1341) | function h4(i5) {
  function p4 (line 1433) | function p4(i5, r5, n3, t4) {
  function useSignal (line 1491) | function useSignal(i5) {
  function useSignalEffect (line 1496) | function useSignalEffect(i5) {
  function descending (line 1507) | function descending(a4, b3) {
  function getComponentGroupNames (line 1510) | function getComponentGroupNames(group) {
  function getComponentGroupTotalTime (line 1519) | function getComponentGroupTotalTime(group) {
  function componentGroupHasForget (line 1526) | function componentGroupHasForget(group) {
  function isEqual (line 1578) | function isEqual(a4, b3) {
  function g4 (line 1626) | function g4(n3, t4) {
  function E3 (line 1630) | function E3(n3, t4) {
  function C3 (line 1635) | function C3(n3, t4) {
  function x3 (line 1645) | function x3(n3) {
  function N2 (line 1654) | function N2(n3, t4) {
  function M2 (line 1657) | function M2(n3, e4) {
  function D3 (line 1675) | function D3(n3) {
  function V2 (line 1690) | function V2(n3, t4, e4) {
  function W (line 1697) | function W(n3, t4, e4) {
  function P3 (line 1702) | function P3() {
  function j3 (line 1705) | function j3(n3) {
  function B3 (line 1709) | function B3() {
  function Z (line 1753) | function Z(n3) {
  function Y (line 1758) | function Y(n3) {
  function $2 (line 1774) | function $2(n3, e4) {
  function rn (line 1814) | function rn() {
  function un (line 1816) | function un() {
  function on (line 1819) | function on() {
  function u4 (line 1859) | function u4(e4, t4, n3, o4, i5, u5) {
  function r4 (line 1918) | function r4(e4) {
  function clsx (line 1927) | function clsx() {
  method get (line 2077) | get(key) {
  method set (line 2087) | set(key, value) {
  function twJoin (line 2219) | function twJoin() {
  function createTailwindMerge (line 2250) | function createTailwindMerge(createConfigFirst, ...createConfigRest) {
  function CONSTANT_UPDATE (line 4560) | function CONSTANT_UPDATE() {
  function constant (line 4563) | function constant(Component) {
  method create (line 4685) | create(container) {
  method cleanup (line 4732) | cleanup(container) {
  method cleanupAll (line 4749) | cleanupAll() {
  method constructor (line 7023) | constructor() {
  method getDerivedStateFromError (line 7034) | static getDerivedStateFromError(e4) {
  method render (line 7037) | render() {
  function hackyJsFormatter (line 7530) | function hackyJsFormatter(code) {
  function getContextChangesTraversal (line 8172) | function getContextChangesTraversal(nextValue, prevValue) {
  function getFiberIdentifier (line 8194) | function getFiberIdentifier(fiber) {
  function getRenderData (line 8197) | function getRenderData(fiber) {
  function setRenderData (line 8205) | function setRenderData(fiber, value) {
  method onCommitFiberRoot (line 8248) | onCommitFiberRoot(_rendererID, root) {
  method onPostCommitFiberRoot (line 8346) | onPostCommitFiberRoot() {
  function sortEntry (line 8448) | function sortEntry(prev, next) {
  function getSortedEntries (line 8451) | function getSortedEntries(countByNames) {
  function getLabelTextPart (line 8455) | function getLabelTextPart([count, names]) {
  function onIntersect (line 8713) | function onIntersect(entries, observer) {
  method onPostCommitFiberRoot (line 9130) | onPostCommitFiberRoot() {
  method constructor (line 9355) | constructor(capacity = 25) {
  method push (line 9359) | push(...items) {
  method fromArray (line 9367) | static fromArray(array, capacity) {
  method constructor (line 9376) | constructor(initialValue) {
  method subscribe (line 9380) | subscribe(subscriber) {
  method setState (line 9387) | setState(data) {
  method getCurrentState (line 9391) | getCurrentState() {
  method constructor (line 9403) | constructor() {
  method publish (line 9406) | publish(item, to, createIfNoChannel = true) {
  method getAvailableChannels (line 9422) | getAvailableChannels() {
  method subscribe (line 9425) | subscribe(to, cb, dropFirst = false) {
  method updateChannelState (line 9454) | updateChannelState(channel, updater, createIfNoChannel = true) {
  method getChannelState (line 9471) | getChannelState(channel) {
  function startLongPipelineTracking (line 10316) | function startLongPipelineTracking() {
  function cleanup2 (line 12223) | function cleanup2() {
  method constructor (line 14895) | constructor(width, height) {
  method rightEdge (line 14901) | rightEdge(width) {
  method bottomEdge (line 14904) | bottomEdge(height) {
  method isFullWidth (line 14907) | isFullWidth(width) {
  method isFullHeight (line 14910) | isFullHeight(height) {
  method constructor (line 16058) | constructor() {
  method getDerivedStateFromError (line 16065) | static getDerivedStateFromError(error) {
  method render (line 16068) | render() {

FILE: playwright.config.ts
  constant PORT (line 3) | const PORT = 5173;

FILE: scripts/version-warning.mjs
  constant MESSAGES (line 22) | const MESSAGES = {
  function getWorkspacePackages (line 32) | function getWorkspacePackages() {
  function getPackageInfo (line 61) | function getPackageInfo() {
Condensed preview — 238 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,688K chars).
[
  {
    "path": ".changeset/README.md",
    "chars": 510,
    "preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
  },
  {
    "path": ".changeset/config.json",
    "chars": 331,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.5/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \""
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "chars": 3333,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": ".github/workflows/build-extension.yml",
    "chars": 1413,
    "preview": "name: Build Extension\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'packages/extension/**'\n\njobs:\n  build:"
  },
  {
    "path": ".github/workflows/pkg-pr-new.yaml",
    "chars": 959,
    "preview": "name: Publish Any Commit\non:\n  push:\n    branches:\n      - \"**\"\n  pull_request:\n    branches:\n      - \"**\"\n\njobs:\n  buil"
  },
  {
    "path": ".gitignore",
    "chars": 198,
    "preview": "node_modules\n.DS_Store\n.env\ndist\n**/*.tgz\n*.log\nbuild\n!packages/extension/build/\nplaygrounds\n# SSL Certificates\nbin/cert"
  },
  {
    "path": ".npmrc",
    "chars": 59,
    "preview": "prefer-workspace-packages=true\nlink-workspace-packages=true"
  },
  {
    "path": ".oxlintrc.json",
    "chars": 735,
    "preview": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\"typescript\", \"react\", \"import\"],\n  \"ign"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 306,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"css.lint.unknownAtRules\": \"ignore\",\n  \"oxc.lint.enable\": true,\n  \"[markdown]\": {\n   "
  },
  {
    "path": "AGENTS.md",
    "chars": 1220,
    "preview": "## General Rules\n\n- MUST: Use TypeScript interfaces over types.\n- MUST: Keep all types in the global scope.\n- MUST: Use "
  },
  {
    "path": "BROWSER_EXTENSION_GUIDE.md",
    "chars": 1603,
    "preview": "# Browser Extension Installation Guide\n\n> [!NOTE]\n> The React Scan browser extension currently uses `react-scan@0.4.3`\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2505,
    "preview": "# Contributing to React Scan\n\nFirst off, thanks for taking the time to contribute! ❤️\n\n## Table of Contents\n\n- [Contribu"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "Copyright 2025 Aiden Bai, Million Software, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 6041,
    "preview": "# <img src=\"https://github.com/aidenybai/react-scan/blob/main/.github/assets/logo.svg\" width=\"30\" height=\"30\" align=\"cen"
  },
  {
    "path": "bin/generate-certs.sh",
    "chars": 156,
    "preview": "#!/bin/bash\n\nmkdir -p bin/certs\nopenssl req -x509 -newkey rsa:2048 -keyout bin/certs/key.pem -out bin/certs/cert.pem -da"
  },
  {
    "path": "bin/serve-scan.sh",
    "chars": 973,
    "preview": "#!/bin/bash\n\n# Determine the directory of the script\nSCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\n\n# Default"
  },
  {
    "path": "docs/installation/astro.md",
    "chars": 964,
    "preview": "# Astro Guide\n\n## As a script tag\n\nAdd the script tag to your root layout.\n\nRefer to the [CDN Guide](https://github.com/"
  },
  {
    "path": "docs/installation/cdn.md",
    "chars": 366,
    "preview": "# CDN\n\nYou can choose one of the following URLs to initialize React Scan via CDN.\n\n## Usage\n\n```html\n<script src=\"https:"
  },
  {
    "path": "docs/installation/create-react-app.md",
    "chars": 1094,
    "preview": "# Create React App (CRA) Guide\n\n## As a script tag\n\nAdd the script tag to your `index.html`.\n\nRefer to the [CDN Guide](h"
  },
  {
    "path": "docs/installation/next-js-app-router.md",
    "chars": 1453,
    "preview": "# NextJS App Router Guide\n\n## As a script tag\n\nAdd the script tag to your `app/layout`.\n\nRefer to the [CDN Guide](https:"
  },
  {
    "path": "docs/installation/next-js-page-router.md",
    "chars": 1264,
    "preview": "# NextJS Page Router Guide\n\n## As a script tag\n\nAdd the script tag to your `pages/_document`\n\nRefer to the [CDN Guide](h"
  },
  {
    "path": "docs/installation/parcel.md",
    "chars": 432,
    "preview": "# Parcel Guide\n\n## As a script tag\n\nAdd the script tag to your `index.html`\n\nRefer to the [CDN Guide](https://github.com"
  },
  {
    "path": "docs/installation/react-router.md",
    "chars": 1969,
    "preview": "# React Router v7 Guide\n\n## As a script tag\n\nAdd the script tag to your `Layout` component in the `app/root`.\n\nRefer to "
  },
  {
    "path": "docs/installation/remix.md",
    "chars": 2636,
    "preview": "# Remix Guide\n\n## As a script tag\n\nAdd the script tag to your `<Layout>` component in `app/root`.\n\nRefer to the [CDN Gui"
  },
  {
    "path": "docs/installation/rsbuild.md",
    "chars": 1513,
    "preview": "# Rsbuild Guide\n\n## As a script tag\n\nIf you are using Rsbuild's default HTML template, add the script tag via [html.tags"
  },
  {
    "path": "docs/installation/tanstack-start.md",
    "chars": 2165,
    "preview": "# TanStack Router Guide\n\n## As a script tag\n\nAdd the script tag to your `<RootDocument>` component at `app/routes/__root"
  },
  {
    "path": "docs/installation/vite.md",
    "chars": 1133,
    "preview": "# Vite Guide\n\n## As a script tag\n\nAdd the script tag to your `index.html`.\n\nRefer to the [CDN Guide](https://github.com/"
  },
  {
    "path": "e2e/helpers.ts",
    "chars": 2502,
    "preview": "import { type Page } from '@playwright/test';\n\nexport const FIXTURE_URL = '/?example=e2e-fixture';\n\nexport async functio"
  },
  {
    "path": "e2e/inspector.spec.ts",
    "chars": 1738,
    "preview": "import { test, expect } from '@playwright/test';\nimport { gotoFixture } from './helpers';\n\ntest.describe('Inspector', ()"
  },
  {
    "path": "e2e/notifications.spec.ts",
    "chars": 1534,
    "preview": "import { test, expect } from '@playwright/test';\nimport { gotoFixture } from './helpers';\n\ntest.describe('Notifications'"
  },
  {
    "path": "e2e/outlines.spec.ts",
    "chars": 2287,
    "preview": "import { test, expect, type Page } from '@playwright/test';\nimport { gotoFixture, getRenderCount } from './helpers';\n\nas"
  },
  {
    "path": "e2e/toolbar.spec.ts",
    "chars": 2282,
    "preview": "import { test, expect } from '@playwright/test';\nimport { gotoFixture, isReactScanActive, hasShadowRoot } from './helper"
  },
  {
    "path": "kitchen-sink/index.html",
    "chars": 2267,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "kitchen-sink/package.json",
    "chars": 572,
    "preview": "{\n  \"name\": \"@react-scan/kitchen-sink\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"publishConfig\": {\n    \"access\": \"restr"
  },
  {
    "path": "kitchen-sink/postcss.config.mjs",
    "chars": 81,
    "preview": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "kitchen-sink/src/examples/e2e-fixture/index.tsx",
    "chars": 3602,
    "preview": "import { useState, useContext, createContext, memo } from 'react';\nimport { scan, Store } from 'react-scan';\n\nStore.isIn"
  },
  {
    "path": "kitchen-sink/src/examples/sierpinski/index.tsx",
    "chars": 2968,
    "preview": "/**\n *  Modified version of https://github.com/ryansolid/solid-sierpinski-triangle-demo\n **/\n// import { Analytics } fro"
  },
  {
    "path": "kitchen-sink/src/examples/sierpinski/styles.css",
    "chars": 1078,
    "preview": "body {\n  background: #fff;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 15px;\n  line-heig"
  },
  {
    "path": "kitchen-sink/src/examples/todo-list/index.tsx",
    "chars": 2901,
    "preview": "import { useState } from 'react';\nimport { scan, Store } from 'react-scan';\nimport './styles.css';\n\n\nStore.isInIframe.va"
  },
  {
    "path": "kitchen-sink/src/examples/todo-list/styles.css",
    "chars": 2280,
    "preview": "* {\n  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Ar"
  },
  {
    "path": "kitchen-sink/src/index.css",
    "chars": 22,
    "preview": "body {\n  margin: 0;\n}\n"
  },
  {
    "path": "kitchen-sink/src/index.tsx",
    "chars": 672,
    "preview": "import 'react-scan';\n\nimport { FC, lazy } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport Home from "
  },
  {
    "path": "kitchen-sink/src/main.css",
    "chars": 89,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  overflow: hidden;\n}\n"
  },
  {
    "path": "kitchen-sink/src/main.tsx",
    "chars": 1265,
    "preview": "import { type JSX, useState } from 'react';\nimport './main.css';\n\ninterface Example {\n  title: string;\n  url: string;\n}\n"
  },
  {
    "path": "kitchen-sink/tailwind.config.mjs",
    "chars": 63,
    "preview": "export default {\n  content: ['./src/**/*.{js,jsx,ts,tsx}'],\n};\n"
  },
  {
    "path": "kitchen-sink/tsconfig.json",
    "chars": 567,
    "preview": "{\n  \"exclude\": [\"node_modules\"],\n  \"include\": [\"src\", \"types\"],\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"lib\""
  },
  {
    "path": "kitchen-sink/vite.config.ts",
    "chars": 213,
    "preview": "import react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  plugins:"
  },
  {
    "path": "package.json",
    "chars": 1144,
    "preview": "{\n  \"name\": \"root\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"node scripts/workspace.mjs build\",\n    \"postbuild\": "
  },
  {
    "path": "packages/extension/.gitignore",
    "chars": 291,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "packages/extension/CHANGELOG.md",
    "chars": 420,
    "preview": "# @react-scan/extension\n\n## 0.5.3\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-scan@0.5.3\n\n## 0.5.2\n\n### P"
  },
  {
    "path": "packages/extension/README.md",
    "chars": 1526,
    "preview": "# React Scanner Extension\n\nBrowser extension for scanning React applications and identifying performance issues.\n\n\n### E"
  },
  {
    "path": "packages/extension/package.json",
    "chars": 1679,
    "preview": "{\n  \"name\": \"@react-scan/extension\",\n  \"version\": \"0.5.3\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"cl"
  },
  {
    "path": "packages/extension/src/assets/css/no-react.css",
    "chars": 1958,
    "preview": "html.freeze > body {\n  pointer-events: none;\n}\n\nhtml.freeze {\n  overflow: auto;\n  overscroll-behavior-x: contain;\n}\n\nhtm"
  },
  {
    "path": "packages/extension/src/background/icon.ts",
    "chars": 1606,
    "preview": "import browser from 'webextension-polyfill';\n\nexport enum IconState {\n  DISABLED = 'disabled',\n  ENABLED = 'enabled',\n}\n"
  },
  {
    "path": "packages/extension/src/background/index.ts",
    "chars": 2709,
    "preview": "import browser from 'webextension-polyfill';\nimport { isInternalUrl } from '~utils/helpers';\nimport { IconState, updateI"
  },
  {
    "path": "packages/extension/src/content/index.ts",
    "chars": 1292,
    "preview": "import browser from 'webextension-polyfill';\nimport {\n  type BroadcastMessage,\n  BroadcastSchema,\n  type IEvents,\n} from"
  },
  {
    "path": "packages/extension/src/inject/index.ts",
    "chars": 4913,
    "preview": "import * as reactScan from 'react-scan';\nimport { gt } from 'semver';\nimport type { IEvents } from '~types/messages';\nim"
  },
  {
    "path": "packages/extension/src/inject/notification.ts",
    "chars": 3929,
    "preview": "import noReactStyles from '~assets/css/no-react.css?inline';\nimport type { IEvents } from '~types/messages';\nimport { bu"
  },
  {
    "path": "packages/extension/src/inject/react-scan.ts",
    "chars": 67,
    "preview": "// Bippy has a side-effect that installs the hook.\nimport 'bippy';\n"
  },
  {
    "path": "packages/extension/src/manifest.chrome.json",
    "chars": 1290,
    "preview": "{\n  \"manifest_version\": 3,\n  \"name\": \"React Scan\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Scan React apps for performan"
  },
  {
    "path": "packages/extension/src/manifest.firefox.json",
    "chars": 1270,
    "preview": "{\n  \"manifest_version\": 2,\n  \"name\": \"React Scan\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Scan React apps for performan"
  },
  {
    "path": "packages/extension/src/types/global.d.ts",
    "chars": 1078,
    "preview": "import type * as reactScan from 'react-scan';\n\ndeclare global {\n  interface Window {\n    __REACT_SCAN__?: {\n      ReactS"
  },
  {
    "path": "packages/extension/src/types/messages.ts",
    "chars": 546,
    "preview": "import { z } from 'zod';\n\nexport const BroadcastSchema = z.object({\n  type: z.enum([\n    'react-scan:ping',\n    'react-s"
  },
  {
    "path": "packages/extension/src/utils/constants.ts",
    "chars": 110,
    "preview": "export const STORAGE_KEY = 'react-scan-options';\nexport const EXTENSION_STORAGE_KEY = 'react-scan-extension';\n"
  },
  {
    "path": "packages/extension/src/utils/helpers.ts",
    "chars": 6285,
    "preview": "export const isIframe = window !== window.top;\nexport const isPopup = window.opener !== null;\nexport const canLoadReactS"
  },
  {
    "path": "packages/extension/src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "packages/extension/tsconfig.json",
    "chars": 876,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true"
  },
  {
    "path": "packages/extension/tsconfig.node.json",
    "chars": 184,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSynthe"
  },
  {
    "path": "packages/extension/vite.config.ts",
    "chars": 2919,
    "preview": "import react from '@vitejs/plugin-react';\nimport { type UserConfig, defineConfig, loadEnv } from 'vite';\nimport webExten"
  },
  {
    "path": "packages/scan/.gitignore",
    "chars": 30,
    "preview": "src/web/assets/css/styles.css\n"
  },
  {
    "path": "packages/scan/CHANGELOG.md",
    "chars": 890,
    "preview": "# react-scan\n\n## 0.5.3\n\n### Patch Changes\n\n- fix\n\n## 0.5.2\n\n### Patch Changes\n\n- fix\n\n## 0.5.1\n\n### Patch Changes\n\n- fix"
  },
  {
    "path": "packages/scan/README.md",
    "chars": 9840,
    "preview": "# <img src=\"https://github.com/aidenybai/react-scan/blob/main/.github/assets/logo.svg\" width=\"30\" height=\"30\" align=\"cen"
  },
  {
    "path": "packages/scan/auto.d.ts",
    "chars": 29,
    "preview": "export * from './dist/auto';\n"
  },
  {
    "path": "packages/scan/bin/cli.js",
    "chars": 45,
    "preview": "#! /usr/bin/env node\nrequire('../dist/cli');\n"
  },
  {
    "path": "packages/scan/global.d.ts",
    "chars": 164,
    "preview": "declare module '*.css' {\n  const content: string;\n  export default content;\n}\n\ndeclare module '*.astro' {\n  const Compon"
  },
  {
    "path": "packages/scan/package.json",
    "chars": 7863,
    "preview": "{\n  \"name\": \"react-scan\",\n  \"version\": \"0.5.3\",\n  \"description\": \"Scan your React app for renders\",\n  \"keywords\": [\n    "
  },
  {
    "path": "packages/scan/postcss.config.mjs",
    "chars": 213,
    "preview": "import autoprefixer from 'autoprefixer';\nimport tailwindcss from 'tailwindcss';\nimport remToPx from './postcss.rem2px.mj"
  },
  {
    "path": "packages/scan/postcss.rem2px.mjs",
    "chars": 1208,
    "preview": "const remToPx = (options = {}) => {\n  const baseValue = options.baseValue || 16;\n\n  // Improved regex that handles all r"
  },
  {
    "path": "packages/scan/scripts/bump-version.js",
    "chars": 1031,
    "preview": "import { execSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n// Read the curr"
  },
  {
    "path": "packages/scan/src/auto.ts",
    "chars": 235,
    "preview": "import './polyfills';\n// Prioritize bippy side-effect\nimport 'bippy';\n\nimport { IS_CLIENT } from '~web/utils/constants';"
  },
  {
    "path": "packages/scan/src/cli-utils.mts",
    "chars": 13843,
    "preview": "import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\ntype PackageManager = 'npm' | 'ya"
  },
  {
    "path": "packages/scan/src/cli-utils.test.mts",
    "chars": 22989,
    "preview": "import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join"
  },
  {
    "path": "packages/scan/src/cli.mts",
    "chars": 5943,
    "preview": "import { execSync } from 'node:child_process';\nimport { existsSync, writeFileSync } from 'node:fs';\nimport { join, relat"
  },
  {
    "path": "packages/scan/src/core/all-environments.ts",
    "chars": 265,
    "preview": "import { ReactScanInternals, scan as innerScan } from '.';\n\nexport const scan = /*#__PURE__*/ (...params: Parameters<typ"
  },
  {
    "path": "packages/scan/src/core/fast-serialize.test.ts",
    "chars": 1774,
    "preview": "import { describe, expect, it } from 'vitest';\nimport { fastSerialize } from '~core/instrumentation';\n\ndescribe('fastSer"
  },
  {
    "path": "packages/scan/src/core/index.ts",
    "chars": 14994,
    "preview": "import { type Signal, signal } from '@preact/signals';\nimport {\n  type Fiber,\n  detectReactBuildType,\n  getRDTHook,\n  ge"
  },
  {
    "path": "packages/scan/src/core/instrumentation.ts",
    "chars": 18283,
    "preview": "import { type Signal, signal } from '@preact/signals';\nimport {\n  ClassComponentTag,\n  type Fiber,\n  type FiberRoot,\n  F"
  },
  {
    "path": "packages/scan/src/core/notifications/event-tracking.ts",
    "chars": 13154,
    "preview": "import { useSyncExternalStore } from 'preact/compat';\nimport { not_globally_unique_generateId } from '~core/utils';\nimpo"
  },
  {
    "path": "packages/scan/src/core/notifications/interaction-store.ts",
    "chars": 899,
    "preview": "import { BoundedArray } from \"~core/notifications/performance-utils\";\nimport { CompletedInteraction } from \"./performanc"
  },
  {
    "path": "packages/scan/src/core/notifications/outline-overlay.ts",
    "chars": 5971,
    "preview": "import { signal } from '@preact/signals';\nimport { iife } from './performance-utils';\n\nexport let highlightCanvas: HTMLC"
  },
  {
    "path": "packages/scan/src/core/notifications/performance-store.ts",
    "chars": 3739,
    "preview": "import { BoundedArray } from \"./performance-utils\";\nimport { PerformanceEntryChannelEvent } from \"./performance\";\n\ntype "
  },
  {
    "path": "packages/scan/src/core/notifications/performance-utils.ts",
    "chars": 2096,
    "preview": "import { Fiber } from 'bippy';\nexport const getChildrenFromFiberLL = (fiber: Fiber) => {\n  const children: Array<Fiber> "
  },
  {
    "path": "packages/scan/src/core/notifications/performance.ts",
    "chars": 31318,
    "preview": "import {\n  Fiber,\n  getDisplayName,\n  getTimings,\n  hasMemoCache,\n  isHostFiber,\n  traverseFiber,\n} from 'bippy';\nimport"
  },
  {
    "path": "packages/scan/src/core/notifications/types.ts",
    "chars": 991,
    "preview": "export interface PerformanceInteractionEntry extends PerformanceEntry {\n  interactionId: string;\n  target: Element;\n  na"
  },
  {
    "path": "packages/scan/src/core/utils.ts",
    "chars": 5858,
    "preview": "// @ts-nocheck\nimport { type Fiber, getType } from 'bippy';\nimport { ReactScanInternals } from '~core/index';\nimport typ"
  },
  {
    "path": "packages/scan/src/index.ts",
    "chars": 120,
    "preview": "import './polyfills';\n// Bippy has a side-effect that installs the hook.\nimport 'bippy';\n\nexport * from './core/index';\n"
  },
  {
    "path": "packages/scan/src/install-hook.ts",
    "chars": 44,
    "preview": "export { getRDTHook as init } from 'bippy';\n"
  },
  {
    "path": "packages/scan/src/monitoring/next.ts",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "packages/scan/src/new-outlines/canvas.ts",
    "chars": 8066,
    "preview": "import type { ActiveOutline, OutlineData } from './types';\n\nexport const OUTLINE_ARRAY_SIZE = 7;\nconst MONO_FONT =\n  'Me"
  },
  {
    "path": "packages/scan/src/new-outlines/index.ts",
    "chars": 17386,
    "preview": "import {\n  type Fiber,\n  didFiberCommit,\n  getDisplayName,\n  getFiberId,\n  getNearestHostFibers,\n  getTimings,\n  getType"
  },
  {
    "path": "packages/scan/src/new-outlines/offscreen-canvas.worker.ts",
    "chars": 2750,
    "preview": "import { OUTLINE_ARRAY_SIZE, drawCanvas, initCanvas } from './canvas';\nimport type { ActiveOutline } from './types';\n\nle"
  },
  {
    "path": "packages/scan/src/new-outlines/types.ts",
    "chars": 903,
    "preview": "export interface OutlineData {\n  id: number;\n  name: string;\n  count: number;\n  x: number;\n  y: number;\n  width: number;"
  },
  {
    "path": "packages/scan/src/polyfills.ts",
    "chars": 274,
    "preview": "if (!Array.prototype.toSorted) {\n  Object.defineProperty(Array.prototype, 'toSorted', {\n    value: function <T>(this: Ar"
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/arrow-function.test.ts",
    "chars": 1428,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('arrow function components"
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/complex-patterns.test.ts",
    "chars": 2038,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('complex component pattern"
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/function-declarations.test.ts",
    "chars": 745,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('function declarations', ("
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/general-cases.test.ts",
    "chars": 27451,
    "preview": "import { describe, expect, it } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('edge cases', () => {\n  it"
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/react-patterns.test.ts",
    "chars": 2321,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('modern React patterns', ("
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/ts-patterns.test.ts",
    "chars": 1126,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { transform } from './utils';\n\ndescribe('typescript patterns', () "
  },
  {
    "path": "packages/scan/src/react-component-name/__tests__/utils.ts",
    "chars": 662,
    "preview": "import { type Options, reactComponentNamePlugin } from '..';\n\ntype TransformFn = (\n  code: string,\n  id: string,\n) => Pr"
  },
  {
    "path": "packages/scan/src/react-component-name/astro.ts",
    "chars": 362,
    "preview": "import type { Options } from '.';\nimport vite from './vite';\n\nexport default (options: Options = {}) => ({\n  name: 'reac"
  },
  {
    "path": "packages/scan/src/react-component-name/babel/get-descriptive-name.ts",
    "chars": 1013,
    "preview": "import type * as babel from '@babel/core';\n\nexport function getDescriptiveName(\n  path: babel.NodePath,\n  defaultName: s"
  },
  {
    "path": "packages/scan/src/react-component-name/babel/get-root-statement-path.ts",
    "chars": 357,
    "preview": "import type * as babel from '@babel/core';\nimport * as t from '@babel/types';\n\nexport function getRootStatementPath(path"
  },
  {
    "path": "packages/scan/src/react-component-name/babel/index.ts",
    "chars": 8316,
    "preview": "import type { NodePath, PluginObj } from '@babel/core';\nimport * as t from '@babel/types';\nimport type { Options } from "
  },
  {
    "path": "packages/scan/src/react-component-name/babel/is-componentish-name.ts",
    "chars": 406,
    "preview": "// This is just a Pascal heuristic\n// we only assume a function is a component\n\nimport type { Options } from '../core/op"
  },
  {
    "path": "packages/scan/src/react-component-name/babel/is-nested-expression.ts",
    "chars": 638,
    "preview": "import type * as t from '@babel/types';\ntype NestedExpression =\n  | t.ParenthesizedExpression\n  | t.TypeCastExpression\n "
  },
  {
    "path": "packages/scan/src/react-component-name/babel/is-path-valid.ts",
    "chars": 308,
    "preview": "import type { NodePath } from '@babel/core';\nimport type * as t from '@babel/types';\n\ntype TypeFilter<V extends t.Node> "
  },
  {
    "path": "packages/scan/src/react-component-name/babel/is-statement-top-level.ts",
    "chars": 467,
    "preview": "import type * as babel from '@babel/core';\nimport type * as t from '@babel/types';\n\nexport function isStatementTopLevel("
  },
  {
    "path": "packages/scan/src/react-component-name/babel/path-references-import.ts",
    "chars": 2249,
    "preview": "import type { NodePath } from '@babel/core';\nimport * as t from '@babel/types';\nimport { isPathValid } from './is-path-v"
  },
  {
    "path": "packages/scan/src/react-component-name/babel/unwrap.ts",
    "chars": 1087,
    "preview": "import type { NodePath } from '@babel/core';\nimport type * as t from '@babel/types';\nimport { isNestedExpression } from "
  },
  {
    "path": "packages/scan/src/react-component-name/core/options.ts",
    "chars": 789,
    "preview": "import type { FilterPattern } from '@rollup/pluginutils';\n\nexport interface Options {\n  include?: FilterPattern;\n  exclu"
  },
  {
    "path": "packages/scan/src/react-component-name/esbuild.ts",
    "chars": 94,
    "preview": "import { reactComponentNamePlugin } from '.'\n\nexport default reactComponentNamePlugin.esbuild\n"
  },
  {
    "path": "packages/scan/src/react-component-name/index.ts",
    "chars": 2032,
    "preview": "import { transformAsync } from '@babel/core';\nimport { createFilter } from '@rollup/pluginutils';\nimport { createUnplugi"
  },
  {
    "path": "packages/scan/src/react-component-name/loader.ts",
    "chars": 1337,
    "preview": "import { type FilterPattern, createFilter } from '@rollup/pluginutils';\nimport { DEFAULT_EXCLUDE, DEFAULT_INCLUDE, trans"
  },
  {
    "path": "packages/scan/src/react-component-name/rolldown.ts",
    "chars": 98,
    "preview": "import { reactComponentNamePlugin } from \".\";\n\n\nexport default reactComponentNamePlugin.rolldown;\n"
  },
  {
    "path": "packages/scan/src/react-component-name/rollup.ts",
    "chars": 91,
    "preview": "import reactComponentNamePlugin from '.';\n\nexport default reactComponentNamePlugin.rollup;\n"
  },
  {
    "path": "packages/scan/src/react-component-name/rspack.ts",
    "chars": 91,
    "preview": "import reactComponentNamePlugin from '.';\n\nexport default reactComponentNamePlugin.rspack;\n"
  },
  {
    "path": "packages/scan/src/react-component-name/tsconfig.json",
    "chars": 353,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ESNext\"],\n    \"moduleResolution\": "
  },
  {
    "path": "packages/scan/src/react-component-name/vite.ts",
    "chars": 93,
    "preview": "import { reactComponentNamePlugin } from '.';\n\nexport default reactComponentNamePlugin.vite;\n"
  },
  {
    "path": "packages/scan/src/react-component-name/webpack.ts",
    "chars": 96,
    "preview": "import { reactComponentNamePlugin } from '.';\n\nexport default reactComponentNamePlugin.webpack;\n"
  },
  {
    "path": "packages/scan/src/types.d.ts",
    "chars": 117,
    "preview": "declare module './new-outlines/offscreen-canvas.worker' {\n  const workerCode: string;\n  export default workerCode;\n} "
  },
  {
    "path": "packages/scan/src/types.ts",
    "chars": 1800,
    "preview": "import type { Fiber, FiberRoot } from 'bippy';\n\ntype ReactScanInternals = typeof import('./core/index')['ReactScanIntern"
  },
  {
    "path": "packages/scan/src/web/assets/css/styles.tailwind.css",
    "chars": 12399,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n* {\n  outline: none !important;\n  text-rendering: optimizeLe"
  },
  {
    "path": "packages/scan/src/web/components/copy-to-clipboard/index.tsx",
    "chars": 1975,
    "preview": "import { memo } from 'preact/compat';\nimport { useCallback, useEffect, useState } from 'preact/hooks';\nimport { cn } fro"
  },
  {
    "path": "packages/scan/src/web/components/icon/index.tsx",
    "chars": 1076,
    "preview": "import type { JSX } from 'preact';\nimport { type ForwardedRef, forwardRef } from 'preact/compat';\n\nexport interface SVGI"
  },
  {
    "path": "packages/scan/src/web/components/slider/index.tsx",
    "chars": 2211,
    "preview": "import { useCallback, useEffect, useRef } from \"preact/hooks\";\nimport { cn } from \"~web/utils/helpers\";\n\ninterface Slide"
  },
  {
    "path": "packages/scan/src/web/components/sticky-section/index.tsx",
    "chars": 3097,
    "preview": "import { memo } from 'preact/compat';\nimport { useCallback, useRef, useState } from 'preact/hooks';\nimport type { useMer"
  },
  {
    "path": "packages/scan/src/web/components/svg-sprite/index.tsx",
    "chars": 6448,
    "preview": "export const SvgSprite = () => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display: none;\">\n      <t"
  },
  {
    "path": "packages/scan/src/web/components/toggle/index.tsx",
    "chars": 462,
    "preview": "import type { JSX } from 'preact';\nimport { cn } from '~web/utils/helpers';\n\ninterface ToggleProps extends JSX.HTMLAttri"
  },
  {
    "path": "packages/scan/src/web/constants.ts",
    "chars": 377,
    "preview": "export const SAFE_AREA = 24;\nexport const MIN_SIZE = {\n  width: 550,\n  height: 350,\n  initialHeight: 400,\n} as const;\n\ne"
  },
  {
    "path": "packages/scan/src/web/hooks/use-delayed-value.ts",
    "chars": 1636,
    "preview": "import { useEffect, useState } from 'preact/hooks';\n\n/**\n * Delays a boolean value change by a specified duration.\n * Pe"
  },
  {
    "path": "packages/scan/src/web/hooks/use-merged-refs.ts",
    "chars": 673,
    "preview": "import type { Ref, RefCallback } from 'preact';\nimport { type MutableRefObject, useCallback } from 'preact/compat';\n\ntyp"
  },
  {
    "path": "packages/scan/src/web/hooks/use-virtual-list.ts",
    "chars": 3238,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'preact/hooks';\n\nexport interface VirtualIt"
  },
  {
    "path": "packages/scan/src/web/state.ts",
    "chars": 3056,
    "preview": "import { signal } from \"@preact/signals\";\nimport {\n  LOCALSTORAGE_KEY,\n  MIN_CONTAINER_WIDTH,\n  MIN_SIZE,\n  SAFE_AREA,\n "
  },
  {
    "path": "packages/scan/src/web/toolbar.tsx",
    "chars": 2417,
    "preview": "import { Component, render } from 'preact';\nimport { Icon } from './components/icon';\nimport { Widget } from './widget';"
  },
  {
    "path": "packages/scan/src/web/utils/constants.ts",
    "chars": 56,
    "preview": "export const IS_CLIENT = typeof window !== 'undefined';\n"
  },
  {
    "path": "packages/scan/src/web/utils/create-store.ts",
    "chars": 4863,
    "preview": "/**\n * Adapted from zustand v5.0.3\n *\n * https://github.com/pmndrs/zustand\n *\n * Do not modify unless you know what you "
  },
  {
    "path": "packages/scan/src/web/utils/geiger.ts",
    "chars": 1210,
    "preview": "// MIT License\n// Copyright (c) 2025 Kristian Dupont\n\n// Permission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "packages/scan/src/web/utils/helpers.ts",
    "chars": 4066,
    "preview": "import {\n  type Fiber,\n  MemoComponentTag,\n  SimpleMemoComponentTag,\n  SuspenseComponentTag,\n  getDisplayName,\n  hasMemo"
  },
  {
    "path": "packages/scan/src/web/utils/log.ts",
    "chars": 2891,
    "preview": "// @ts-nocheck\nimport { ChangeReason, type Render } from '~core/instrumentation';\nimport { getLabelText } from '~core/ut"
  },
  {
    "path": "packages/scan/src/web/utils/pin.ts",
    "chars": 676,
    "preview": "import type { Fiber } from 'bippy';\n\nexport const getFiberPath = (fiber: Fiber): string => {\n  const pathSegments: strin"
  },
  {
    "path": "packages/scan/src/web/utils/preact/constant.ts",
    "chars": 559,
    "preview": "import {\n  type Attributes,\n  type Component,\n  type FunctionComponent,\n  createElement,\n} from 'preact';\n\nfunction CONS"
  },
  {
    "path": "packages/scan/src/web/views/index.tsx",
    "chars": 2549,
    "preview": "import { type ReadonlySignal, computed } from '@preact/signals';\nimport type { ReactNode } from 'preact/compat';\nimport "
  },
  {
    "path": "packages/scan/src/web/views/inspector/components-tree/index.tsx",
    "chars": 35172,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'preact/hooks';\nimport { Store } from '~cor"
  },
  {
    "path": "packages/scan/src/web/views/inspector/components-tree/state.ts",
    "chars": 770,
    "preview": "import { signal } from '@preact/signals';\nimport type { Fiber } from 'bippy';\nimport type { RenderData } from '~core/ins"
  },
  {
    "path": "packages/scan/src/web/views/inspector/diff-value.tsx",
    "chars": 5457,
    "preview": "import { useState } from 'preact/hooks';\nimport { CopyToClipboard } from '~web/components/copy-to-clipboard';\nimport { I"
  },
  {
    "path": "packages/scan/src/web/views/inspector/flash-overlay.ts",
    "chars": 3054,
    "preview": "interface FlashEntry {\n  element: HTMLElement;\n  overlay: HTMLElement;\n  scrollCleanup?: () => void;\n}\n\nconst fadeOutTim"
  },
  {
    "path": "packages/scan/src/web/views/inspector/header.tsx",
    "chars": 4138,
    "preview": "import { computed, untracked, useSignalEffect } from '@preact/signals';\nimport type { Fiber } from 'bippy';\nimport { use"
  },
  {
    "path": "packages/scan/src/web/views/inspector/index.tsx",
    "chars": 6156,
    "preview": "import { computed, untracked, useSignalEffect } from '@preact/signals';\nimport type { Fiber } from 'bippy';\nimport { Com"
  },
  {
    "path": "packages/scan/src/web/views/inspector/logging.ts",
    "chars": 839,
    "preview": "/**\n * Safely stringifies any value, handling circular references and special types\n */\nexport function safeStringify(va"
  },
  {
    "path": "packages/scan/src/web/views/inspector/overlay/index.tsx",
    "chars": 20616,
    "preview": "import { type Fiber, getDisplayName } from 'bippy';\nimport { useEffect, useRef } from 'preact/hooks';\nimport { ReactScan"
  },
  {
    "path": "packages/scan/src/web/views/inspector/properties.tsx",
    "chars": 20372,
    "preview": "import { getDisplayName } from 'bippy';\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'pr"
  },
  {
    "path": "packages/scan/src/web/views/inspector/states.ts",
    "chars": 4517,
    "preview": "import { signal } from '@preact/signals';\nimport type { Fiber } from 'bippy';\nimport type { ComponentType } from 'preact"
  },
  {
    "path": "packages/scan/src/web/views/inspector/timeline/index.tsx",
    "chars": 3096,
    "preview": "import { isInstrumentationActive } from 'bippy';\nimport { memo } from 'preact/compat';\nimport { useCallback, useEffect, "
  },
  {
    "path": "packages/scan/src/web/views/inspector/timeline/utils.ts",
    "chars": 14198,
    "preview": "import {\n  ClassComponentTag,\n  type ContextDependency,\n  type Fiber,\n  ForwardRefTag,\n  FunctionComponentTag,\n  MemoCom"
  },
  {
    "path": "packages/scan/src/web/views/inspector/utils.ts",
    "chars": 52003,
    "preview": "import {\n  type Fiber,\n  FunctionComponentTag,\n  type MemoizedState,\n  getDisplayName,\n  getTimings,\n  isCompositeFiber,"
  },
  {
    "path": "packages/scan/src/web/views/inspector/what-changed.tsx",
    "chars": 23681,
    "preview": "import { type ReactNode, memo } from 'preact/compat';\nimport {\n  type Dispatch,\n  type StateUpdater,\n  useEffect,\n  useR"
  },
  {
    "path": "packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts",
    "chars": 13143,
    "preview": "import { useEffect, useRef, useState } from 'preact/hooks';\nimport {\n  ChangesListener,\n  ChangesPayload,\n  ContextChang"
  },
  {
    "path": "packages/scan/src/web/views/notifications/collapsed-event.tsx",
    "chars": 5352,
    "preview": "import { useEffect, useRef, useState } from 'preact/hooks';\nimport {\n  DroppedFramesEvent,\n  getComponentName,\n  getEven"
  },
  {
    "path": "packages/scan/src/web/views/notifications/data.ts",
    "chars": 6578,
    "preview": "import { createContext } from 'preact';\nimport { SetStateAction } from 'preact/compat';\nimport { Dispatch, useContext } "
  },
  {
    "path": "packages/scan/src/web/views/notifications/details-routes.tsx",
    "chars": 6759,
    "preview": "import { ReactNode, useEffect, useRef, useState } from 'preact/compat';\nimport { playNotificationSound } from '~core/uti"
  },
  {
    "path": "packages/scan/src/web/views/notifications/icons.tsx",
    "chars": 7485,
    "preview": "import { ReactScanInternals } from '~core/index';\nimport { cn } from '~web/utils/helpers';\n\nexport const ChevronRight = "
  },
  {
    "path": "packages/scan/src/web/views/notifications/notification-header.tsx",
    "chars": 4036,
    "preview": "import { cn } from '~web/utils/helpers';\nimport {\n  NotificationEvent,\n  getComponentName,\n  getEventSeverity,\n  getTota"
  },
  {
    "path": "packages/scan/src/web/views/notifications/notification-tabs.tsx",
    "chars": 4337,
    "preview": "import { cn } from '~web/utils/helpers';\nimport { NotificationEvent, useNotificationsContext } from './data';\nimport { P"
  },
  {
    "path": "packages/scan/src/web/views/notifications/notifications.tsx",
    "chars": 15948,
    "preview": "import { forwardRef } from 'preact/compat';\nimport { useEffect, useRef, useState } from 'preact/hooks';\nimport { not_glo"
  },
  {
    "path": "packages/scan/src/web/views/notifications/optimize.tsx",
    "chars": 28465,
    "preview": "import { useState } from 'preact/hooks';\nimport { cn } from '~web/utils/helpers';\nimport {\n  GroupedFiberRender,\n  Notif"
  },
  {
    "path": "packages/scan/src/web/views/notifications/other-visualization.tsx",
    "chars": 25554,
    "preview": "import { ReactNode } from 'preact/compat';\nimport { useContext, useEffect, useState } from 'preact/hooks';\nimport { getI"
  },
  {
    "path": "packages/scan/src/web/views/notifications/popover.tsx",
    "chars": 5351,
    "preview": "import {\n  ComponentProps,\n  ReactNode,\n  createPortal,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'preact/"
  },
  {
    "path": "packages/scan/src/web/views/notifications/render-bar-chart.tsx",
    "chars": 16781,
    "preview": "import { useRef, useState } from 'preact/hooks';\nimport { getBatchedRectMap } from 'src/new-outlines';\nimport { getIsPro"
  },
  {
    "path": "packages/scan/src/web/views/notifications/render-explanation.tsx",
    "chars": 8839,
    "preview": "import { cn } from '~web/utils/helpers';\nimport { NotificationEvent, useNotificationsContext } from './data';\nimport { u"
  },
  {
    "path": "packages/scan/src/web/views/notifications/slowdown-history.tsx",
    "chars": 13357,
    "preview": "import { useEffect, useRef, useState } from 'preact/compat';\nimport { cn } from '~web/utils/helpers';\nimport {\n  Interac"
  },
  {
    "path": "packages/scan/src/web/views/settings/header.tsx",
    "chars": 460,
    "preview": "import { signalIsSettingsOpen } from '~web/state';\nimport { cn } from '~web/utils/helpers';\n\nexport const HeaderSettings"
  },
  {
    "path": "packages/scan/src/web/views/toolbar/index.tsx",
    "chars": 6566,
    "preview": "import { useSignalEffect } from '@preact/signals';\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useState,\n}"
  },
  {
    "path": "packages/scan/src/web/widget/fps-meter.tsx",
    "chars": 1473,
    "preview": "import { useEffect, useState } from 'preact/hooks';\nimport { getFPS } from '~core/instrumentation';\nimport { cn } from '"
  },
  {
    "path": "packages/scan/src/web/widget/header.tsx",
    "chars": 1259,
    "preview": "import { Store } from '~core/index';\nimport { Icon } from '~web/components/icon';\nimport { useDelayedValue } from '~web/"
  },
  {
    "path": "packages/scan/src/web/widget/helpers.ts",
    "chars": 11037,
    "preview": "import { MIN_SIZE, SAFE_AREA } from '../constants';\nimport type { Corner, Position, ResizeHandleProps, Size } from './ty"
  },
  {
    "path": "packages/scan/src/web/widget/index.tsx",
    "chars": 24861,
    "preview": "import { createContext, type JSX } from \"preact\";\nimport { useCallback, useEffect, useRef, useState } from \"preact/hooks"
  },
  {
    "path": "packages/scan/src/web/widget/resize-handle.tsx",
    "chars": 11884,
    "preview": "import type { JSX } from 'preact';\nimport { useCallback, useEffect, useRef } from 'preact/hooks';\nimport { Store } from "
  },
  {
    "path": "packages/scan/src/web/widget/types.ts",
    "chars": 924,
    "preview": "export interface Position {\n  x: number;\n  y: number;\n}\n\nexport interface Size {\n  width: number;\n  height: number;\n}\n\ne"
  },
  {
    "path": "packages/scan/src/worker-shim.ts",
    "chars": 227,
    "preview": "// This gets injected into the bundle\nexport function createInlineWorker(code: string) {\n  const blob = new Blob([code],"
  },
  {
    "path": "packages/scan/tailwind.config.mjs",
    "chars": 4252,
    "preview": "export default {\n  content: ['./src/**/*.{js,jsx,ts,tsx}'],\n  corePlugins: {\n    preflight: true,\n  },\n  darkMode: 'clas"
  },
  {
    "path": "packages/scan/tsconfig.json",
    "chars": 508,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"outDir\": \"dist\",\n    \"baseUrl\": "
  },
  {
    "path": "packages/scan/tsup.config.ts",
    "chars": 6905,
    "preview": "import * as fs from 'node:fs';\nimport fsPromise from 'node:fs/promises';\nimport path from 'node:path';\nimport { Tsconfig"
  },
  {
    "path": "packages/scan/vite.config.mts",
    "chars": 160,
    "preview": "import tsconfigPaths from 'vite-tsconfig-paths';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineCon"
  },
  {
    "path": "packages/scan/worker-plugin.ts",
    "chars": 945,
    "preview": "import * as esbuild from 'esbuild';\n\n/**\n * A hacky plugin to build the worker file (resolving all imports), and inline\n"
  },
  {
    "path": "packages/vite-plugin-react-scan/.npmignore",
    "chars": 114,
    "preview": "# Source\nsrc/\n\n# Config files\ntsconfig.json\n.biome*\nbiome.json\n\n# Development files\nnode_modules/\n*.log\n.DS_Store\n"
  },
  {
    "path": "packages/vite-plugin-react-scan/CHANGELOG.md",
    "chars": 433,
    "preview": "# @react-scan/vite-plugin-react-scan\n\n## 0.2.3\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n  - react-scan@0.5.3\n\n##"
  },
  {
    "path": "packages/vite-plugin-react-scan/LICENSE",
    "chars": 1073,
    "preview": "Copyright 2025 Aiden Bai, Million Software, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "packages/vite-plugin-react-scan/README.md",
    "chars": 2212,
    "preview": "# @react-scan/vite-plugin-react-scan\n\nA Vite plugin that integrates React Scan into your Vite application, automatically"
  },
  {
    "path": "packages/vite-plugin-react-scan/package.json",
    "chars": 1751,
    "preview": "{\n  \"name\": \"@react-scan/vite-plugin-react-scan\",\n  \"version\": \"0.2.3\",\n  \"description\": \"A Vite plugin for React Scan -"
  }
]

// ... and 38 more files (download for full content)

About this extraction

This page contains the full source code of the aidenybai/react-scan GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 238 files (1.5 MB), approximately 410.6k tokens, and a symbol index with 524 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!