Full Code of zcf0508/vue-scan for AI

master 604e143ddb7b cached
97 files
128.6 KB
37.0k tokens
115 symbols
1 requests
Download .txt
Repository: zcf0508/vue-scan
Branch: master
Commit: 604e143ddb7b
Files: 97
Total size: 128.6 KB

Directory structure:
gitextract_wgm2a3xm/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── README.md
├── build.config.ts
├── eslint.config.js
├── examples/
│   ├── vue2/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── env.d.ts
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── assets/
│   │   │   │   ├── base.css
│   │   │   │   └── main.css
│   │   │   ├── components/
│   │   │   │   └── HelloWorld.vue
│   │   │   └── main.ts
│   │   ├── tsconfig.config.json
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   └── vue3/
│       ├── .gitignore
│       ├── .vscode/
│       │   └── extensions.json
│       ├── README.md
│       ├── env.d.ts
│       ├── index.html
│       ├── package.json
│       ├── src/
│       │   ├── App.vue
│       │   ├── assets/
│       │   │   ├── base.css
│       │   │   └── main.css
│       │   ├── components/
│       │   │   └── HelloWorld.vue
│       │   └── main.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── package.json
├── packages/
│   ├── devpilot-plugin-vue-scan/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── client/
│   │   │   │   ├── control-panel.ts
│   │   │   │   ├── fps.ts
│   │   │   │   ├── helpers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── runtime-control.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── vue-injector.ts
│   │   │   ├── data-store.ts
│   │   │   ├── index.ts
│   │   │   ├── skill.md
│   │   │   └── types.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── entrypoints/
│   │   │   ├── background.ts
│   │   │   ├── content.ts
│   │   │   └── popup/
│   │   │       ├── App.vue
│   │   │       ├── index.html
│   │   │       └── main.ts
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   ├── uno.config.ts
│   │   ├── utils/
│   │   │   └── storage.ts
│   │   └── wxt.config.ts
│   └── nuxt/
│       ├── .gitignore
│       ├── Readme.md
│       ├── package.json
│       ├── playground/
│       │   ├── app.vue
│       │   ├── components/
│       │   │   └── HelloWorld.vue
│       │   ├── nuxt.config.ts
│       │   ├── package.json
│       │   ├── server/
│       │   │   └── tsconfig.json
│       │   └── tsconfig.json
│       ├── src/
│       │   ├── module.ts
│       │   └── runtime/
│       │       ├── plugin.ts
│       │       └── server/
│       │           └── tsconfig.json
│       ├── test/
│       │   ├── basic.test.ts
│       │   └── fixtures/
│       │       └── basic/
│       │           ├── app.vue
│       │           ├── nuxt.config.ts
│       │           └── package.json
│       └── tsconfig.json
├── patches/
│   └── @vue__devtools-kit.patch
├── pnpm-workspace.yaml
├── src/
│   ├── auto.ts
│   ├── core/
│   │   ├── fps.ts
│   │   ├── highlight.ts
│   │   ├── hook.ts
│   │   ├── index.ts
│   │   └── utils.ts
│   ├── global.d.ts
│   ├── index.ts
│   ├── index_vue2.ts
│   ├── types.ts
│   ├── utils/
│   │   └── MutationObserverDom.ts
│   └── utils.ts
├── tsconfig.json
└── uno.config.ts

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

================================================
FILE: .github/FUNDING.yml
================================================
github: [antfu]
ko_fi: huali58081
custom: ['https://afdian.com/a/huali08']


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches:
      - master

  pull_request:
    branches:
      - master

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set node
        uses: actions/setup-node@v3
        with:
          node-version: 20.x

      - name: Setup
        run: npm i -g pnpm@9

      - name: Install
        run: pnpm i

      - name: Lint
        run: pnpm run lint

      - name: Typecheck devpilot plugin
        run: pnpm -C packages/devpilot-plugin-vue-scan run typecheck

      # - name: Test
      #   run: pnpm run test


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v5
        with:
          node-version: lts/*
          registry-url: 'https://registry.npmjs.org'

      - run: npm i -g npm@latest

      - run: npx changelogithub
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

      - name: Setup
        run: npm i -g pnpm@9

      - name: Install Dependencies
        run: pnpm i

      - name: PNPM build
        run: pnpm run build

      - name: Publish vue scan to NPM
        run: pnpm publish --access public --no-git-checks

      # publish devpilot plugin

      - name: Build devpilot plugin
        working-directory: packages/devpilot-plugin-vue-scan
        run: pnpm run build

      - name: Publish devpilot plugin to NPM
        working-directory: packages/devpilot-plugin-vue-scan
        run: pnpm publish --access public --no-git-checks

      # publish nuxt module

      - name: Prepare vue scan nuxt module
        working-directory: packages/nuxt
        run: pnpm run dev:prepare

      - name: Build vue scan nuxt module
        working-directory: packages/nuxt
        run: pnpm run prepack

      - name: Publish vue scan nuxt module to NPM
        working-directory: packages/nuxt
        run: pnpm publish --access public --no-git-checks

  createrelease:
    name: Create Release
    runs-on: [ubuntu-latest]
    steps:
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false
      - name: Output Release URL File
        run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt
      - name: Save Release URL File for publish
        uses: actions/upload-artifact@v4
        with:
          name: release_url
          path: release_url.txt

  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set node
        uses: actions/setup-node@v3
        with:
          node-version: 20.x

      - name: Setup
        run: npm i -g pnpm@9

      - name: Install
        run: pnpm i

      - name: Build vue scan
        run: pnpm run build

      - name: Build Extension
        run: pnpm -C packages/extension run zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Load Release URL File from release job
        uses: actions/download-artifact@v4
        with:
          name: release_url

      - name: Get Release File Name & Upload URL
        id: get_release_info
        shell: bash
        run: |
          value=`cat release_url.txt`
          echo ::set-output name=upload_url::$value

      - name: Set env
        shell: bash
        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV

      - name: Upload Release Asset
        id: upload-release-asset
        uses: softprops/action-gh-release@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          files: |
            ./packages/extension/.output/vue-scan-ext-${{ env.RELEASE_VERSION }}-chrome.zip


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

node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts

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


dist
test/**/pnpm-lock.yaml
package.back.json
cache.json
packages/extension/public/auto.[c|m]js

================================================
FILE: README.md
================================================
# z-vue-scan

A Vue scanning plugin that works with both Vue 2 and Vue 3. The component will flash with a red border when it will update.

[![NPM version](https://img.shields.io/npm/v/z-vue-scan?color=a1b858&label=)](https://www.npmjs.com/package/z-vue-scan)

## Features

- 🎯 Works with both Vue 2 and Vue 3
- 🔄 Powered by [vue-demi](https://github.com/vueuse/vue-demi)
- 📦 Lightweight
- 💪 Written in TypeScript

## Installation

```bash
# npm
npm install z-vue-scan

# yarn
yarn add z-vue-scan

# pnpm
pnpm add z-vue-scan
```

## Usage

```ts
interface Options {
  enable?: boolean
  hideCompnentName?: boolean
}
```

### Vue 3

```ts
// vue3
import { createApp } from 'vue'
import VueScan, { type VueScanOptions } from 'z-vue-scan'

import App from './App.vue'

const isProduction = import.meta.env.PROD // or `process.env.NODE_ENV === 'production'`

const app = createApp(App)

if (!isProduction) {
  app.use<VueScanOptions>(VueScan, {})
}

app.mount('#app')
```

### Vue 2

```ts
// vue2
import Vue from 'vue'
import VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'
import App from './App.vue'

const isProduction = import.meta.env.PROD // or `process.env.NODE_ENV === 'production'`

if (!isProduction) {
  Vue.use<VueScanBaseOptions>(VueScan, {})
}

new Vue({
  render: h => h(App),
}).$mount('#app')
```

### Nuxt Module

```bash
# npm
npm install z-vue-scan-nuxt-module

# yarn
yarn add z-vue-scan-nuxt-module

# pnpm
pnpm add z-vue-scan-nuxt-module
```

You can use z-vue-scan in your Nuxt project by adding it to the `modules` section in your `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  modules: ['z-vue-scan-nuxt-module'],
  vueScan: {
    // options
    enable: true,
    hideCompnentName: false
  }
})
```

### DevPilot Plugin (MCP for LLMs)

```bash
pnpm add unplugin-devpilot devpilot-plugin-vue-scan -D
```

The [DevPilot plugin](./packages/devpilot-plugin-vue-scan) exposes Vue component render performance data to LLMs via MCP. It tracks component re-renders in real time and provides a `queryVueScanData` tool that returns per-component aggregated summaries with source code locations — enabling LLMs to analyze render performance and pinpoint issues.

See [devpilot-plugin-vue-scan README](./packages/devpilot-plugin-vue-scan/README.md) for details.

## Development

```bash
# Install dependencies
pnpm install

# Run development server with Vue 3 example
pnpm dev

# Run development server with Vue 2 example
pnpm dev:vue2

# Build the package
pnpm build

# Run type check
pnpm typecheck

# Run linting
pnpm lint
```

## License

[MIT](./LICENSE) License  2024 [zcf0508](https://github.com/zcf0508)


================================================
FILE: build.config.ts
================================================
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig(
  [
    {
      entries: [
        'src/index',
        'src/index_vue2',
      ],
      rollup: {
        emitCJS: true,
        inlineDependencies: true,
        json: {
          compact: true,
          namedExports: false,
          preferConst: false,
        },
        commonjs: {
          requireReturnsDefault: 'auto',
        },
        dts: {
          respectExternal: false,
        },
      },
      externals: ['vue-demi'],
      clean: true,
      declaration: true,
    },
    {
      entries: [
        'src/auto',
      ],
      outDir: 'packages/extension/public',
      rollup: {
        emitCJS: true,
        output: {
          format: 'iife',
        },
      },
      externals: ['vue-demi'],
      clean: true,
      failOnWarn: false,
    },
  ],
)


================================================
FILE: eslint.config.js
================================================
import antfu from '@antfu/eslint-config'

export default antfu({
  ignores: [
    'docs',
    'dist',
    'packages/extension/.output',
    'packages/extension/.wxt',
    'packages/extension/public',
  ],
}, [
  {
    rules: {
      'no-console': ['warn'],
    },
  },
])


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

node_modules
.DS_Store
dist
dist-ssr
*.local

/cypress/videos/
/cypress/screenshots/

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


================================================
FILE: examples/vue2/README.md
================================================
# vue2

This template should help get you started developing with Vue 3 in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

## Type Support for `.vue` Imports in TS

TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.

If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:

1. Disable the built-in TypeScript Extension
    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

## Customize configuration

See [Vite Configuration Reference](https://vitejs.dev/config/).

## Project Setup

```sh
npm install
```

### Compile and Hot-Reload for Development

```sh
npm run dev
```

### Type-Check, Compile and Minify for Production

```sh
npm run build
```


================================================
FILE: examples/vue2/env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: examples/vue2/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: examples/vue2/package.json
================================================
{
  "name": "vue2",
  "version": "0.0.37",
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check build-only",
    "preview": "vite preview --port 4173",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit"
  },
  "dependencies": {
    "vue": "^2.7.7"
  },
  "devDependencies": {
    "@types/node": "^16.11.45",
    "@vitejs/plugin-legacy": "^2.0.0",
    "@vitejs/plugin-vue2": "^1.1.2",
    "@vue/composition-api": "^1.7.2",
    "@vue/tsconfig": "^0.1.3",
    "npm-run-all": "^4.1.5",
    "terser": "^5.14.2",
    "typescript": "~4.7.4",
    "vite": "^3.0.2",
    "vue-tsc": "^0.38.8",
    "z-vue-scan": "workspace:*"
  }
}


================================================
FILE: examples/vue2/src/App.vue
================================================
<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

const b = ref('')
</script>

<template>
  <div>
    <div>
      {{ b }}
    </div>
    <HelloWorld :value="b" @update:value="(v) => b = v" />
  </div>
</template>


================================================
FILE: examples/vue2/src/assets/base.css
================================================
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --vt-c-white: #ffffff;
  --vt-c-white-soft: #f8f8f8;
  --vt-c-white-mute: #f2f2f2;

  --vt-c-black: #181818;
  --vt-c-black-soft: #222222;
  --vt-c-black-mute: #282828;

  --vt-c-indigo: #2c3e50;

  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
  --vt-c-divider-dadarkrk-1: rgba(84, 84, 84, 0.65);
  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);

  --vt-c-text-light-1: var(--vt-c-indigo);
  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
  --vt-c-text-dark-1: var(--vt-c-white);
  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}

/* semantic color variables for this project */
:root {
  --color-background: var(--vt-c-white);
  --color-background-soft: var(--vt-c-white-soft);
  --color-background-mute: var(--vt-c-white-mute);

  --color-border: var(--vt-c-divider-light-2);
  --color-border-hover: var(--vt-c-divider-light-1);

  --color-heading: var(--vt-c-text-light-1);
  --color-text: var(--vt-c-text-light-1);

  --section-gap: 160px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: var(--vt-c-black);
    --color-background-soft: var(--vt-c-black-soft);
    --color-background-mute: var(--vt-c-black-mute);

    --color-border: var(--vt-c-divider-dark-2);
    --color-border-hover: var(--vt-c-divider-dark-1);

    --color-heading: var(--vt-c-text-dark-1);
    --color-text: var(--vt-c-text-dark-2);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  position: relative;
  font-weight: normal;
}

body {
  min-height: 100vh;
  color: var(--color-text);
  background: var(--color-background);
  transition: color 0.5s, background-color 0.5s;
  line-height: 1.6;
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
  font-size: 15px;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}


================================================
FILE: examples/vue2/src/assets/main.css
================================================
@import "./base.css";

#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;

  font-weight: normal;
}

a,
.green {
  text-decoration: none;
  color: hsla(160, 100%, 37%, 1);
  transition: 0.4s;
}

@media (hover: hover) {
  a:hover {
    background-color: hsla(160, 100%, 37%, 0.2);
  }
}

@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  #app {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  }
}


================================================
FILE: examples/vue2/src/components/HelloWorld.vue
================================================
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'HelloWorld',
  props: {
    value: String,
  },
  emits: ['update:value'],
  methods: {
    handleInput(e: Event) {
      this.$emit('update:value', (e.target as HTMLInputElement)?.value)
    },
  },
})
</script>

<template>
  <div>
    <div>
      {{ $props.value }}
    </div>
    input: <input :value="$props.value" @input="handleInput">
  </div>
</template>


================================================
FILE: examples/vue2/src/main.ts
================================================
import Vue from 'vue'
import VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'
import App from './App.vue'

import './assets/main.css'

Vue.use<VueScanBaseOptions>(VueScan, {})

new Vue({
  render: h => h(App),
}).$mount('#app')


================================================
FILE: examples/vue2/tsconfig.config.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.node.json",
  "compilerOptions": {
    "composite": true,
    "types": ["node"]
  },
  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"]
}


================================================
FILE: examples/vue2/tsconfig.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },

  "references": [
    {
      "path": "./tsconfig.config.json"
    }
  ],
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"]
}


================================================
FILE: examples/vue2/vite.config.ts
================================================
import { fileURLToPath, URL } from 'node:url'

import legacy from '@vitejs/plugin-legacy'
import vue2 from '@vitejs/plugin-vue2'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue2(),
    legacy({
      targets: ['ie >= 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})


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

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

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

*.tsbuildinfo


================================================
FILE: examples/vue3/.vscode/extensions.json
================================================
{
  "recommendations": ["Vue.volar"]
}


================================================
FILE: examples/vue3/README.md
================================================
# vue3

This template should help get you started developing with Vue 3 in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).

## Type Support for `.vue` Imports in TS

TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.

## Customize configuration

See [Vite Configuration Reference](https://vite.dev/config/).

## Project Setup

```sh
npm install
```

### Compile and Hot-Reload for Development

```sh
npm run dev
```

### Type-Check, Compile and Minify for Production

```sh
npm run build
```


================================================
FILE: examples/vue3/env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: examples/vue3/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: examples/vue3/package.json
================================================
{
  "name": "vue3",
  "type": "module",
  "version": "0.0.37",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force"
  },
  "dependencies": {
    "vue": "^3.5.12"
  },
  "devDependencies": {
    "@tsconfig/node22": "^22.0.0",
    "@types/node": "^22.9.0",
    "@vitejs/plugin-vue": "^5.1.4",
    "@vue/tsconfig": "^0.5.1",
    "npm-run-all2": "^7.0.1",
    "typescript": "~5.6.3",
    "vite": "^5.4.10",
    "vite-plugin-vue-devtools": "^7.5.4",
    "vue-tsc": "^2.1.10",
    "z-vue-scan": "workspace:*"
  }
}


================================================
FILE: examples/vue3/src/App.vue
================================================
<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

const b = ref('')
</script>

<template>
  <div>
    <div>
      {{ b }}
    </div>
    <HelloWorld v-model:msg="b" />
  </div>
</template>


================================================
FILE: examples/vue3/src/assets/base.css
================================================
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --vt-c-white: #ffffff;
  --vt-c-white-soft: #f8f8f8;
  --vt-c-white-mute: #f2f2f2;

  --vt-c-black: #181818;
  --vt-c-black-soft: #222222;
  --vt-c-black-mute: #282828;

  --vt-c-indigo: #2c3e50;

  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);

  --vt-c-text-light-1: var(--vt-c-indigo);
  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
  --vt-c-text-dark-1: var(--vt-c-white);
  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}

/* semantic color variables for this project */
:root {
  --color-background: var(--vt-c-white);
  --color-background-soft: var(--vt-c-white-soft);
  --color-background-mute: var(--vt-c-white-mute);

  --color-border: var(--vt-c-divider-light-2);
  --color-border-hover: var(--vt-c-divider-light-1);

  --color-heading: var(--vt-c-text-light-1);
  --color-text: var(--vt-c-text-light-1);

  --section-gap: 160px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: var(--vt-c-black);
    --color-background-soft: var(--vt-c-black-soft);
    --color-background-mute: var(--vt-c-black-mute);

    --color-border: var(--vt-c-divider-dark-2);
    --color-border-hover: var(--vt-c-divider-dark-1);

    --color-heading: var(--vt-c-text-dark-1);
    --color-text: var(--vt-c-text-dark-2);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  font-weight: normal;
}

body {
  min-height: 100vh;
  color: var(--color-text);
  background: var(--color-background);
  transition:
    color 0.5s,
    background-color 0.5s;
  line-height: 1.6;
  font-family:
    Inter,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    'Fira Sans',
    'Droid Sans',
    'Helvetica Neue',
    sans-serif;
  font-size: 15px;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}


================================================
FILE: examples/vue3/src/assets/main.css
================================================
@import './base.css';

#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  font-weight: normal;
}

a,
.green {
  text-decoration: none;
  color: hsla(160, 100%, 37%, 1);
  transition: 0.4s;
  padding: 3px;
}

@media (hover: hover) {
  a:hover {
    background-color: hsla(160, 100%, 37%, 0.2);
  }
}

@media (min-width: 1024px) {
  body {
    display: flex;
    place-items: center;
  }

  #app {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  }
}


================================================
FILE: examples/vue3/src/components/HelloWorld.vue
================================================
<script setup lang="ts">
const msg = defineModel('msg', {
  default: '',
})
</script>

<template>
  <div>
    <div>
      {{ msg }}
    </div>
    input: <input v-model="msg">
  </div>
</template>


================================================
FILE: examples/vue3/src/main.ts
================================================
import { createApp } from 'vue'
import VueScan, { type VueScanOptions } from 'z-vue-scan'

import App from './App.vue'
import './assets/main.css'

const app = createApp(App)
app.use<VueScanOptions>(VueScan, {})
app.mount('#app')


================================================
FILE: examples/vue3/tsconfig.app.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"]
}


================================================
FILE: examples/vue3/tsconfig.json
================================================
{
  "references": [
    {
      "path": "./tsconfig.node.json"
    },
    {
      "path": "./tsconfig.app.json"
    }
  ],
  "files": []
}


================================================
FILE: examples/vue3/tsconfig.node.json
================================================
{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",

    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"],
    "noEmit": true
  },
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*"
  ]
}


================================================
FILE: examples/vue3/vite.config.ts
================================================
import { fileURLToPath, URL } from 'node:url'

import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})


================================================
FILE: package.json
================================================
{
  "name": "z-vue-scan",
  "type": "module",
  "version": "0.0.37",
  "description": "The component will flash with a red border when it will update.",
  "author": "zcf0508",
  "license": "MIT",
  "homepage": "https://github.com/zcf0508/vue-scan#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zcf0508/vue-scan.git"
  },
  "bugs": {
    "url": "https://github.com/zcf0508/vue-scan/issues"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./vue2": {
      "types": "./dist/index_vue2.d.mts",
      "import": "./dist/index_vue2.mjs",
      "require": "./dist/index_vue2.cjs"
    }
  },
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "lint": "eslint .",
    "dev": "pnpm -C examples/vue3 dev",
    "dev:vue2": "pnpm -C examples/vue2 dev",
    "build": "unbuild",
    "typecheck": "tsc",
    "release": "bumpp -r"
  },
  "peerDependencies": {
    "@vue/composition-api": "^1.0.0-rc.1",
    "vue": "^2.0.0 || >=3.0.0"
  },
  "peerDependenciesMeta": {
    "@vue/composition-api": {
      "optional": true
    }
  },
  "dependencies": {
    "vue-demi": "latest"
  },
  "devDependencies": {
    "@antfu/eslint-config": "^3.9.2",
    "@types/lodash-es": "^4.17.12",
    "@types/node": "^22.10.0",
    "@vue/devtools-kit": "^7.6.4",
    "@vue/devtools-shared": "^7.6.7",
    "bumpp": "^9.8.1",
    "consola": "^3.2.3",
    "lodash-es": "^4.17.21",
    "typescript": "~5.6.3",
    "unbuild": "^2.0.0",
    "vue": "^3.0.0"
  },
  "pnpm": {
    "patchedDependencies": {
      "@vue/devtools-kit": "patches/@vue__devtools-kit.patch"
    }
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/README.md
================================================
# devpilot-plugin-vue-scan

A [DevPilot](https://github.com/zcf0508/unplugin-devpilot) plugin that exposes Vue component render performance data to LLMs via MCP.

[![NPM version](https://img.shields.io/npm/v/devpilot-plugin-vue-scan?color=a1b858&label=)](https://www.npmjs.com/package/devpilot-plugin-vue-scan)

## What It Does

- Injects hooks into Vue 2/3 component `beforeUpdate` lifecycle to track re-renders in real time
- Stores update events server-side in a circular buffer (10,000 events max)
- Provides a `queryVueScanData` MCP tool that returns **per-component aggregated summaries** with source code locations
- Enables LLMs to analyze render performance, identify hot components, and pinpoint the source file

## Installation

```bash
pnpm add devpilot-plugin-vue-scan
```

## Usage

Register as a DevPilot plugin in your Vite config:

```ts
import vueScanPlugin from 'devpilot-plugin-vue-scan'
import DevPilot from 'unplugin-devpilot/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    DevPilot({
      plugins: [vueScanPlugin],
    }),
  ],
})
```

## How It Works

1. **Start recording** — Press `Ctrl+Shift+R` or click the control panel's Start button
2. **Interact with the page** — Component updates are captured automatically
3. **Query via MCP** — LLM calls `queryVueScanData` to get aggregated performance data

## MCP Tool: `queryVueScanData`

Returns per-component render performance summaries.

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | number | 50 | Max components to return |
| `componentName` | string | - | Filter by name (partial match) |
| `timeRange` | `{ start, end }` | - | Filter by timestamp range (ms) |
| `minUpdateCount` | number | - | Only components updated ≥ N times |
| `onlyInViewport` | boolean | - | Exclude off-screen renders |
| `onlyUserComponents` | boolean | true | Filter out library components |
| `includeRawEvents` | boolean | false | Include individual event details |
| `sortBy` | string | totalUpdates | `totalUpdates` / `updatesPerSecond` / `componentName` |

### Response

```jsonc
{
  "components": [
    {
      "componentName": "UserList",
      "sourceLocation": "src/views/users/UserList.vue:25:5:div",
      "totalUpdates": 42,
      "updatesPerSecond": 14.0,
      "firstUpdate": 1772592779510,
      "lastUpdate": 1772592782451
    }
  ],
  "metadata": {
    "recordingStatus": "active",
    "totalEvents": 997,
    "uniqueComponents": 20,
    "bufferSize": 997,
    "bufferCapacity": 10000,
    "timeRange": { "start": 1772592779510, "end": 1772592782453 }
  }
}
```

### User vs Library Components

Components are identified as user code when their DOM root element has a `data-insp-path` attribute (injected by `unplugin-devpilot` at build time) or when `instance.type.__file` points to a path outside `node_modules`. With `onlyUserComponents: true` (default), library components are filtered out.

## Control Panel

A floating panel at the bottom-right corner of the page provides:

- **Start/Pause** recording toggle
- **Clear** recorded data
- **Event counter** showing buffer usage
- **Keyboard shortcuts**: `Ctrl+Shift+V` (toggle panel), `Ctrl+Shift+R` (toggle recording)

## License

[MIT](../../LICENSE)


================================================
FILE: packages/devpilot-plugin-vue-scan/package.json
================================================
{
  "name": "devpilot-plugin-vue-scan",
  "type": "module",
  "version": "0.0.37",
  "description": "Vue Scan plugin for DevPilot",
  "author": "zcf0508 <zcf0508@live.com>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zcf0508/vue-scan.git"
  },
  "exports": {
    ".": "./dist/index.mjs",
    "./client": "./dist/client/index.mjs",
    "./package.json": "./package.json"
  },
  "typesVersions": {
    "*": {
      "*": [
        "./dist/*.d.ts",
        "./*"
      ]
    }
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsdown",
    "dev": "tsdown --watch",
    "typecheck:client": "tsc --project tsconfig.client.json --noEmit",
    "typecheck:node": "tsc --noEmit",
    "typecheck": "pnpm run typecheck:client && pnpm run typecheck:node",
    "prepublishOnly": "pnpm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.25.3",
    "@types/ws": "^8.18.1",
    "birpc": "^4.0.0",
    "es-toolkit": "^1.44.0",
    "hookable": "^6.0.1",
    "mitt": "^3.0.1",
    "unplugin": "^3.0.0",
    "unplugin-devpilot": "^0.0.11",
    "ws": "^8.19.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "tsdown": "^0.20.1"
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/control-panel.ts
================================================
import type { DevpilotClient } from 'unplugin-devpilot/client'
import type { VueScanServerMethods } from './types'

// Extend global interface for storing listener reference
declare global {
  interface Document {
    __vueScanKeydownListener?: (e: KeyboardEvent) => void
  }
}

// Update control panel UI
function updatePanelContent(panel: HTMLDivElement, client: DevpilotClient<VueScanServerMethods>): void {
  const runtime = window.__VUE_SCAN_RUNTIME__
  const isRecording = runtime?.isRecording || false

  panel.innerHTML = `
    <div style="margin-bottom: 8px; font-weight: bold;">
      🔍 Vue Scan
    </div>
    <div style="margin-bottom: 8px; display: flex; align-items: center;">
      <span style="
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: ${isRecording ? '#00ff00' : '#666'};
        margin-right: 6px;
        display: inline-block;
      "></span>
      <span>${isRecording ? 'Recording' : 'Paused'}</span>
    </div>
    <div style="margin-bottom: 8px; color: #888;">
      Events: <span id="event-count">-</span>
    </div>
    <div style="display: flex; gap: 4px; flex-wrap: wrap;">
      <button id="toggle-btn" style="
        padding: 4px 8px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        background: ${isRecording ? '#ff6b6b' : '#51cf66'};
        color: white;
      ">
        ${isRecording ? '⏸️ Pause' : '▶️ Start'}
      </button>
      <button id="clear-btn" style="
        padding: 4px 8px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        background: #495057;
        color: white;
      ">
        🗑️ Clear
      </button>
      <button id="stats-btn" style="
        padding: 4px 8px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        background: #495057;
        color: white;
      ">
        📊 Stats
      </button>
    </div>
    <div style="margin-top: 8px; font-size: 10px; color: #666;">
      Ctrl+Shift+V: Toggle Panel<br>
      Ctrl+Shift+R: Toggle Recording
    </div>
  `

  // Bind events
  const toggleBtn = panel.querySelector('#toggle-btn')
  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
      runtime?.toggleRecording()
      updatePanelContent(panel, client)
    })
  }

  const clearBtn = panel.querySelector('#clear-btn')
  if (clearBtn) {
    clearBtn.addEventListener('click', () => {
      runtime?.clearData()
    })
  }

  const statsBtn = panel.querySelector('#stats-btn')
  if (statsBtn) {
    statsBtn.addEventListener('click', async () => {
      const data = await client.rpcCall('vue-scan:exportData')
      // eslint-disable-next-line no-alert
      alert(JSON.stringify(data, null, 2))
    })
  }
}

// Get current page's panel from DOM
function getCurrentPanel(): HTMLDivElement | null {
  return document.getElementById('vue-scan-control-panel') as HTMLDivElement | null
}

// Register keyboard shortcuts
export function registerKeyboardShortcuts(client: DevpilotClient<VueScanServerMethods>): void {
  // Remove existing listener if any to avoid duplicates
  if (document.__vueScanKeydownListener) {
    document.removeEventListener('keydown', document.__vueScanKeydownListener)
  }

  const keydownListener = (e: KeyboardEvent) => {
    // Ctrl+Shift+V: Toggle panel visibility
    if (e.ctrlKey && e.shiftKey && e.key === 'V') {
      const panel = getCurrentPanel()
      if (panel) {
        panel.style.display = panel.style.display === 'none' ? 'block' : 'none'
        // eslint-disable-next-line no-console
        console.log(`[Vue Scan] Panel ${panel.style.display === 'none' ? 'hidden' : 'shown'}`)
      }
      e.preventDefault()
    }

    // Ctrl+Shift+R: Toggle recording
    if (e.ctrlKey && e.shiftKey && e.key === 'R') {
      window.__VUE_SCAN_RUNTIME__?.toggleRecording()
      const panel = getCurrentPanel()
      if (panel) {
        updatePanelContent(panel, client)
      }
      e.preventDefault()
    }
  }

  document.addEventListener('keydown', keydownListener)
  document.__vueScanKeydownListener = keydownListener

  // eslint-disable-next-line no-console
  console.log('[Vue Scan] Keyboard shortcuts registered (Ctrl+Shift+V/R)')
}

// Create control panel UI
export function createControlPanel(client: DevpilotClient<VueScanServerMethods>): HTMLDivElement | null {
  // Check if panel already exists in current document
  if (document.getElementById('vue-scan-control-panel')) {
    return null
  }

  const panel = document.createElement('div')
  panel.id = 'vue-scan-control-panel'
  panel.style.cssText = `
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: rgba(0, 0, 0, 0.9);
    color: white;
    padding: 12px;
    border-radius: 8px;
    font-family: monospace;
    font-size: 12px;
    z-index: 10000;
    min-width: 200px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
  `

  document.body.appendChild(panel)
  panel.style.display = 'none'
  updatePanelContent(panel, client)

  // Update event count periodically (only when panel is visible)
  setInterval(async () => {
    const currentPanel = getCurrentPanel()
    if (!currentPanel || currentPanel.style.display === 'none') {
      return
    }
    try {
      const data = await client.rpcCall('vue-scan:exportData') as any
      const countEl = currentPanel.querySelector('#event-count')
      if (countEl && data) {
        countEl.textContent = `${data.events?.length || 0} / 10000`
      }
    }
    catch {
      // Ignore errors
    }
  }, 1000)

  // eslint-disable-next-line no-console
  console.log('[Vue Scan] Control panel created')

  return panel
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/fps.ts
================================================
const WINDOW_MS = 1000

let frameTimestamps: number[] = []
let rafId: number | null = null

function tick() {
  const now = performance.now()
  frameTimestamps.push(now)

  const cutoff = now - WINDOW_MS
  while (frameTimestamps.length > 0 && frameTimestamps[0] < cutoff) {
    frameTimestamps.shift()
  }

  rafId = requestAnimationFrame(tick)
}

function ensureRunning() {
  if (rafId === null) {
    rafId = requestAnimationFrame(tick)
  }
}

export function getCurrentFps(): number {
  ensureRunning()
  if (frameTimestamps.length < 2)
    return -1
  return Math.round(frameTimestamps.length * 1000 / WINDOW_MS)
}

export function stopFpsMonitor(): void {
  if (rafId !== null) {
    cancelAnimationFrame(rafId)
    rafId = null
  }
  frameTimestamps = []
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/helpers.ts
================================================
export interface ComponentBoundingRect {
  top: number
  left: number
  width: number
  height: number
  right: number
  bottom: number
}

const DEFAULT_RECT: ComponentBoundingRect = {
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  width: 0,
  height: 0,
}

export function getInstanceName(instance: any): string {
  const type = instance?.type || instance?.$vnode || {}
  const name = type?.name || type?._componentTag || type?.tag
    || type?.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ || type?.__name
  if (name)
    return name
  if (instance?.root === instance || instance?.$root === instance)
    return 'Root'

  // guess from parent
  for (const key in instance?.parent?.type?.components) {
    if (instance.parent.type.components[key] === instance?.type)
      return key
  }
  for (const key in instance?.appContext?.components) {
    if (instance.appContext.components[key] === instance?.type)
      return key
  }

  // from filename
  const file = type?.__file
  if (file) {
    const base = file.split(/[/\\]/).pop() || ''
    return base.replace(/\.vue$/, '')
  }

  return 'Anonymous Component'
}

function isFragment(instance: any): boolean {
  const subTreeType = instance?.subTree?.type
  if (!subTreeType)
    return false
  const appRecord = instance?.__VUE_DEVTOOLS_NEXT_APP_RECORD__
    || instance?.root?.appContext?.app?.__VUE_DEVTOOLS_NEXT_APP_RECORD__
  if (appRecord)
    return appRecord?.types?.Fragment === subTreeType
  return false
}

function createRect() {
  const rect = {
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    get width() { return rect.right - rect.left },
    get height() { return rect.bottom - rect.top },
  }
  return rect
}

function mergeRects(a: any, b: any) {
  if (!a.top || b.top < a.top)
    a.top = b.top
  if (!a.bottom || b.bottom > a.bottom)
    a.bottom = b.bottom
  if (!a.left || b.left < a.left)
    a.left = b.left
  if (!a.right || b.right > a.right)
    a.right = b.right
  return a
}

let range: Range | null = null
function getTextRect(node: any) {
  if (!range)
    range = document.createRange()
  range.selectNode(node)
  return range.getBoundingClientRect()
}

function getFragmentRect(vnode: any): ComponentBoundingRect {
  const rect = createRect()
  if (!vnode.children)
    return rect
  for (let i = 0; i < vnode.children.length; i++) {
    const childVnode = vnode.children[i]
    let childRect
    if (childVnode.component) {
      childRect = getComponentBoundingRect(childVnode.component)
    }
    else if (childVnode.el) {
      const el = childVnode.el
      if (el.nodeType === 1 || el.getBoundingClientRect)
        childRect = el.getBoundingClientRect()
      else if (el.nodeType === 3 && el.data.trim())
        childRect = getTextRect(el)
    }
    if (childRect)
      mergeRects(rect, childRect)
  }
  return rect
}

export function getComponentBoundingRect(instance: any): ComponentBoundingRect {
  const el = instance?.subTree?.el || instance?.$el
  if (typeof window === 'undefined')
    return DEFAULT_RECT
  if (isFragment(instance))
    return getFragmentRect(instance?.subTree)
  if (el?.nodeType === 1)
    return el.getBoundingClientRect()
  if (instance?.subTree?.component || instance?.$vnode)
    return getComponentBoundingRect(instance?.subTree?.component || instance?.$vnode)
  return DEFAULT_RECT
}

export function isInViewport(bounds: ComponentBoundingRect): boolean {
  return !(
    bounds.left >= window.innerWidth
    || bounds.right <= 0
    || bounds.top >= window.innerHeight
    || bounds.bottom <= 0
  )
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/index.ts
================================================
import type { VueScanClientRpc, VueScanServerMethods } from './types'
// Client-side entry point for Vue Scan DevPilot Plugin
import { defineRpcHandlers, getDevpilotClient } from 'unplugin-devpilot/client'
import { createControlPanel, registerKeyboardShortcuts } from './control-panel'
import { initRuntimeControl } from './runtime-control'
import { injectVueScanWithRuntimeControl } from './vue-injector'

// Define RPC handlers (currently empty as we don't need server-to-client RPC)
export const rpcHandlers: VueScanClientRpc = defineRpcHandlers<VueScanClientRpc>({
  // Add client-side RPC handlers here if needed
})

function setup() {
  const client = getDevpilotClient<VueScanServerMethods>()
  if (!client) {
    // Virtual module not initialized yet, retry
    // eslint-disable-next-line no-console
    console.log('[Vue Scan] Waiting for DevPilot client...')
    setTimeout(setup, 100)
    return
  }

  // eslint-disable-next-line no-console
  console.log('[Vue Scan] DevPilot client found, connecting...')

  // Track connection timeout
  let connectionTimeout: ReturnType<typeof setTimeout> | null = null

  // Wait for WebSocket connection to be ready
  client.onConnected(() => {
    if (connectionTimeout) {
      clearTimeout(connectionTimeout)
    }

    // eslint-disable-next-line no-console
    console.log('[Vue Scan] Connected to DevPilot server')

    // 1. Initialize runtime control
    initRuntimeControl(client)

    // 2. Inject Vue monitoring with runtime control
    injectVueScanWithRuntimeControl(client)

    // 3. Create control panel
    createControlPanel(client)

    // 4. Register keyboard shortcuts (now that client is available)
    registerKeyboardShortcuts(client)

    // eslint-disable-next-line no-console
    console.log(
      '%c 🔍 Vue Scan DevPilot %c Ctrl+Shift+V → Toggle Panel | Ctrl+Shift+R → Toggle Recording ',
      'background:#35495e;color:#fff;padding:2px 4px;border-radius:3px 0 0 3px;',
      'background:#41b883;color:#fff;padding:2px 4px;border-radius:0 3px 3px 0;',
    )
  })

  // Set connection timeout warning
  connectionTimeout = setTimeout(() => {
    // eslint-disable-next-line no-console
    console.warn('[Vue Scan] WebSocket connection timeout - DevPilot server may not be running')
  }, 5000)
}

setup()


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/runtime-control.ts
================================================
import type { DevpilotClient } from 'unplugin-devpilot/client'
import type { RuntimeControl, VueScanServerMethods } from './types'

// Initialize runtime control
export function initRuntimeControl(client: DevpilotClient<VueScanServerMethods>): RuntimeControl {
  if (!window.__VUE_SCAN_RUNTIME__) {
    window.__VUE_SCAN_RUNTIME__ = {
      isRecording: false,
      showHighlight: false,

      startRecording() {
        this.isRecording = true
        this.showHighlight = true
        client.rpcCall('vue-scan:startRecording').catch(() => {})
        console.log('📊 Vue Scan: Recording started')
      },

      stopRecording() {
        this.isRecording = false
        this.showHighlight = false
        client.rpcCall('vue-scan:stopRecording').catch(() => {})
        console.log('⏸️ Vue Scan: Recording stopped')
      },

      toggleRecording() {
        if (this.isRecording) {
          this.stopRecording()
        }
        else {
          this.startRecording()
        }
      },

      clearData() {
        client.rpcCall('vue-scan:clearData').catch(() => {})
        console.log('🗑️ Vue Scan: Data cleared')
      },
    }
  }

  return window.__VUE_SCAN_RUNTIME__
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/types.ts
================================================
// Server-side RPC methods that client can call
export interface VueScanServerMethods {
  'vue-scan:recordUpdate': (data: {
    timestamp: number
    componentName: string
    componentId: string
    phase: 'mount' | 'update'
    renderTime?: number
    fps: number
    updateCount: number
    bounds: {
      width: number
      height: number
      top: number
      left: number
    }
    isInViewport: boolean
    isUserComponent: boolean
    sourceLocation?: string
  }) => Promise<void>
  'vue-scan:startRecording': () => Promise<{ status: string }>
  'vue-scan:stopRecording': () => Promise<{ status: string }>
  'vue-scan:clearData': () => Promise<{ status: string }>
  'vue-scan:exportData': () => Promise<unknown>
}

// Client-side RPC interface (methods that server can call on client)
export interface VueScanClientRpc {
  // Add client-side RPC methods here if needed in the future
}

export interface RuntimeControl {
  isRecording: boolean
  showHighlight: boolean
  startRecording: () => void
  stopRecording: () => void
  toggleRecording: () => void
  clearData: () => void
}

// Extend window interface
declare global {
  interface Window {
    __VUE_SCAN_RUNTIME__?: RuntimeControl
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/client/vue-injector.ts
================================================
/**
 * Vue component injection for data collection.
 * Follows the same injection pattern as src/auto.ts,
 * adding a data-report hook that sends update events via RPC.
 */
import type { DevpilotClient } from 'unplugin-devpilot/client'
import type { VueScanServerMethods } from './types'
import { throttle } from 'lodash-es'
import { getCurrentFps } from './fps'
import { getComponentBoundingRect, getInstanceName, isInViewport } from './helpers'

interface VueInstance {
  uid?: number
  _uid?: number
  $el?: HTMLElement
  subTree?: {
    el?: HTMLElement
    type?: any
    component?: VueInstance
    children?: any
  }
  children?: any
  $options?: any
  $children?: VueInstance[]
  $vnode?: { componentInstance?: VueInstance }
  $set?: (target: any, key: string, value: any) => void
  type?: any
  root?: any
  $root?: any
  appContext?: any
  parent?: any
  __vue_scan_injected__?: boolean
  __flashCount?: number
  __flashTimeout?: ReturnType<typeof setTimeout> | null
  __renderStartTime?: number | null
  __dataReportHook?: (() => void) | null
  __beforeUpdateHook?: (() => void) | null
  __updatedHook?: (() => void) | null
  bu?: Array<() => void> | null
  u?: Array<() => void> | null
  bum?: Array<() => void> | null
}

function getSourceLocation(instance: VueInstance): string | undefined {
  // 1. Check data-insp-path on root element (injected by unplugin-devpilot)
  const el = instance?.subTree?.el || instance.$el
  if (el instanceof HTMLElement) {
    const inspPath = el.getAttribute('data-insp-path')
    if (inspPath)
      return inspPath
  }

  // 2. Fallback to __file from Vue compiler
  const file = instance?.type?.__file
  if (file)
    return file

  return undefined
}

function isFromUserCode(source: string | undefined): boolean {
  if (!source)
    return false
  return !source.includes('node_modules')
}

function sendReportEvent(
  instance: VueInstance,
  client: DevpilotClient<VueScanServerMethods>,
  phase: 'mount' | 'update',
  renderTime?: number,
) {
  const runtime = window.__VUE_SCAN_RUNTIME__
  if (!runtime?.isRecording)
    return

  const name = getInstanceName(instance)
  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')
  const sourceLocation = getSourceLocation(instance)
  const isUserComponent = isFromUserCode(sourceLocation)

  if (!instance.__flashCount) {
    instance.__flashCount = 0
  }
  instance.__flashCount++

  const bounds = getComponentBoundingRect(instance)

  client.rpcCall('vue-scan:recordUpdate', {
    timestamp: Date.now(),
    componentName: name,
    componentId: uuid,
    phase,
    renderTime,
    fps: getCurrentFps(),
    updateCount: instance.__flashCount,
    bounds: {
      width: bounds.width,
      height: bounds.height,
      top: bounds.top,
      left: bounds.left,
    },
    isInViewport: isInViewport(bounds),
    isUserComponent,
    sourceLocation,
  }).catch(() => {})
}

function createBeforeUpdateHook(instance: VueInstance) {
  return () => {
    const runtime = window.__VUE_SCAN_RUNTIME__
    if (!runtime?.isRecording)
      return
    instance.__renderStartTime = performance.now()
  }
}

function createMountedReportHook(instance: VueInstance, client: DevpilotClient<VueScanServerMethods>) {
  const el = instance?.subTree?.el || instance.$el
  if (!el)
    return

  return () => {
    sendReportEvent(instance, client, 'mount')
  }
}

function createUpdatedReportHook(instance: VueInstance, client: DevpilotClient<VueScanServerMethods>) {
  const el = instance?.subTree?.el || instance.$el
  if (!el)
    return

  return () => {
    const renderTime = instance.__renderStartTime != null
      ? performance.now() - instance.__renderStartTime
      : undefined
    instance.__renderStartTime = null
    sendReportEvent(instance, client, 'update', renderTime)
  }
}

function injectVueScan(node: HTMLElement, client: DevpilotClient<VueScanServerMethods>) {
  // @ts-expect-error vue internal
  if (node.__vue_app__) {
    // Vue 3
    // @ts-expect-error vue internal
    const vueApp = node.__vue_app__

    // Register mixin to cover future-mounted components
    if (!vueApp.__vue_scan_mixin_installed__) {
      vueApp.mixin({
        mounted(this: any) {
          const instance = this.$ as VueInstance
          if (!instance.__dataReportHook) {
            instance.__dataReportHook = createMountedReportHook(instance, client)
          }
          if (!instance.__beforeUpdateHook) {
            instance.__beforeUpdateHook = createBeforeUpdateHook(instance)
          }
          if (!instance.__updatedHook) {
            instance.__updatedHook = createUpdatedReportHook(instance, client)
          }
          instance.__dataReportHook?.()
        },
        beforeUpdate(this: any) {
          const instance = this.$ as VueInstance
          instance.__beforeUpdateHook?.()
        },
        updated(this: any) {
          const instance = this.$ as VueInstance
          instance.__updatedHook?.()
        },
      })
      vueApp.__vue_scan_mixin_installed__ = true
    }

    const vueInstance = vueApp._container._vnode.component as VueInstance

    function mixinChildren(children: any) {
      if (!children || typeof children === 'string' || !Array.isArray(children))
        return

      children.forEach((item: any) => {
        if (typeof item !== 'object')
          return
        if (item && 'component' in item && item.component) {
          mixin(item.component as VueInstance)
        }
        else if (item && 'children' in item) {
          mixinChildren(item.children)
        }
      })
    }

    function mixin(instance: VueInstance) {
      if (instance.subTree?.el && instance.__vue_scan_injected__ !== true) {
        const beforeUpdate = createBeforeUpdateHook(instance)
        const updated = createUpdatedReportHook(instance, client)

        if (beforeUpdate) {
          if (instance.bu) {
            instance.bu.push(beforeUpdate)
          }
          else {
            instance.bu = [beforeUpdate]
          }
        }

        if (updated) {
          if (instance.u) {
            instance.u.push(updated)
          }
          else {
            instance.u = [updated]
          }
        }

        instance.__vue_scan_injected__ = true
      }

      if (!instance.subTree?.component && instance.subTree?.children) {
        mixinChildren(instance.subTree.children)
      }
      else if (instance.subTree?.component) {
        mixin(instance.subTree.component as VueInstance)
      }
      else if (!instance.subTree && instance.children) {
        mixinChildren(instance.children)
      }
    }

    mixin(vueInstance)
    vueInstance.__vue_scan_injected__ = true
  }
  // @ts-expect-error vue internal
  else if (node.__vue__) {
    // Vue 2
    // @ts-expect-error vue internal
    const vueInstance = (node.__vue__?.$vnode?.componentInstance || node.__vue__) as VueInstance

    function mixin(instance: VueInstance) {
      if (instance?.$el && instance.__vue_scan_injected__ !== true) {
        const beforeUpdate = createBeforeUpdateHook(instance)
        const updated = createUpdatedReportHook(instance, client)

        if (beforeUpdate) {
          if (instance.$options?.beforeUpdate) {
            const arr = [...instance.$options.beforeUpdate]
            arr.push(beforeUpdate)
            instance.$set!(instance.$options, 'beforeUpdate', arr)
          }
          else if (instance.$options) {
            instance.$set!(instance.$options, 'beforeUpdate', [beforeUpdate])
          }
        }

        if (updated) {
          if (instance.$options?.updated) {
            const arr = [...instance.$options.updated]
            arr.push(updated)
            instance.$set!(instance.$options, 'updated', arr)
          }
          else if (instance.$options) {
            instance.$set!(instance.$options, 'updated', [updated])
          }
        }

        instance.__vue_scan_injected__ = true
      }

      if (instance.$children) {
        (instance.$children as Array<VueInstance>).forEach((child) => {
          mixin(child)
        })
      }
    }

    mixin(vueInstance)
    vueInstance.__vue_scan_injected__ = true
  }
}

function getMountDoms() {
  return Array.from(document.body.children).filter((element) => {
    // @ts-expect-error vue internal
    return !!(element.__vue_app__ || element.__vue__)
  }) as HTMLElement[]
}

function createDomMutationObserver(
  getTarget: () => HTMLElement | null,
  callback: MutationCallback,
  options: MutationObserverInit,
  throttleWait: number,
) {
  const targetObserver = new MutationObserver(throttle(callback, throttleWait))

  const findTargetObserver = new MutationObserver(throttle(() => {
    const target = getTarget()
    if (target) {
      findTargetObserver.disconnect()
      targetObserver.observe(target, options)
    }
  }, 200))

  findTargetObserver.observe(document.body, {
    childList: true,
    subtree: true,
  })

  return targetObserver
}

export function injectVueScanWithRuntimeControl(client: DevpilotClient<VueScanServerMethods>): void {
  const vue2ObserverMap = new WeakMap<HTMLElement, MutationObserver>()
  let vue3Found = false
  let checkCount = 0
  const maxChecks = 100 // ~60 seconds with 600ms throttle

  // eslint-disable-next-line no-console
  console.log('[Vue Scan] Starting Vue app detection...')

  const documentObserver = new MutationObserver(throttle(() => {
    const mountDoms = getMountDoms()
    checkCount++

    if (mountDoms.length === 0) {
      if (checkCount === 1) {
        // eslint-disable-next-line no-console
        console.log('[Vue Scan] No Vue app found yet, waiting for mount...')
      }
      if (checkCount >= maxChecks) {
        // eslint-disable-next-line no-console
        console.warn('[Vue Scan] Timeout: No Vue app detected after 60s')
        documentObserver.disconnect()
      }
      return
    }

    // eslint-disable-next-line no-console
    console.log(`[Vue Scan] Found ${mountDoms.length} Vue mount point(s)`)

    const isVue3 = mountDoms.some((mountDom) => {
      // @ts-expect-error vue internal
      return !!mountDom.__vue_app__
    })

    if (isVue3 && !vue3Found) {
      // eslint-disable-next-line no-console
      console.log('[Vue Scan] Vue 3 app detected, injecting scanner...')
      vue3Found = true
    }

    if (isVue3) {
      documentObserver.disconnect()
    }

    mountDoms.forEach((mountDom) => {
      // @ts-expect-error vue internal
      if (mountDom.__vue_app__) {
        documentObserver.disconnect()
        injectVueScan(mountDom, client)
      }
      else {
        if (!vue2ObserverMap.get(mountDom)) {
          // eslint-disable-next-line no-console
          console.log('[Vue Scan] Vue 2 app detected, injecting scanner...')
          vue2ObserverMap.set(mountDom, createDomMutationObserver(
            () => mountDom,
            () => injectVueScan(mountDom, client),
            { childList: true, subtree: true },
            600,
          ))
        }
      }
    })
  }, 600))

  documentObserver.observe(document.body, {
    attributes: true,
    childList: true,
    subtree: true,
  })

  // Try immediate injection
  const mountDoms = getMountDoms()
  if (mountDoms.length > 0) {
    // eslint-disable-next-line no-console
    console.log(`[Vue Scan] ${mountDoms.length} Vue mount point(s) found immediately`)
    mountDoms.forEach((mountDom) => {
      injectVueScan(mountDom, client)
    })
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/data-store.ts
================================================
import type { ComponentSummary, ComponentUpdateEvent, QueryParams, QueryResult } from './types'

function normalize(name: string): string {
  return name
    .replace(/[-_]/g, '')
    .toLowerCase()
}

export class VueScanDataStore {
  private events: ComponentUpdateEvent[] = []
  private maxEvents = 10000
  public isRecording = false

  addEvent(event: ComponentUpdateEvent) {
    if (!this.isRecording)
      return

    if (this.events.length >= this.maxEvents) {
      this.events.shift()
    }

    this.events.push(event)
  }

  query(params: QueryParams): QueryResult {
    let filtered = [...this.events]

    if (params.componentName) {
      const normalizedQuery = normalize(params.componentName)
      filtered = filtered.filter(e =>
        normalize(e.componentName).includes(normalizedQuery),
      )
    }

    if (params.timeRange && params.timeRange.start > 0 && params.timeRange.end > 0) {
      filtered = filtered.filter(e =>
        e.timestamp >= params.timeRange!.start
        && e.timestamp <= params.timeRange!.end,
      )
    }

    if (params.minUpdateCount) {
      filtered = filtered.filter(e =>
        e.updateCount >= params.minUpdateCount!,
      )
    }

    if (params.minRenderTime) {
      filtered = filtered.filter(e =>
        e.renderTime != null && e.renderTime >= params.minRenderTime!,
      )
    }

    if (params.onlyInViewport) {
      filtered = filtered.filter(e => e.isInViewport)
    }

    if (params.onlyUserComponents !== false) {
      filtered = filtered.filter(e => e.isUserComponent)
    }

    // Aggregate by component
    const componentMap = new Map<string, {
      sourceLocation?: string
      timestamps: number[]
      renderTimes: number[]
      fpsValues: number[]
    }>()

    for (const event of filtered) {
      const key = event.componentId
      const existing = componentMap.get(key)
      if (existing) {
        existing.timestamps.push(event.timestamp)
        if (event.renderTime != null)
          existing.renderTimes.push(event.renderTime)
        if (event.fps > 0)
          existing.fpsValues.push(event.fps)
      }
      else {
        componentMap.set(key, {
          sourceLocation: event.sourceLocation,
          timestamps: [event.timestamp],
          renderTimes: event.renderTime != null ? [event.renderTime] : [],
          fpsValues: event.fps > 0 ? [event.fps] : [],
        })
      }
    }

    let components: ComponentSummary[] = Array.from(componentMap.entries())
      .map(([componentId, data]) => {
        const totalUpdates = data.timestamps.length
        const firstUpdate = Math.min(...data.timestamps)
        const lastUpdate = Math.max(...data.timestamps)
        const durationSec = (lastUpdate - firstUpdate) / 1000
        const rawName = componentId.split('__')[0] || componentId
        const componentName = rawName === 'Anonymous Component' && data.sourceLocation
          ? `Anonymous (${data.sourceLocation})`
          : rawName
        const avgRenderTime = data.renderTimes.length > 0
          ? data.renderTimes.reduce((a, b) => a + b, 0) / data.renderTimes.length
          : undefined
        const avgFps = data.fpsValues.length > 0
          ? Math.round(data.fpsValues.reduce((a, b) => a + b, 0) / data.fpsValues.length)
          : undefined
        return {
          componentName,
          sourceLocation: data.sourceLocation,
          totalUpdates,
          updatesPerSecond: durationSec > 0 ? totalUpdates / durationSec : totalUpdates,
          avgRenderTime,
          avgFps,
          firstUpdate,
          lastUpdate,
        }
      })

    // Sort
    components.sort((a, b) => {
      switch (params.sortBy) {
        case 'totalUpdates': return b.totalUpdates - a.totalUpdates
        case 'updatesPerSecond': return b.updatesPerSecond - a.updatesPerSecond
        case 'renderTime': return (b.avgRenderTime ?? 0) - (a.avgRenderTime ?? 0)
        case 'componentName': return a.componentName.localeCompare(b.componentName)
        default: return b.totalUpdates - a.totalUpdates
      }
    })

    components = components.slice(0, params.limit)

    // Time range for all filtered events
    const timeRange = filtered.length > 0
      ? {
          start: filtered[0].timestamp,
          end: filtered[filtered.length - 1].timestamp,
        }
      : null

    return {
      components,
      events: params.includeRawEvents ? filtered.slice(0, params.limit) : undefined,
      metadata: {
        recordingStatus: this.isRecording ? 'active' : 'paused',
        totalEvents: filtered.length,
        uniqueComponents: componentMap.size,
        bufferSize: this.events.length,
        bufferCapacity: this.maxEvents,
        timeRange,
      },
    }
  }

  clear() {
    this.events = []
  }

  startRecording() {
    this.isRecording = true
  }

  stopRecording() {
    this.isRecording = false
  }

  exportAll() {
    return {
      events: this.events,
      exportedAt: Date.now(),
    }
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/src/index.ts
================================================
import type { DevpilotPlugin } from 'unplugin-devpilot'
import type { ComponentUpdateEvent, QueryParams } from './types'
import { defineMcpToolRegister, resolveClientModule, resolveSkillModule } from 'unplugin-devpilot'
import { z } from 'zod'
import { VueScanDataStore } from './data-store'

// Create data store instance
const dataStore = new VueScanDataStore()

// Define the DevPilot plugin
export const vueScanPlugin: DevpilotPlugin = {
  namespace: 'vue-scan',

  // Client-side module for auto-injection and data collection
  clientModule: resolveClientModule(import.meta.url, './client/index.mjs'),
  skillModule: resolveSkillModule(import.meta.url, './skill.md'),

  // Server-side RPC methods
  serverSetup: () => ({
    'vue-scan:recordUpdate': async (data: ComponentUpdateEvent) => {
      dataStore.addEvent(data)
    },

    'vue-scan:startRecording': async () => {
      dataStore.startRecording()
      return { status: 'active' }
    },

    'vue-scan:stopRecording': async () => {
      dataStore.stopRecording()
      return { status: 'paused' }
    },

    'vue-scan:clearData': async () => {
      dataStore.clear()
      return { status: 'cleared' }
    },

    'vue-scan:exportData': async () => {
      return dataStore.exportAll()
    },
  }),

  // MCP tools for LLM integration
  mcpSetup() {
    const tools = [
      defineMcpToolRegister(
        'queryVueScanData',
        {
          title: 'Query Vue Component Render Performance',
          description: 'Query Vue component render performance data. Returns per-component aggregated summaries (update count, frequency, source location) for performance analysis. Use includeRawEvents only when you need individual event details.',
          inputSchema: z.object({
            limit: z.number()
              .default(50)
              .describe('Maximum number of components to return'),

            componentName: z.string()
              .optional()
              .describe('Filter by component name (supports partial match)'),

            timeRange: z.object({
              start: z.number().describe('Start timestamp (ms)'),
              end: z.number().describe('End timestamp (ms)'),
            }).optional().describe('Filter by time range'),

            minUpdateCount: z.number()
              .optional()
              .describe('Only return components with update count >= this value'),

            minRenderTime: z.number()
              .optional()
              .describe('Only return events with render time >= this value (ms)'),

            onlyInViewport: z.boolean()
              .optional()
              .describe('Only return updates that occurred in viewport'),

            onlyUserComponents: z.boolean()
              .default(true)
              .describe('Only return user code components, filter out library components. Default true'),

            includeRawEvents: z.boolean()
              .default(false)
              .describe('Include raw update events in addition to component summaries. Usually not needed'),

            sortBy: z.enum(['totalUpdates', 'updatesPerSecond', 'renderTime', 'componentName'])
              .default('totalUpdates')
              .describe('Sort components by field'),
          }),
        },
        async (params) => {
          const data = dataStore.query(params as QueryParams)
          return {
            content: [{
              type: 'text' as const,
              text: JSON.stringify(data, null, 2),
            }],
          }
        },
      ),
    ]

    return tools
  },
}

export default vueScanPlugin
export * from './types'


================================================
FILE: packages/devpilot-plugin-vue-scan/src/skill.md
================================================
---
name: vue-scan
description: Vue component render performance monitoring. Captures mount/update events with render time, FPS, and source locations. Provides per-component aggregated summaries for analyzing render frequency, slow components, and frame drops.
allowed-tools: [
  "queryVueScanData"
]
---

# Vue Scan Skill

Monitor and analyze Vue component render performance via real-time update event tracking.

## ⚠️ CRITICAL: Recording Must Be Active

**Data is only collected while recording is active.** Before querying, ensure recording has been started via the control panel (Ctrl+Shift+R) or RPC. If `bufferSize` is 0, remind the user to start recording and reproduce their scenario.

## How It Works

- Client-side hooks are injected into Vue 2/3 component `mounted` and `updated` lifecycle
- Each render emits an event with: phase (mount/update), render time (ms), FPS, component name, source location, update count, viewport visibility
- Render time is measured between `beforeUpdate` and `updated` hooks via `performance.now()`
- FPS is tracked using a `requestAnimationFrame` sliding window (1s)
- Events are stored server-side in a circular buffer (max 10,000 events)
- `queryVueScanData` returns **per-component aggregated summaries** (not raw events) by default

## Workflow

1. **Ensure recording**: Ask user to start recording and interact with the page
2. **Query overview**: Call `queryVueScanData()` — returns components sorted by `totalUpdates`
3. **Drill down**: Filter by `componentName` or sort by `updatesPerSecond` to find hot spots
4. **Locate source**: Use `sourceLocation` (format: `file:line:col:tag`) to pinpoint code

## queryVueScanData Parameters

| Parameter | Default | Purpose |
|-----------|---------|---------|
| `limit` | 50 | Max components to return |
| `componentName` | - | Filter by name (partial match, case-insensitive, ignores `-` and `_`) |
| `timeRange` | - | `{ start, end }` timestamps in ms (0 values ignored) |
| `minUpdateCount` | - | Only components updated >= N times |
| `minRenderTime` | - | Only events with render time >= N ms |
| `onlyInViewport` | - | Exclude off-screen renders |
| `onlyUserComponents` | true | Filter out library components (no source location) |
| `includeRawEvents` | false | Include individual event details (usually not needed) |
| `sortBy` | totalUpdates | `totalUpdates` / `updatesPerSecond` / `renderTime` / `componentName` |

## Response Structure

- `components[]`: `{ componentName, sourceLocation, totalUpdates, updatesPerSecond, avgRenderTime, avgFps, firstUpdate, lastUpdate }`
- `metadata`: `{ recordingStatus, totalEvents, uniqueComponents, bufferSize, bufferCapacity, timeRange }`
- `events[]`: Only present when `includeRawEvents: true`

## Best Practices

- **Don't set timeRange to zeros**: Omit it entirely if not filtering by time
- **Sort by renderTime**: Find the slowest components causing frame drops
- **Sort by updatesPerSecond**: Reveals components re-rendering too rapidly
- **Check avgFps**: Low avgFps (< 30) indicates the component causes jank
- **Use sourceLocation**: Directly identifies the source file and line for fixes
- **Check onlyUserComponents=false**: If results seem sparse, library components may be the source

## Example: Find Performance Issues

```
1. queryVueScanData({ sortBy: "renderTime" })
   → Find slowest components by average render time
2. queryVueScanData({ sortBy: "totalUpdates" })
   → Review top re-rendering components with source locations
3. queryVueScanData({ minRenderTime: 16, sortBy: "renderTime" })
   → Find components exceeding one frame budget (16ms)
4. queryVueScanData({ componentName: "UserList", sortBy: "updatesPerSecond" })
   → Find the most frequently re-rendering component among partial matches
5. queryVueScanData({ onlyUserComponents: false, sortBy: "totalUpdates" })
   → Include library components to see full picture
```


================================================
FILE: packages/devpilot-plugin-vue-scan/src/types.ts
================================================
// Server-side types
export interface ComponentUpdateEvent {
  timestamp: number
  componentName: string
  componentId: string
  phase: 'mount' | 'update'
  renderTime?: number
  fps: number
  updateCount: number
  bounds: {
    width: number
    height: number
    top: number
    left: number
  }
  isInViewport: boolean
  isUserComponent: boolean
  sourceLocation?: string
  parentComponent?: string
}

export interface ComponentSummary {
  componentName: string
  sourceLocation?: string
  totalUpdates: number
  updatesPerSecond: number
  avgRenderTime?: number
  avgFps?: number
  firstUpdate: number
  lastUpdate: number
}

export interface QueryParams {
  limit: number
  componentName?: string
  timeRange?: { start: number, end: number }
  minUpdateCount?: number
  minRenderTime?: number
  onlyInViewport?: boolean
  onlyUserComponents?: boolean
  includeRawEvents?: boolean
  sortBy: 'totalUpdates' | 'updatesPerSecond' | 'renderTime' | 'componentName'
}

export interface QueryResult {
  components: ComponentSummary[]
  events?: ComponentUpdateEvent[]
  metadata: {
    recordingStatus: 'active' | 'paused'
    totalEvents: number
    uniqueComponents: number
    bufferSize: number
    bufferCapacity: number
    timeRange: { start: number, end: number } | null
  }
}


================================================
FILE: packages/devpilot-plugin-vue-scan/tsconfig.client.json
================================================
{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["es2023", "dom"],
    "moduleDetection": "force",
    "module": "preserve",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "types": [
      "unplugin-devpilot/virtual",
      "node"
    ],
    "strict": true,
    "noUnusedLocals": true,
    "declaration": true,
    "esModuleInterop": true,
    "isolatedDeclarations": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true
  },
  "include": ["src/client", "tests/client"]
}


================================================
FILE: packages/devpilot-plugin-vue-scan/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["es2023"],
    "moduleDetection": "force",
    "module": "preserve",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "types": ["node"],
    "strict": true,
    "noUnusedLocals": true,
    "declaration": true,
    "esModuleInterop": true,
    "isolatedDeclarations": false,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true
  },
  "include": ["src", "tests"],
  "exclude": ["src/client", "tests/client"]
}


================================================
FILE: packages/devpilot-plugin-vue-scan/tsdown.config.ts
================================================
import { copyFileSync, mkdirSync } from 'node:fs'
import { defineConfig } from 'tsdown'

export default defineConfig([
  {
    entry: [
      'src/*.ts',
    ],
    dts: true,
    inlineOnly: false,
  },
  {
    entry: [
      'src/client/index.ts',
    ],
    outDir: 'dist/client',
    inlineOnly: false,
    hooks: {
      'build:done': () => {
        mkdirSync('dist', { recursive: true })
        copyFileSync('src/skill.md', 'dist/skill.md')
      },
    },
  },
])


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

node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts

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


================================================
FILE: packages/extension/README.md
================================================
# WXT + Vue 3

This template should help get you started developing with Vue 3 in WXT.

## Recommended IDE Setup

- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).


================================================
FILE: packages/extension/entrypoints/background.ts
================================================
import { autoInject, blacklist } from '../utils/storage'

export default defineBackground(() => {
  console.log('Background script started', { id: browser.runtime.id })

  console.log({
    autoInject,
    blacklist,
  })
})


================================================
FILE: packages/extension/entrypoints/content.ts
================================================
import { autoInject, blacklist } from '../utils/storage'

export default defineContentScript({
  matches: ['<all_urls>'],
  async main() {
    const [isAutoInject, _blacklistPatterns] = await Promise.all([
      autoInject.getValue(),
      blacklist.getValue(),
    ])

    const blacklistPatterns = Object.values(_blacklistPatterns)

    // Check if current URL is blacklisted
    const currentUrl = window.location.href
    const isBlacklisted = blacklistPatterns.some((pattern) => {
      if (pattern === '<all_urls>')
        return true

      try {
        const regexPattern = pattern
          .replace(/\./g, '\\.')
          .replace(/\*/g, '.*')
          .replace(/\?/g, '.')
        const regex = new RegExp(`^${regexPattern}$`)
        return regex.test(currentUrl)
      }
      catch {
        return false
      }
    })

    if (isAutoInject && !isBlacklisted) {
      console.log('Auto inject enabled and not blacklisted, injecting script...')
      const script = document.createElement('script')
      script.src = browser.runtime.getURL('/auto.cjs')
      script.onload = function () {
        console.log('script injected.')
      }
      document.body.appendChild(script)
    }
    else {
      console.log('Script injection skipped:', {
        isAutoInject,
        isBlacklisted,
        currentUrl,
      })
    }
  },
})


================================================
FILE: packages/extension/entrypoints/popup/App.vue
================================================
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import { autoInject, blacklist } from '../../utils/storage'

const autoInjectEnabled = ref(false)
const blacklistPatterns = ref<string[]>([])
const newPattern = ref('')

onMounted(async () => {
  const [isAutoInject, _patterns] = await Promise.all([
    autoInject.getValue(),
    blacklist.getValue(),
  ])

  autoInjectEnabled.value = Boolean(isAutoInject)
  blacklistPatterns.value = Object.values(_patterns)
})

// Watch autoInject changes
watch(autoInjectEnabled, async (value) => {
  await autoInject.setValue(value)
})

// Watch blacklist changes
watch(blacklistPatterns, async (value) => {
  await blacklist.setValue(value)
}, { deep: true })

function isValidPattern(pattern: string) {
  try {
    if (pattern === '<all_urls>')
      return true
    return /^(?:\*|https?|file|ftp|urn):\/\/[^/]*\/.*$/.test(pattern)
  }
  catch {
    return false
  }
}

async function addPattern() {
  if (!newPattern.value || !isValidPattern(newPattern.value)) {
    // eslint-disable-next-line no-alert
    alert('Invalid pattern! Format should be like: *://*.example.com/*')
    return
  }

  if (!blacklistPatterns.value.includes(newPattern.value)) {
    blacklistPatterns.value = [...blacklistPatterns.value, newPattern.value]
    newPattern.value = ''
  }
}

async function removePattern(pattern: string) {
  blacklistPatterns.value = blacklistPatterns.value.filter(p => p !== pattern)
}
</script>

<template>
  <div class="min-w-[300px] p-4 flex flex-col gap-4">
    <label class="flex gap-1 items-center justify-between cursor-pointer">
      <span>
        Inject &nbsp;<a href="https://github.com/zcf0508/vue-scan">Vue Scan</a>.
      </span>
      <input
        v-model="autoInjectEnabled"
        class="cursor-pointer"
        type="checkbox"
      >
    </label>

    <div class="flex flex-col gap-2">
      <h3 class="font-medium">
        Blacklist patterns:
      </h3>
      <div class="flex gap-2">
        <input
          v-model="newPattern"
          placeholder="e.g. *://*.example.com/*"
          class="flex-1 px-2 py-1 border rounded"
          @keyup.enter="addPattern"
        >
        <button
          class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 outline-none border-none cursor-pointer"
          @click="addPattern"
        >
          Add
        </button>
      </div>

      <ul class="flex flex-col gap-1 pl-0">
        <li
          v-for="pattern in blacklistPatterns"
          :key="pattern"
          class="flex justify-between items-center gap-2 px-2 py-1 bg-gray-100 rounded"
        >
          <span class="truncate">{{ pattern }}</span>
          <button
            class="text-red-500 hover:text-red-600 cursor-pointer"
            @click="removePattern(pattern)"
          >
            ✕
          </button>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
input[type="checkbox"] {
  accent-color: #2563eb;
}
</style>


================================================
FILE: packages/extension/entrypoints/popup/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Default Popup Title</title>
    <meta name="manifest.type" content="browser_action" />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./main.ts"></script>
  </body>
</html>


================================================
FILE: packages/extension/entrypoints/popup/main.ts
================================================
import { createApp } from 'vue'
import App from './App.vue'
import 'uno.css'

createApp(App).mount('#app')


================================================
FILE: packages/extension/package.json
================================================
{
  "name": "vue-scan-ext",
  "type": "module",
  "version": "0.0.37",
  "private": true,
  "description": "manifest.json description",
  "scripts": {
    "dev": "wxt",
    "dev:firefox": "wxt -b firefox",
    "build": "wxt build",
    "build:firefox": "wxt build -b firefox",
    "zip": "wxt zip",
    "zip:firefox": "wxt zip -b firefox",
    "compile": "vue-tsc --noEmit",
    "postinstall": "wxt prepare"
  },
  "dependencies": {
    "vue": "^3.5.12"
  },
  "devDependencies": {
    "@types/chrome": "^0.0.280",
    "@wxt-dev/module-vue": "^1.0.1",
    "typescript": "5.6.3",
    "unocss": "^0.65.1",
    "vue-tsc": "^2.1.10",
    "wxt": "^0.19.13"
  }
}


================================================
FILE: packages/extension/tsconfig.json
================================================
{
  "extends": "./.wxt/tsconfig.json"
}


================================================
FILE: packages/extension/uno.config.ts
================================================
import { defineConfig, presetIcons, presetWind } from 'unocss'

export default defineConfig({
  content: { filesystem: ['entrypoints/popup/*/*.{ts,tsx,vue}'] },
  theme: {
    colors: {
      primary: '#0088ff',
      success: '#00cca3',
      warn: '#ff7f0f',
      error: '#f54327',
    },
    boxShadow: {
      default: '0 2px 16px rgba(0, 0, 0, 0.09)',
    },
  },
  presets: [
    presetWind(),
    presetIcons({
      scale: 1,
      prefix: 'i-',
      extraProperties: {
        'display': 'inline-block',
        'min-width': '1em',
      },
    }),
  ],
})


================================================
FILE: packages/extension/utils/storage.ts
================================================
import { storage } from 'wxt/storage'

export const autoInject = storage.defineItem<boolean>('local:autoInject', {
  fallback: false,
})

export const blacklist = storage.defineItem<string[]>('local:blacklist', {
  fallback: [],
})


================================================
FILE: packages/extension/wxt.config.ts
================================================
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'wxt'

// See https://wxt.dev/api/config.html
export default defineConfig({
  extensionApi: 'chrome',
  modules: ['@wxt-dev/module-vue'],
  manifest: {
    permissions: ['storage'],
    web_accessible_resources: [{
      resources: ['auto.cjs'],
      matches: ['<all_urls>'],
    }],
  },
  vite: () => {
    return {
      plugins: [
        UnoCSS(),
      ],
    }
  },
})


================================================
FILE: packages/nuxt/.gitignore
================================================
# Dependencies
node_modules

# Logs
*.log*

# Temp directories
.temp
.tmp
.cache

# Yarn
**/.yarn/cache
**/.yarn/*state*

# Generated dirs
dist

# Nuxt
.nuxt
.output
.data
.vercel_build_output
.build-*
.netlify

# Env
.env

# Testing
reports
coverage
*.lcov
.nyc_output

# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Intellij idea
*.iml
.idea

# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk


================================================
FILE: packages/nuxt/Readme.md
================================================
Reference [vue-scan](https://github.com/zcf0508/vue-scan) .


================================================
FILE: packages/nuxt/package.json
================================================
{
  "name": "z-vue-scan-nuxt-module",
  "type": "module",
  "version": "0.0.37",
  "description": "z-vue-scan Nuxt module",
  "license": "MIT",
  "repository": "https://github.com/zcf0508/vue-scan",
  "exports": {
    ".": {
      "types": "./dist/types.d.ts",
      "import": "./dist/module.mjs",
      "require": "./dist/module.cjs"
    }
  },
  "main": "./dist/module.cjs",
  "types": "./dist/types.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "prepack": "nuxt-module-build build",
    "dev": "nuxi dev playground",
    "dev:build": "nuxi build playground",
    "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
    "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
    "lint": "eslint .",
    "test": "vitest run",
    "test:watch": "vitest watch",
    "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
  },
  "dependencies": {
    "@nuxt/kit": "^3.15.2",
    "z-vue-scan": "workspace:*"
  },
  "devDependencies": {
    "@nuxt/devtools": "^1.7.0",
    "@nuxt/eslint-config": "^0.7.5",
    "@nuxt/module-builder": "^0.8.4",
    "@nuxt/schema": "^3.15.2",
    "@nuxt/test-utils": "^3.15.4",
    "@types/node": "latest",
    "changelogen": "^0.5.7",
    "eslint": "^9.18.0",
    "nuxt": "^3.15.2",
    "typescript": "~5.6.3",
    "vitest": "^3.0.2",
    "vue-tsc": "^2.2.0"
  }
}


================================================
FILE: packages/nuxt/playground/app.vue
================================================
<script setup lang="ts">
import { ref } from 'vue'

const b = ref('')
</script>

<template>
  <div>
    <div>
      {{ b }}
    </div>
    <HelloWorld v-model:msg="b" />
  </div>
</template>


================================================
FILE: packages/nuxt/playground/components/HelloWorld.vue
================================================
<script setup lang="ts">
const msg = defineModel('msg', {
  default: '',
})
</script>

<template>
  <div>
    <div>
      {{ msg }}
    </div>
    input: <input v-model="msg">
  </div>
</template>


================================================
FILE: packages/nuxt/playground/nuxt.config.ts
================================================
export default defineNuxtConfig({
  modules: ['../src/module'],
  vueScan: {
    enable: true,
  },
  devtools: { enabled: true },
  compatibilityDate: '2025-01-22',
})


================================================
FILE: packages/nuxt/playground/package.json
================================================
{
  "name": "z-vue-scan-nuxt-module-playground",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "nuxi dev",
    "build": "nuxi build",
    "generate": "nuxi generate"
  },
  "dependencies": {
    "nuxt": "^3.15.2"
  }
}


================================================
FILE: packages/nuxt/playground/server/tsconfig.json
================================================
{
  "extends": "../.nuxt/tsconfig.server.json"
}


================================================
FILE: packages/nuxt/playground/tsconfig.json
================================================
{
  "extends": "./.nuxt/tsconfig.json"
}


================================================
FILE: packages/nuxt/src/module.ts
================================================
import type { VueScanBaseOptions } from 'z-vue-scan'
import process from 'node:process'
import { addPluginTemplate, defineNuxtModule } from '@nuxt/kit'

// Module options TypeScript interface definition
export interface ModuleOptions extends VueScanBaseOptions {}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: 'z-vue-scan-nuxt-module',
    configKey: 'vueScan',
  },
  // Default configuration options of the Nuxt module
  defaults: {
    enable: process.env.NODE_ENV === 'development',
  },
  setup(_options, _nuxt) {
    addPluginTemplate({
      filename: 'vue-scan.plugin.mjs',
      getContents: () => `
import { defineNuxtPlugin } from '#app'
import VueScan from 'z-vue-scan'

export default defineNuxtPlugin((nuxtApp) => {
  const options = ${JSON.stringify(_options)}
  
  nuxtApp.vueApp.use(VueScan, options)
})
      `,
      mode: 'client',
    })
  },
})


================================================
FILE: packages/nuxt/src/runtime/plugin.ts
================================================
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((_nuxtApp) => {
  console.log('Plugin injected by z-vue-scan-nuxt-module!')
})


================================================
FILE: packages/nuxt/src/runtime/server/tsconfig.json
================================================
{
  "extends": "../../../.nuxt/tsconfig.server.json"
}


================================================
FILE: packages/nuxt/test/basic.test.ts
================================================
import { fileURLToPath } from 'node:url'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
import { describe, expect, it } from 'vitest'

describe('ssr', async () => {
  await setup({
    rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
  })

  it('renders the index page', async () => {
    // Get response to a server-rendered page with `$fetch`.
    const html = await $fetch('/')
    expect(html).toContain('<div>basic</div>')
  })
})


================================================
FILE: packages/nuxt/test/fixtures/basic/app.vue
================================================
<script setup>
</script>

<template>
  <div>basic</div>
</template>


================================================
FILE: packages/nuxt/test/fixtures/basic/nuxt.config.ts
================================================
import MyModule from '../../../src/module'

export default defineNuxtConfig({
  modules: [
    MyModule,
  ],
})


================================================
FILE: packages/nuxt/test/fixtures/basic/package.json
================================================
{
  "name": "basic",
  "type": "module",
  "private": true
}


================================================
FILE: packages/nuxt/tsconfig.json
================================================
{
  "extends": "./.nuxt/tsconfig.json",
  "exclude": [
    "dist",
    "node_modules",
    "playground"
  ]
}


================================================
FILE: patches/@vue__devtools-kit.patch
================================================
diff --git a/dist/index.cjs b/dist/index.cjs
index 0004140e1a340e020f3227a4c0eb63ae04ee7a4c..70ad9a77e82fc0605f21064c7bac0e1513de9984 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -2179,6 +2179,24 @@ function update(options) {
     indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;
   }
 }
+function updateHighlight(options) {
+  const containerEl = options?.elementId ? document.getElementById(options.elementId) :getContainerElement();
+  if (containerEl) {
+    const cardEl = containerEl.querySelector('#__vue-devtools-component-inspector__card__');
+    const nameEl = containerEl.querySelector('#__vue-devtools-component-inspector__name__');
+    const indicatorEl = containerEl.querySelector('#__vue-devtools-component-inspector__indicator__');
+    Object.assign(containerEl.style, {
+      ...containerStyles,
+      ...getStyles(options.bounds),
+      ...options.style
+    });
+    Object.assign(cardEl.style, {
+      top: options.bounds.top < 35 ? 0 : "-35px"
+    });
+    nameEl.innerHTML = `&lt;${options.name}&gt;&nbsp;&nbsp;`;
+    indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;
+  }
+}
 function highlight(instance) {
   const bounds = getComponentBoundingRect(instance);
   if (!bounds.width && !bounds.height)
@@ -6658,6 +6676,9 @@ var devtools = {
     return devtoolsContext.api;
   }
 };
+
+const createHighlight = create;
+
 // Annotate the CommonJS export names for ESM import in node:
 0 && (module.exports = {
   DevToolsContextHookKeys,
@@ -6740,5 +6761,9 @@ var devtools = {
   toggleHighPerfMode,
   updateDevToolsClientDetected,
   updateDevToolsState,
-  updateTimelineLayersState
+  updateTimelineLayersState,
+  getComponentBoundingRect,
+  getInstanceName,
+  createHighlight,
+  updateHighlight
 });
diff --git a/dist/index.d.cts b/dist/index.d.cts
index d7e6b9b8c83d3178da6b41a054bb0da69e7e77cf..a5f91ba0a0f55cd9feb0bdc6cabf12d0bb2e30dd 100644
--- a/dist/index.d.cts
+++ b/dist/index.d.cts
@@ -1099,4 +1099,9 @@ declare const devtools: {
     };
 };
 
-export { type AddInspectorApiPayload, type App, type AppRecord, type ComponentBoundingRect, type ComponentBoundingRectApiPayload, type ComponentBounds, type ComponentHighLighterOptions, type ComponentInspector, type ComponentInstance, type ComponentState, type ComponentTreeNode, type CreateRpcClientOptions, type CreateRpcServerOptions, type CustomCommand, type CustomCommandAction, type CustomInspectorNode, type CustomInspectorOptions, type CustomInspectorState, type CustomTab, type DevToolsApiType, type DevToolsAppRecords, DevToolsContextHookKeys, type DevToolsContextHookPayloads, type DevToolsContextHooks, type DevToolsEvent, type DevToolsHook, DevToolsHooks, DevToolsMessagingHookKeys, type DevToolsMessagingHookPayloads, type DevToolsMessagingHooks, type DevToolsPlugin, type DevToolsState, DevToolsV6PluginAPIHookKeys, type DevToolsV6PluginAPIHookPayloads, type DevToolsV6PluginAPIHooks, type DevtoolsContext, type EditStatePayload, INFINITY, type InspectedComponentData, type InspectorCustomState, type InspectorNodeTag, type InspectorState, type InspectorStateApiPayload, type InspectorStateEditorPayload, type InspectorTree, type InspectorTreeApiPayload, type ModuleIframeView, type ModuleVNodeView, type ModuleView, NAN, NEGATIVE_INFINITY, type OpenInEditorOptions, type PluginDescriptor, type PluginSetupFunction, type Presets, type PropPath, ROUTER_INFO_KEY, ROUTER_KEY, type RouterInfo, type ScreenshotData, type ScreenshotOverlayEvent, type ScreenshotOverlayRenderContext, type ScreenshotOverlayRenderResult, type ScrollToComponentOptions, type StateBase, type TimelineEvent, type TimelineEventOptions, type TimelineLayerOptions, UNDEFINED, type VueAppInstance, type VueHooks, activeAppRecord, addCustomCommand, addCustomTab, addDevToolsAppRecord, addDevToolsPluginToBuffer, addInspector, callConnectedUpdatedHook, callDevToolsPluginSetupFn, callInspectorUpdatedHook, callStateUpdatedHook, cancelInspectComponentHighLighter, createComponentsDevToolsPlugin, createDevToolsApi, createDevToolsCtxHooks, createRpcClient, createRpcProxy, createRpcServer, type customTypeEnums, devtools, devtoolsAppRecords, devtoolsContext, devtoolsInspector, devtoolsPluginBuffer, devtoolsRouter, devtoolsRouterInfo, devtoolsState, formatInspectorStateValue, getActiveInspectors, getComponentInspector, getDevToolsEnv, getExtensionClientContext, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getInspectorStateValueType, getRaw, getRpcClient, getRpcServer, getViteRpcClient, getViteRpcServer, highlight, initDevTools, inspectComponentHighLighter, isPlainObject, onDevToolsClientConnected, onDevToolsConnected, openInEditor, parse, registerDevToolsPlugin, removeCustomCommand, removeDevToolsAppRecord, removeRegisteredPluginApp, resetDevToolsState, scrollToComponent, setActiveAppRecord, setActiveAppRecordId, setDevToolsEnv, setElectronClientContext, setElectronProxyContext, setElectronServerContext, setExtensionClientContext, setIframeServerContext, setOpenInEditorBaseUrl, setRpcServerToGlobal, setViteClientContext, setViteRpcClientToGlobal, setViteRpcServerToGlobal, setViteServerContext, setupDevToolsPlugin, stringify, toEdit, toSubmit, toggleClientConnected, toggleComponentHighLighter, toggleComponentInspectorEnabled, toggleHighPerfMode, unhighlight, updateDevToolsClientDetected, updateDevToolsState, updateTimelineLayersState };
+declare function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect
+declare function getInstanceName(instance: VueAppInstance): string
+declare function createHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): HTMLDivElement
+declare function updateHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): void
+
+export { type AddInspectorApiPayload, type App, type AppRecord, type ComponentBoundingRect, type ComponentBoundingRectApiPayload, type ComponentBounds, type ComponentHighLighterOptions, type ComponentInspector, type ComponentInstance, type ComponentState, type ComponentTreeNode, type CreateRpcClientOptions, type CreateRpcServerOptions, type CustomCommand, type CustomCommandAction, type CustomInspectorNode, type CustomInspectorOptions, type CustomInspectorState, type CustomTab, type DevToolsApiType, type DevToolsAppRecords, DevToolsContextHookKeys, type DevToolsContextHookPayloads, type DevToolsContextHooks, type DevToolsEvent, type DevToolsHook, DevToolsHooks, DevToolsMessagingHookKeys, type DevToolsMessagingHookPayloads, type DevToolsMessagingHooks, type DevToolsPlugin, type DevToolsState, DevToolsV6PluginAPIHookKeys, type DevToolsV6PluginAPIHookPayloads, type DevToolsV6PluginAPIHooks, type DevtoolsContext, type EditStatePayload, INFINITY, type InspectedComponentData, type InspectorCustomState, type InspectorNodeTag, type InspectorState, type InspectorStateApiPayload, type InspectorStateEditorPayload, type InspectorTree, type InspectorTreeApiPayload, type ModuleIframeView, type ModuleVNodeView, type ModuleView, NAN, NEGATIVE_INFINITY, type OpenInEditorOptions, type PluginDescriptor, type PluginSetupFunction, type Presets, type PropPath, ROUTER_INFO_KEY, ROUTER_KEY, type RouterInfo, type ScreenshotData, type ScreenshotOverlayEvent, type ScreenshotOverlayRenderContext, type ScreenshotOverlayRenderResult, type ScrollToComponentOptions, type StateBase, type TimelineEvent, type TimelineEventOptions, type TimelineLayerOptions, UNDEFINED, type VueAppInstance, type VueHooks, activeAppRecord, addCustomCommand, addCustomTab, addDevToolsAppRecord, addDevToolsPluginToBuffer, addInspector, callConnectedUpdatedHook, callDevToolsPluginSetupFn, callInspectorUpdatedHook, callStateUpdatedHook, cancelInspectComponentHighLighter, createComponentsDevToolsPlugin, createDevToolsApi, createDevToolsCtxHooks, createRpcClient, createRpcProxy, createRpcServer, type customTypeEnums, devtools, devtoolsAppRecords, devtoolsContext, devtoolsInspector, devtoolsPluginBuffer, devtoolsRouter, devtoolsRouterInfo, devtoolsState, formatInspectorStateValue, getActiveInspectors, getComponentInspector, getDevToolsEnv, getExtensionClientContext, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getInspectorStateValueType, getRaw, getRpcClient, getRpcServer, getViteRpcClient, getViteRpcServer, highlight, initDevTools, inspectComponentHighLighter, isPlainObject, onDevToolsClientConnected, onDevToolsConnected, openInEditor, parse, registerDevToolsPlugin, removeCustomCommand, removeDevToolsAppRecord, removeRegisteredPluginApp, resetDevToolsState, scrollToComponent, setActiveAppRecord, setActiveAppRecordId, setDevToolsEnv, setElectronClientContext, setElectronProxyContext, setElectronServerContext, setExtensionClientContext, setIframeServerContext, setOpenInEditorBaseUrl, setRpcServerToGlobal, setViteClientContext, setViteRpcClientToGlobal, setViteRpcServerToGlobal, setViteServerContext, setupDevToolsPlugin, stringify, toEdit, toSubmit, toggleClientConnected, toggleComponentHighLighter, toggleComponentInspectorEnabled, toggleHighPerfMode, unhighlight, updateDevToolsClientDetected, updateDevToolsState, updateTimelineLayersState, getComponentBoundingRect, getInstanceName, createHighlight, updateHighlight };
diff --git a/dist/index.d.ts b/dist/index.d.ts
index d7e6b9b8c83d3178da6b41a054bb0da69e7e77cf..a5f91ba0a0f55cd9feb0bdc6cabf12d0bb2e30dd 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1099,4 +1099,9 @@ declare const devtools: {
     };
 };
 
-export { type AddInspectorApiPayload, type App, type AppRecord, type ComponentBoundingRect, type ComponentBoundingRectApiPayload, type ComponentBounds, type ComponentHighLighterOptions, type ComponentInspector, type ComponentInstance, type ComponentState, type ComponentTreeNode, type CreateRpcClientOptions, type CreateRpcServerOptions, type CustomCommand, type CustomCommandAction, type CustomInspectorNode, type CustomInspectorOptions, type CustomInspectorState, type CustomTab, type DevToolsApiType, type DevToolsAppRecords, DevToolsContextHookKeys, type DevToolsContextHookPayloads, type DevToolsContextHooks, type DevToolsEvent, type DevToolsHook, DevToolsHooks, DevToolsMessagingHookKeys, type DevToolsMessagingHookPayloads, type DevToolsMessagingHooks, type DevToolsPlugin, type DevToolsState, DevToolsV6PluginAPIHookKeys, type DevToolsV6PluginAPIHookPayloads, type DevToolsV6PluginAPIHooks, type DevtoolsContext, type EditStatePayload, INFINITY, type InspectedComponentData, type InspectorCustomState, type InspectorNodeTag, type InspectorState, type InspectorStateApiPayload, type InspectorStateEditorPayload, type InspectorTree, type InspectorTreeApiPayload, type ModuleIframeView, type ModuleVNodeView, type ModuleView, NAN, NEGATIVE_INFINITY, type OpenInEditorOptions, type PluginDescriptor, type PluginSetupFunction, type Presets, type PropPath, ROUTER_INFO_KEY, ROUTER_KEY, type RouterInfo, type ScreenshotData, type ScreenshotOverlayEvent, type ScreenshotOverlayRenderContext, type ScreenshotOverlayRenderResult, type ScrollToComponentOptions, type StateBase, type TimelineEvent, type TimelineEventOptions, type TimelineLayerOptions, UNDEFINED, type VueAppInstance, type VueHooks, activeAppRecord, addCustomCommand, addCustomTab, addDevToolsAppRecord, addDevToolsPluginToBuffer, addInspector, callConnectedUpdatedHook, callDevToolsPluginSetupFn, callInspectorUpdatedHook, callStateUpdatedHook, cancelInspectComponentHighLighter, createComponentsDevToolsPlugin, createDevToolsApi, createDevToolsCtxHooks, createRpcClient, createRpcProxy, createRpcServer, type customTypeEnums, devtools, devtoolsAppRecords, devtoolsContext, devtoolsInspector, devtoolsPluginBuffer, devtoolsRouter, devtoolsRouterInfo, devtoolsState, formatInspectorStateValue, getActiveInspectors, getComponentInspector, getDevToolsEnv, getExtensionClientContext, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getInspectorStateValueType, getRaw, getRpcClient, getRpcServer, getViteRpcClient, getViteRpcServer, highlight, initDevTools, inspectComponentHighLighter, isPlainObject, onDevToolsClientConnected, onDevToolsConnected, openInEditor, parse, registerDevToolsPlugin, removeCustomCommand, removeDevToolsAppRecord, removeRegisteredPluginApp, resetDevToolsState, scrollToComponent, setActiveAppRecord, setActiveAppRecordId, setDevToolsEnv, setElectronClientContext, setElectronProxyContext, setElectronServerContext, setExtensionClientContext, setIframeServerContext, setOpenInEditorBaseUrl, setRpcServerToGlobal, setViteClientContext, setViteRpcClientToGlobal, setViteRpcServerToGlobal, setViteServerContext, setupDevToolsPlugin, stringify, toEdit, toSubmit, toggleClientConnected, toggleComponentHighLighter, toggleComponentInspectorEnabled, toggleHighPerfMode, unhighlight, updateDevToolsClientDetected, updateDevToolsState, updateTimelineLayersState };
+declare function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect
+declare function getInstanceName(instance: VueAppInstance): string
+declare function createHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): HTMLDivElement
+declare function updateHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): void
+
+export { type AddInspectorApiPayload, type App, type AppRecord, type ComponentBoundingRect, type ComponentBoundingRectApiPayload, type ComponentBounds, type ComponentHighLighterOptions, type ComponentInspector, type ComponentInstance, type ComponentState, type ComponentTreeNode, type CreateRpcClientOptions, type CreateRpcServerOptions, type CustomCommand, type CustomCommandAction, type CustomInspectorNode, type CustomInspectorOptions, type CustomInspectorState, type CustomTab, type DevToolsApiType, type DevToolsAppRecords, DevToolsContextHookKeys, type DevToolsContextHookPayloads, type DevToolsContextHooks, type DevToolsEvent, type DevToolsHook, DevToolsHooks, DevToolsMessagingHookKeys, type DevToolsMessagingHookPayloads, type DevToolsMessagingHooks, type DevToolsPlugin, type DevToolsState, DevToolsV6PluginAPIHookKeys, type DevToolsV6PluginAPIHookPayloads, type DevToolsV6PluginAPIHooks, type DevtoolsContext, type EditStatePayload, INFINITY, type InspectedComponentData, type InspectorCustomState, type InspectorNodeTag, type InspectorState, type InspectorStateApiPayload, type InspectorStateEditorPayload, type InspectorTree, type InspectorTreeApiPayload, type ModuleIframeView, type ModuleVNodeView, type ModuleView, NAN, NEGATIVE_INFINITY, type OpenInEditorOptions, type PluginDescriptor, type PluginSetupFunction, type Presets, type PropPath, ROUTER_INFO_KEY, ROUTER_KEY, type RouterInfo, type ScreenshotData, type ScreenshotOverlayEvent, type ScreenshotOverlayRenderContext, type ScreenshotOverlayRenderResult, type ScrollToComponentOptions, type StateBase, type TimelineEvent, type TimelineEventOptions, type TimelineLayerOptions, UNDEFINED, type VueAppInstance, type VueHooks, activeAppRecord, addCustomCommand, addCustomTab, addDevToolsAppRecord, addDevToolsPluginToBuffer, addInspector, callConnectedUpdatedHook, callDevToolsPluginSetupFn, callInspectorUpdatedHook, callStateUpdatedHook, cancelInspectComponentHighLighter, createComponentsDevToolsPlugin, createDevToolsApi, createDevToolsCtxHooks, createRpcClient, createRpcProxy, createRpcServer, type customTypeEnums, devtools, devtoolsAppRecords, devtoolsContext, devtoolsInspector, devtoolsPluginBuffer, devtoolsRouter, devtoolsRouterInfo, devtoolsState, formatInspectorStateValue, getActiveInspectors, getComponentInspector, getDevToolsEnv, getExtensionClientContext, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getInspectorStateValueType, getRaw, getRpcClient, getRpcServer, getViteRpcClient, getViteRpcServer, highlight, initDevTools, inspectComponentHighLighter, isPlainObject, onDevToolsClientConnected, onDevToolsConnected, openInEditor, parse, registerDevToolsPlugin, removeCustomCommand, removeDevToolsAppRecord, removeRegisteredPluginApp, resetDevToolsState, scrollToComponent, setActiveAppRecord, setActiveAppRecordId, setDevToolsEnv, setElectronClientContext, setElectronProxyContext, setElectronServerContext, setExtensionClientContext, setIframeServerContext, setOpenInEditorBaseUrl, setRpcServerToGlobal, setViteClientContext, setViteRpcClientToGlobal, setViteRpcServerToGlobal, setViteServerContext, setupDevToolsPlugin, stringify, toEdit, toSubmit, toggleClientConnected, toggleComponentHighLighter, toggleComponentInspectorEnabled, toggleHighPerfMode, unhighlight, updateDevToolsClientDetected, updateDevToolsState, updateTimelineLayersState, getComponentBoundingRect, getInstanceName, createHighlight, updateHighlight };
diff --git a/dist/index.js b/dist/index.js
index 530ba2998ca4220345085e1eb09872ddda9d8735..eb5240d07f2cc452461e30de811ed5dbf63fc7c6 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -2088,6 +2088,26 @@ function update(options) {
     indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;
   }
 }
+
+function updateHighlight(options) {
+  const containerEl = options?.elementId ? document.getElementById(options.elementId) :getContainerElement();
+  if (containerEl) {
+    const cardEl = containerEl.querySelector('#__vue-devtools-component-inspector__card__');
+    const nameEl = containerEl.querySelector('#__vue-devtools-component-inspector__name__');
+    const indicatorEl = containerEl.querySelector('#__vue-devtools-component-inspector__indicator__');
+    Object.assign(containerEl.style, {
+      ...containerStyles,
+      ...getStyles(options.bounds),
+      ...options.style
+    });
+    Object.assign(cardEl.style, {
+      top: options.bounds.top < 35 ? 0 : "-35px"
+    });
+    nameEl.innerHTML = `&lt;${options.name}&gt;&nbsp;&nbsp;`;
+    indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;
+  }
+}
+
 function highlight(instance) {
   const bounds = getComponentBoundingRect(instance);
   if (!bounds.width && !bounds.height)
@@ -6648,5 +6668,9 @@ export {
   toggleHighPerfMode,
   updateDevToolsClientDetected,
   updateDevToolsState,
-  updateTimelineLayersState
+  updateTimelineLayersState,
+  getComponentBoundingRect,
+  getInstanceName,
+  create as createHighlight,
+  updateHighlight
 };


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - examples/*
  - packages/*


================================================
FILE: src/auto.ts
================================================
import type { VNodeNormalizedChildren } from 'vue-demi'
import { throttle } from 'lodash-es'
import { type BACE_VUE_INSTANCE, createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/hook'
import plugin from './index'
import { createDomMutationObserver } from './utils/MutationObserverDom'

(() => {
  // eslint-disable-next-line node/prefer-global/process
  if (!window.process) {
    // @ts-expect-error browser mock
    // eslint-disable-next-line node/prefer-global/process
    window.process = {
      env: {
        NODE_ENV: 'development',
      },
    }
  }

  if (!window.__VUE_SCAN__) {
    window.__VUE_SCAN__ = {
      plugin,
      createOnBeforeUpdateHook,
      createOnBeforeUnmountHook,
      createOnMountedHook,
      createOnUpdatedHook,
    }
  }
})()

// Check if the __vue_app__ property exists on the #app node of the page

function injectVueScan(node: HTMLElement) {
  // @ts-expect-error vue internal
  if ((node.__vue_app__ || node.__vue__)) {
    // @ts-expect-error vue internal
    if (node.__vue_app__) { // VUE 3
      // @ts-expect-error vue internal
      const vueInstance = node.__vue_app__._container._vnode.component as BACE_VUE_INSTANCE

      // @ts-expect-error vue internal
      node.__vue_app__.use(window.__VUE_SCAN__.plugin)

      const first = !vueInstance?.__vue_scan_injected__

      if (!first) {
        console.log(vueInstance)
      }

      function mixinChildren(children: VNodeNormalizedChildren) {
        if (!children) {
          return
        }

        if (typeof children === 'string') {
          return
        }

        if (!Array.isArray(children)) {
          return
        }

        children.forEach((item) => {
          if (typeof item !== 'object') {
            return
          }

          if (item && 'component' in item && item.component) {
            mixin(item.component as BACE_VUE_INSTANCE)
          }
          else if (item && 'children' in item) {
            mixinChildren(item.children)
          }
        })
      }

      function mixin(vueInstance: BACE_VUE_INSTANCE) {
        if (vueInstance.subTree?.el && vueInstance?.__vue_scan_injected__ !== true) {
          const onBeforeUpdate = createOnBeforeUpdateHook(vueInstance)
          const onUpdated = createOnUpdatedHook(vueInstance)
          const onBeforeUnmount = createOnBeforeUnmountHook(vueInstance)

          if (onBeforeUpdate) {
            if (vueInstance?.bu) {
              vueInstance.bu.push(onBeforeUpdate)
            }
            else {
              vueInstance!.bu = [onBeforeUpdate]
            }
          }

          if (onUpdated) {
            if (vueInstance?.u) {
              vueInstance.u.push(onUpdated)
            }
            else {
              vueInstance!.u = [onUpdated]
            }
          }

          if (onBeforeUnmount) {
            if (vueInstance?.bum) {
              vueInstance.bum.push(onBeforeUnmount)
            }
            else {
              vueInstance!.bum = [onBeforeUnmount]
            }
          }

          vueInstance.__vue_scan_injected__ = true
        }

        if (!vueInstance?.subTree?.component && vueInstance?.subTree?.children) {
          mixinChildren(vueInstance.subTree.children)
        }
        else if (vueInstance?.subTree?.component) {
          mixin(vueInstance.subTree.component as BACE_VUE_INSTANCE)
        }

        else if (!vueInstance?.subTree && vueInstance?.children) {
          mixinChildren(vueInstance.children)
        }
      }

      mixin(vueInstance)

      if (!first) {
        console.log('vue scan inject success')
      }

      vueInstance.__vue_scan_injected__ = true
    }
    // @ts-expect-error vue internal
    else if (node.__vue__) { // VUE 2
      // @ts-expect-error vue internal
      const vueInstance = (node.__vue__?.$vnode?.componentInstance || node.__vue__) as BACE_VUE_INSTANCE

      const first = !vueInstance?.__vue_scan_injected__

      if (first) {
        console.log(vueInstance)
      }

      function mixin(vueInstance: BACE_VUE_INSTANCE) {
        if (vueInstance?.$el && vueInstance?.__vue_scan_injected__ !== true) {
          const onBeforeUpdate = createOnBeforeUpdateHook(vueInstance)
          const onUpdated = createOnUpdatedHook(vueInstance)
          const onBeforeUnmount = createOnBeforeUnmountHook(vueInstance)

          if (onBeforeUpdate) {
            if (vueInstance?.$options?.beforeUpdate) {
              const newBeforeUpdate = [...vueInstance.$options.beforeUpdate]
              newBeforeUpdate.push(onBeforeUpdate)
              vueInstance.$set(vueInstance.$options, 'beforeUpdate', newBeforeUpdate)
            }
            else if (vueInstance?.$options) {
              vueInstance.$set(vueInstance.$options, 'beforeUpdate', [onBeforeUpdate])
            }
          }

          if (onUpdated) {
            if (vueInstance?.$options?.updated) {
              const newUpdated = [...vueInstance.$options.updated]
              newUpdated.push(onUpdated)
              vueInstance.$set(vueInstance.$options, 'updated', newUpdated)
            }
            else if (vueInstance?.$options) {
              vueInstance.$set(vueInstance.$options, 'updated', [onUpdated])
            }
          }

          if (onBeforeUnmount) {
            if (vueInstance?.$options?.beforeDestroy) {
              const newBeforeDestroy = [...vueInstance.$options.beforeDestroy]
              newBeforeDestroy.push(onBeforeUnmount)
              vueInstance.$set(vueInstance.$options, 'beforeDestroy', newBeforeDestroy)
            }
            else if (vueInstance?.$options) {
              vueInstance.$set(vueInstance.$options, 'beforeDestroy', [onBeforeUnmount])
            }
          }

          vueInstance.__vue_scan_injected__ = true
        }

        if (vueInstance?.$children) {
          (vueInstance?.$children as Array<BACE_VUE_INSTANCE>).forEach((child) => {
            mixin(child)
          })
        }
      }

      mixin(vueInstance)

      if (first) {
        console.log('vue scan inject success')
      }

      vueInstance.__vue_scan_injected__ = true
    }
  }
}

function getMountDoms() {
  const elements = Array.from(document.body.children)

  return elements.filter((element) => {
    // @ts-expect-error vue internal
    return (!!element.__vue_app__ || !!element.__vue__)
  }) as HTMLElement[]
}

const vue2ObserverMap = new WeakMap<HTMLElement, MutationObserver>()

const documentObserver = new MutationObserver(throttle(() => {
  if (!window.__VUE_SCAN__) {
    return
  }

  const mountDoms = getMountDoms()

  if (mountDoms.length === 0) {
    return
  }

  const isVue3 = mountDoms.some((mountDom) => {
    // @ts-expect-error vue internal
    return !!mountDom.__vue_app__
  })

  if (isVue3) {
    documentObserver.disconnect()
  }

  mountDoms.forEach((mountDom) => {
    // @ts-expect-error vue internal
    if (mountDom.__vue_app__) {
      // vue3
      documentObserver.disconnect()
      injectVueScan(mountDom)
    }
    else {
      // vue2
      if (!vue2ObserverMap.get(mountDom)) {
        vue2ObserverMap.set(mountDom, createDomMutationObserver(
          () => mountDom,
          () => {
            console.log('injectVueScan')
            injectVueScan(mountDom)
          },
          {
            childList: true,
            subtree: true,
          },
          600,
        ))
      }
    }
  })
}, 600))

documentObserver.observe(document.body, {
  attributes: true,
  childList: true,
  subtree: true,
})


================================================
FILE: src/core/fps.ts
================================================
export type RenderPhase = 'mount' | 'update'

export interface RenderMeta {
  phase: RenderPhase
  renderTime?: number
  fps: number
}

const WINDOW_MS = 1000

let frameTimestamps: number[] = []
let rafId: number | null = null

function tick() {
  const now = performance.now()
  frameTimestamps.push(now)

  // Drop entries older than 1s
  const cutoff = now - WINDOW_MS
  while (frameTimestamps.length > 0 && frameTimestamps[0] < cutoff) {
    frameTimestamps.shift()
  }

  rafId = requestAnimationFrame(tick)
}

function ensureRunning() {
  if (rafId === null) {
    rafId = requestAnimationFrame(tick)
  }
}

export function getCurrentFps(): number {
  ensureRunning()
  if (frameTimestamps.length < 2)
    return -1
  return Math.round(frameTimestamps.length * 1000 / WINDOW_MS)
}

export function stopFpsMonitor() {
  if (rafId !== null) {
    cancelAnimationFrame(rafId)
    rafId = null
  }
  frameTimestamps = []
}


================================================
FILE: src/core/highlight.ts
================================================
import type { RenderMeta } from './fps'
import { throttle } from 'lodash-es'
import { getComponentBoundingRect, getInstanceName } from './utils'

export interface ComponentBoundingRect {
  top: number
  left: number
  width: number
  height: number
  right: number
  bottom: number
}

export function isInViewport(bounds: ComponentBoundingRect): boolean {
  const viewportWidth = window.innerWidth
  const viewportHeight = window.innerHeight

  // 只要元素和视口有交集,就认为是在视口内
  return !(
    bounds.left >= viewportWidth // 完全在视口右侧
    || bounds.right <= 0 // 完全在视口左侧
    || bounds.top >= viewportHeight // 完全在视口下方
    || bounds.bottom <= 0 // 完全在视口上方
  )
}

interface HighlightItem {
  bounds: ComponentBoundingRect
  name: string
  flashCount: number
  hideComponentName: boolean
  startTime: number
  lastUpdateTime: number
  opacity: number
  state: 'fade-in' | 'visible' | 'fade-out'
}

export interface HighlightCanvasOptions {
  /** default 450ms */
  displayDuration?: number
  /** default 25ms */
  fadeInDuration?: number
  /** default 50ms */
  fadeOutDuration?: number
}

class HighlightCanvas {
  private canvas: HTMLCanvasElement
  private ctx: CanvasRenderingContext2D
  private readonly DISPLAY_DURATION: number
  private readonly FADE_IN_DURATION: number
  private readonly FADE_OUT_DURATION: number
  private highlights: Map<string, HighlightItem> = new Map()
  private animationFrame: number | null = null
  private textMetricsCache: Map<string, TextMetrics> = new Map()

  constructor(options?: HighlightCanvasOptions) {
    this.DISPLAY_DURATION = options?.displayDuration ?? 450
    this.FADE_IN_DURATION = options?.fadeInDuration ?? 25
    this.FADE_OUT_DURATION = options?.fadeOutDuration ?? 50
    this.canvas = document.createElement('canvas')
    this.canvas.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      pointer-events: none;
      z-index: 9999;
    `
    this.ctx = this.canvas.getContext('2d')!
    document.body.appendChild(this.canvas)
    this.updateCanvasSize()
    window.addEventListener('resize', () => this.updateCanvasSize())
    window.addEventListener('scroll', () => this.scheduleRender())
  }

  private updateCanvasSize() {
    this.canvas.width = window.innerWidth
    this.canvas.height = window.innerHeight
  }

  drawHighlight(bounds: ComponentBoundingRect, uuid: string, name: string, flashCount: number, hideComponentName = false) {
    const now = Date.now()
    const existingItem = this.highlights.get(uuid)

    if (existingItem) {
      existingItem.bounds = bounds
      existingItem.name = name
      existingItem.flashCount = flashCount
      existingItem.hideComponentName = hideComponentName
      existingItem.lastUpdateTime = now

      if (existingItem.state === 'fade-out') {
        existingItem.state = 'visible'
        existingItem.opacity = 1
      }
    }
    else {
      this.highlights.set(uuid, {
        bounds,
        name,
        flashCount,
        hideComponentName,
        startTime: now,
        lastUpdateTime: now,
        opacity: 0,
        state: 'fade-in',
      })
    }

    this.scheduleRender()
  }

  private scheduleRender() {
    if (this.animationFrame)
      return
    this.animationFrame = requestAnimationFrame(() => this.render())
  }

  private render() {
    const now = Date.now()

    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)

    this.ctx.font = '12px sans-serif'
    this.ctx.textBaseline = 'middle'

    for (const [uuid, item] of this.highlights.entries()) {
      if (!isInViewport(item.bounds))
        continue

      const fadeInElapsed = now - item.startTime
      const idleTime = now - item.lastUpdateTime
      const fadeOutElapsed = now - item.startTime

      switch (item.state) {
        case 'fade-in':
          item.opacity = Math.min(1, fadeInElapsed / this.FADE_IN_DURATION)
          if (fadeInElapsed >= this.FADE_IN_DURATION) {
            item.state = 'visible'
            item.opacity = 1
          }
          break

        case 'visible':
          if (idleTime >= this.DISPLAY_DURATION) {
            item.state = 'fade-out'
            item.startTime = now
          }
          break

        case 'fade-out':
          item.opacity = Math.max(0, 1 - (fadeOutElapsed / this.FADE_OUT_DURATION))
          if (fadeOutElapsed >= this.FADE_OUT_DURATION) {
            this.highlights.delete(uuid)
            continue
          }
          break
      }

      this.drawBorder(item)
      if (!item.hideComponentName) {
        this.drawLabel(item, item.opacity)
      }
    }

    if (this.highlights.size > 0) {
      this.animationFrame = requestAnimationFrame(() => this.render())
    }
    else {
      this.animationFrame = null
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    }
  }

  private drawBorder(item: HighlightItem) {
    const { bounds, flashCount, opacity } = item
    this.ctx.strokeStyle = `rgba(${Math.min(255, flashCount * 6)}, ${Math.max(0, 255 - flashCount * 6)}, 0, ${opacity})`
    this.ctx.lineWidth = 2
    this.ctx.strokeRect(
      bounds.left,
      bounds.top,
      bounds.width,
      bounds.height,
    )
  }

  private drawLabel(item: HighlightItem, opacity: number) {
    const { bounds, name, flashCount } = item
    const labelMetrics = this.getTextMetrics(name)
    const padding = 6
    const labelHeight = 20

    // 计算标签位置 - 移除额外的padding,直接贴在边框上
    let labelX = bounds.left
    let labelY = bounds.top

    // 确保标签在视口内
    const viewportHeight = window.innerHeight
    const labelTotalHeight = labelHeight
    const viewportWidth = window.innerWidth
    const labelTotalWidth = labelMetrics.width + padding * 2

    // 如果标签底部超出视口
    if (labelY + labelTotalHeight > viewportHeight) {
      labelY = viewportHeight - labelTotalHeight
    }

    // 如果标签右侧超出视口
    if (labelX + labelTotalWidth > viewportWidth) {
      labelX = viewportWidth - labelTotalWidth
    }

    // 绘制背景
    this.ctx.fillStyle = `rgba(${Math.min(255, flashCount * 6)}, ${Math.max(0, 255 - flashCount * 6)}, 0, ${opacity * 0.8})`
    this.ctx.fillRect(labelX, labelY, labelMetrics.width + padding * 2, labelHeight)

    // 绘制文本 - 确保文本在背景中居中
    this.ctx.fillStyle = Math.min(255, flashCount * 6) > 128
      ? `rgba(255, 255, 255, ${opacity})`
      : `rgba(0, 0, 0, ${opacity})`
    this.ctx.fillText(name, labelX + padding, labelY + labelHeight / 2)
  }

  private getTextMetrics(text: string): TextMetrics {
    const cached = this.textMetricsCache.get(text)
    if (cached)
      return cached

    const metrics = this.ctx.measureText(text)
    this.textMetricsCache.set(text, metrics)
    return metrics
  }

  clear(uuid: string) {
    const item = this.highlights.get(uuid)
    if (item && item.state !== 'fade-out') {
      item.state = 'fade-out'
      item.startTime = Date.now()
      this.scheduleRender()
    }
  }

  clearAll() {
    this.highlights.clear()
    this.textMetricsCache.clear()
    if (this.animationFrame) {
      cancelAnimationFrame(this.animationFrame)
      this.animationFrame = null
    }
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }

  destroy() {
    this.clearAll()
    if (this.canvas && this.canvas.parentNode) {
      this.canvas.parentNode.removeChild(this.canvas)
    }
  }
}

let highlightCanvas: HighlightCanvas | null = null

function getHighlightCanvas(options?: HighlightCanvasOptions) {
  if (highlightCanvas)
    return highlightCanvas
  highlightCanvas = new HighlightCanvas(options)
  return highlightCanvas
}

window.addEventListener('unload', () => {
  if (highlightCanvas) {
    highlightCanvas.destroy()
    highlightCanvas = null
  }
})

type UpdateHighlightFn = (
  bounds: ComponentBoundingRect,
  name: string,
  flashCount: number,
  hideComponentName?: boolean
) => void

export function createUpdateHighlight(): UpdateHighlightFn {
  return throttle<UpdateHighlightFn>(
    (bounds, name, flashCount, hideComponentName) => {
      if (!isInViewport(bounds) || !highlightCanvas)
        return
      highlightCanvas.drawHighlight(bounds, name, name, flashCount, hideComponentName)
    },
    500,
  )
}

export function highlight(
  instance: any,
  uuid: string,
  flashCount: number,
  meta?: RenderMeta,
  options?: {
    hideComponentName?: boolean
  } & HighlightCanvasOptions,
) {
  const highlightCanvas = getHighlightCanvas(options)

  const bounds = getComponentBoundingRect(instance)

  if (!bounds.width && !bounds.height)
    return
  if (!isInViewport(bounds))
    return

  let name = `${getInstanceName(instance)} x ${flashCount}`
  if (meta) {
    const parts: string[] = [meta.phase]
    if (meta.renderTime != null)
      parts.push(`${meta.renderTime.toFixed(1)}ms`)
    if (meta.fps > 0)
      parts.push(`${meta.fps}fps`)
    name += ` · ${parts.join(' · ')}`
  }
  highlightCanvas?.drawHighlight(bounds, uuid, name, flashCount, options?.hideComponentName)
}

export function unhighlight(uuid: string) {
  highlightCanvas?.clear(uuid)
}

export function clearhighlight(uuid: string) {
  highlightCanvas?.clear(uuid)
}


================================================
FILE: src/core/hook.ts
================================================
import type { VueAppInstance } from '@vue/devtools-kit'
import { getCurrentFps } from './fps'
import {
  clearhighlight,
  createUpdateHighlight,
  highlight,
  type HighlightCanvasOptions,
  unhighlight,
} from './highlight'
import { getInstanceName } from './utils'

export interface BACE_VUE_INSTANCE extends VueAppInstance {
  __vue_scan_injected__?: boolean
  /** beforeUpdate */
  bu?: Array<() => void> | null
  /** updated */
  u?: Array<() => void> | null
  /** beforeUnmount */
  bum?: Array<() => void> | null
  _uid?: number
  __flashCount?: number
  __flashTimeout?: ReturnType<typeof setTimeout> | null
  __renderStartTime?: number | null
  $options?: {
    beforeUpdate?: Array<() => void> | null
    updated?: Array<() => void> | null
    beforeDestroy?: Array<() => void> | null
  }
}

export function createOnBeforeUpdateHook(instance?: BACE_VUE_INSTANCE) {
  if (!instance) {
    return
  }

  const el = instance?.subTree?.el || instance.$el

  if (!el) {
    return
  }

  return () => {
    instance.__renderStartTime = performance.now()
  }
}

export function createOnMountedHook(instance?: BACE_VUE_INSTANCE, options?: {
  hideComponentName?: boolean
  interval?: number
} & HighlightCanvasOptions) {
  const {
    interval = 1000,
  } = options || {}

  if (!instance) {
    return
  }

  const el = instance?.subTree?.el || instance.$el

  if (!el) {
    return
  }

  const name = getInstanceName(instance)
  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')

  return () => {
    if (!instance.__flashCount) {
      instance.__flashCount = 0
    }

    if (!instance.__updateHighlight) {
      instance.__updateHighlight = createUpdateHighlight()
    }

    instance.__flashCount++

    const fps = getCurrentFps()
    highlight(instance, uuid, instance.__flashCount, { phase: 'mount', fps }, options)

    if (instance.__flashTimeout) {
      clearTimeout(instance.__flashTimeout)
      instance.__flashTimeout = null
    }

    instance.__flashTimeout = setTimeout(() => {
      unhighlight(uuid)
      instance.__flashTimeout = null
      instance.__flashCount = 0
    }, interval)
  }
}

export function createOnUpdatedHook(instance?: BACE_VUE_INSTANCE, options?: {
  hideComponentName?: boolean
  interval?: number
} & HighlightCanvasOptions) {
  const {
    interval = 1000,
  } = options || {}

  if (!instance) {
    return
  }

  const el = instance?.subTree?.el || instance.$el

  if (!el) {
    return
  }

  const name = getInstanceName(instance)
  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')

  return () => {
    if (!instance.__flashCount) {
      instance.__flashCount = 0
    }

    if (!instance.__updateHighlight) {
      instance.__updateHighlight = createUpdateHighlight()
    }

    instance.__flashCount++

    const renderTime = instance.__renderStartTime != null
      ? performance.now() - instance.__renderStartTime
      : undefined
    instance.__renderStartTime = null
    const fps = getCurrentFps()

    highlight(instance, uuid, instance.__flashCount, { phase: 'update', renderTime, fps }, options)

    if (instance.__flashTimeout) {
      clearTimeout(instance.__flashTimeout)
      instance.__flashTimeout = null
    }

    instance.__flashTimeout = setTimeout(() => {
      unhighlight(uuid)
      instance.__flashTimeout = null
      instance.__flashCount = 0
    }, interval)
  }
}

export function createOnBeforeUnmountHook(instance?: BACE_VUE_INSTANCE) {
  if (!instance) {
    return
  }

  const el = instance?.subTree?.el || instance.$el

  if (!el) {
    return
  }

  const name = getInstanceName(instance)
  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')

  return () => {
    clearhighlight(uuid)
  }
}


================================================
FILE: src/core/index.ts
================================================
export * from './fps'
export * from './highlight'
export * from './hook'


================================================
FILE: src/core/utils.ts
================================================
import type { VueAppInstance } from '@vue/devtools-kit'
import { basename, classify } from '@vue/devtools-shared'

interface ComponentBoundingRect {
  left: number
  top: number
  right: number
  bottom: number
  width: number
  height: number
}

function createRect() {
  const rect = {
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    get width() { return rect.right - rect.left },
    get height() { return rect.bottom - rect.top },
  }
  return rect
}

const DEFAULT_RECT = {
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  width: 0,
  height: 0,
}

let range: any
function getTextRect(node: any) {
  if (!range)
    range = document.createRange()

  range.selectNode(node)

  return range.getBoundingClientRect()
}

function mergeRects(a: any, b: any) {
  if (!a.top || b.top < a.top)
    a.top = b.top

  if (!a.bottom || b.bottom > a.bottom)
    a.bottom = b.bottom

  if (!a.left || b.left < a.left)
    a.left = b.left

  if (!a.right || b.right > a.right)
    a.right = b.right

  return a
}

function getAppRecord(instance: VueAppInstance) {
  if (instance.__VUE_DEVTOOLS_NEXT_APP_RECORD__)
    return instance.__VUE_DEVTOOLS_NEXT_APP_RECORD__
  else if (instance.root)
    return instance.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__
}

function isFragment(instance: VueAppInstance) {
  const subTreeType = instance?.subTree?.type
  if (!subTreeType) {
    return false
  }
  const appRecord = getAppRecord(instance)
  if (appRecord) {
    return appRecord?.types?.Fragment === subTreeType
  }
  return false
}

function getFragmentRect(vnode: any) {
  const rect = createRect()
  if (!vnode.children)
    return rect

  for (let i = 0, l = vnode.children.length; i < l; i++) {
    const childVnode = vnode.children[i]
    let childRect
    if (childVnode.component) {
      childRect = getComponentBoundingRect(childVnode.component)
    }
    else if (childVnode.el) {
      const el = childVnode.el
      if (el.nodeType === 1 || el.getBoundingClientRect)
        childRect = el.getBoundingClientRect()

      else if (el.nodeType === 3 && el.data.trim())
        childRect = getTextRect(el)
    }
    if (childRect)
      mergeRects(rect, childRect)
  }

  return rect
}

export function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect {
  const el = instance?.subTree?.el || instance?.$el

  if (typeof window === 'undefined') {
    // @TODO: Find position from instance or a vnode (for functional components).
    return DEFAULT_RECT
  }

  if (isFragment(instance))
    return getFragmentRect(instance?.subTree)

  else if (el?.nodeType === 1)
    return el?.getBoundingClientRect()

  else if (instance?.subTree?.component || instance?.$vnode)
    return getComponentBoundingRect(instance?.subTree?.component || instance?.$vnode as VueAppInstance)
  else
    return DEFAULT_RECT
}

// ---

function getComponentTypeName(options: VueAppInstance['type']) {
  const name = options?.name || options?._componentTag || options?.tag || options.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ || options.__name
  if (name === 'index' && options.__file?.endsWith('index.vue')) {
    return ''
  }
  return name
}

function saveComponentGussedName(instance: VueAppInstance, name: string) {
  instance.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ = name
  return name
}

function getComponentFileName(options: VueAppInstance['type']) {
  const file = options.__file
  if (file)
    return classify(basename(file, '.vue'))
}

export function getInstanceName(instance: VueAppInstance) {
  const name = getComponentTypeName(instance?.type || instance?.$vnode || {})
  if (name)
    return name
  if (instance?.root === instance || instance?.$root === instance)
    return 'Root'
  for (const key in instance.parent?.type?.components) {
    if (instance.parent.type.components[key] === instance?.type)
      return saveComponentGussedName(instance, key)
  }

  for (const key in instance.appContext?.components) {
    if (instance.appContext.components[key] === instance?.type)
      return saveComponentGussedName(instance, key)
  }

  const fileName = getComponentFileName(instance?.type || {})
  if (fileName)
    return fileName

  return 'Anonymous Component'
}


================================================
FILE: src/global.d.ts
================================================
declare global {
  interface Window {
    __VUE_SCAN__?: {
      plugin: typeof import('./index').default
      createOnBeforeUpdateHook: typeof import('./core/hook').createOnBeforeUpdateHook
      createOnBeforeUnmountHook: typeof import('./core/hook').createOnBeforeUnmountHook
      createOnMountedHook: typeof import('./core/hook').createOnMountedHook
      createOnUpdatedHook: typeof import('./core/hook').createOnUpdatedHook
    }
  }
}

export {}


================================================
FILE: src/index.ts
================================================
import type { VueAppInstance } from '@vue/devtools-kit'
import type { Plugin } from 'vue-demi'
import type { VueScanBaseOptions, VueScanOptions } from './types'
import { createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/index'
import { isDev } from './utils'

const plugin: Plugin<VueScanOptions> = {
  install: (app, options?: VueScanBaseOptions) => {
    const { enable = isDev() } = options || {}

    if (!enable) {
      return
    }

    app.mixin({
      mounted() {
        const instance = (() => {
          return (this as any).$
        })() as VueAppInstance

        if (!instance.__m) {
          instance.__m = createOnMountedHook(instance, options)
        }

        if (!instance.__bu) {
          instance.__bu = createOnBeforeUpdateHook(instance)
        }

        if (!instance.__u) {
          instance.__u = createOnUpdatedHook(instance, options)
        }

        if (!instance.__bum) {
          instance.__bum = createOnBeforeUnmountHook(instance)
        }

        instance.__vue_scan_injected__ = true
        instance.__m?.()
      },
      beforeUpdate() {
        const instance = (() => {
          return (this as any).$
        })() as VueAppInstance

        instance.__bu?.()
      },
      updated() {
        const instance = (() => {
          return (this as any).$
        })() as VueAppInstance

        instance.__u?.()
      },
      beforeUnmount() {
        const instance = (() => {
          return (this as any).$
        })() as VueAppInstance

        instance.__bum?.()
      },
    })
  },
}

export default plugin

export * from './types'


================================================
FILE: src/index_vue2.ts
================================================
import type { VueAppInstance } from '@vue/devtools-kit'
import type { Plugin } from 'vue-demi'
import type { VueScanBaseOptions } from './types'
import { createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/index'
import { isDev } from './utils'

const plugin: Plugin<VueScanBaseOptions> = {
  install: (app, options?: VueScanBaseOptions) => {
    const { enable = isDev() } = options || {}

    if (!enable) {
      return
    }

    app.mixin({
      mounted() {
        const instance = this as VueAppInstance

        if (!instance.__m) {
          instance.__m = createOnMountedHook(instance, options)
        }

        if (!instance.__bu) {
          instance.__bu = createOnBeforeUpdateHook(instance)
        }

        if (!instance.__u) {
          instance.__u = createOnUpdatedHook(instance, options)
        }

        if (!instance.__bum) {
          instance.__bum = createOnBeforeUnmountHook(instance)
        }

        instance.__vue_scan_injected__ = true
        instance.__m?.()
      },
      beforeUpdate() {
        const instance = this as VueAppInstance

        instance.__bu?.()
      },
      updated() {
        const instance = this as VueAppInstance

        instance.__u?.()
      },
      beforeDestroy() {
        const instance = this as VueAppInstance

        instance.__bum?.()
      },
    })
  },
}

export default plugin

export * from './types'


================================================
FILE: src/types.ts
================================================
import type { HighlightCanvasOptions } from './core'

export interface VueScanBaseOptions extends HighlightCanvasOptions {
  enable?: boolean
  hideComponentName?: boolean
  interval?: number
}

export type VueScanOptions = [
  VueScanBaseOptions | undefined,
]


================================================
FILE: src/utils/MutationObserverDom.ts
================================================
import { throttle } from 'lodash-es'

export function createDomMutationObserver<T extends Element>(
  getTarget: () => T | null,
  callback: MutationCallback,
  options?: MutationObserverInit,
  throttleWait: number = 200,
) {
  const targetObserver = new MutationObserver(throttle(callback, throttleWait))

  const findTargetObserver = new MutationObserver(throttle(() => {
    const target = getTarget()
    if (target) {
      findTargetObserver.disconnect()
      targetObserver.observe(target, options)
    }
  }, 200))

  findTargetObserver.observe(document.body, {
    childList: true,
    subtree: true,
  })

  return targetObserver
}


================================================
FILE: src/utils.ts
================================================
export function isDev() {
  return (import.meta.env && import.meta.env.DEV === true)
    // eslint-disable-next-line node/prefer-global/process
    || (process.env.NODE_ENV === 'development')
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext", "DOM"],
    "moduleDetection": "force",
    "rootDir": ".",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "strict": true,
    "strictNullChecks": true,
    "noEmit": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipDefaultLibCheck": true,
    "skipLibCheck": true
  }
}


================================================
FILE: uno.config.ts
================================================
import config from './packages/extension/uno.config'

export default config
Download .txt
gitextract_wgm2a3xm/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── README.md
├── build.config.ts
├── eslint.config.js
├── examples/
│   ├── vue2/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── env.d.ts
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── assets/
│   │   │   │   ├── base.css
│   │   │   │   └── main.css
│   │   │   ├── components/
│   │   │   │   └── HelloWorld.vue
│   │   │   └── main.ts
│   │   ├── tsconfig.config.json
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   └── vue3/
│       ├── .gitignore
│       ├── .vscode/
│       │   └── extensions.json
│       ├── README.md
│       ├── env.d.ts
│       ├── index.html
│       ├── package.json
│       ├── src/
│       │   ├── App.vue
│       │   ├── assets/
│       │   │   ├── base.css
│       │   │   └── main.css
│       │   ├── components/
│       │   │   └── HelloWorld.vue
│       │   └── main.ts
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
├── package.json
├── packages/
│   ├── devpilot-plugin-vue-scan/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── client/
│   │   │   │   ├── control-panel.ts
│   │   │   │   ├── fps.ts
│   │   │   │   ├── helpers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── runtime-control.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── vue-injector.ts
│   │   │   ├── data-store.ts
│   │   │   ├── index.ts
│   │   │   ├── skill.md
│   │   │   └── types.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── entrypoints/
│   │   │   ├── background.ts
│   │   │   ├── content.ts
│   │   │   └── popup/
│   │   │       ├── App.vue
│   │   │       ├── index.html
│   │   │       └── main.ts
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   ├── uno.config.ts
│   │   ├── utils/
│   │   │   └── storage.ts
│   │   └── wxt.config.ts
│   └── nuxt/
│       ├── .gitignore
│       ├── Readme.md
│       ├── package.json
│       ├── playground/
│       │   ├── app.vue
│       │   ├── components/
│       │   │   └── HelloWorld.vue
│       │   ├── nuxt.config.ts
│       │   ├── package.json
│       │   ├── server/
│       │   │   └── tsconfig.json
│       │   └── tsconfig.json
│       ├── src/
│       │   ├── module.ts
│       │   └── runtime/
│       │       ├── plugin.ts
│       │       └── server/
│       │           └── tsconfig.json
│       ├── test/
│       │   ├── basic.test.ts
│       │   └── fixtures/
│       │       └── basic/
│       │           ├── app.vue
│       │           ├── nuxt.config.ts
│       │           └── package.json
│       └── tsconfig.json
├── patches/
│   └── @vue__devtools-kit.patch
├── pnpm-workspace.yaml
├── src/
│   ├── auto.ts
│   ├── core/
│   │   ├── fps.ts
│   │   ├── highlight.ts
│   │   ├── hook.ts
│   │   ├── index.ts
│   │   └── utils.ts
│   ├── global.d.ts
│   ├── index.ts
│   ├── index_vue2.ts
│   ├── types.ts
│   ├── utils/
│   │   └── MutationObserverDom.ts
│   └── utils.ts
├── tsconfig.json
└── uno.config.ts
Download .txt
SYMBOL INDEX (115 symbols across 23 files)

FILE: packages/devpilot-plugin-vue-scan/src/client/control-panel.ts
  type Document (line 6) | interface Document {
  function updatePanelContent (line 12) | function updatePanelContent(panel: HTMLDivElement, client: DevpilotClien...
  function getCurrentPanel (line 99) | function getCurrentPanel(): HTMLDivElement | null {
  function registerKeyboardShortcuts (line 104) | function registerKeyboardShortcuts(client: DevpilotClient<VueScanServerM...
  function createControlPanel (line 141) | function createControlPanel(client: DevpilotClient<VueScanServerMethods>...

FILE: packages/devpilot-plugin-vue-scan/src/client/fps.ts
  constant WINDOW_MS (line 1) | const WINDOW_MS = 1000
  function tick (line 6) | function tick() {
  function ensureRunning (line 18) | function ensureRunning() {
  function getCurrentFps (line 24) | function getCurrentFps(): number {
  function stopFpsMonitor (line 31) | function stopFpsMonitor(): void {

FILE: packages/devpilot-plugin-vue-scan/src/client/helpers.ts
  type ComponentBoundingRect (line 1) | interface ComponentBoundingRect {
  constant DEFAULT_RECT (line 10) | const DEFAULT_RECT: ComponentBoundingRect = {
  function getInstanceName (line 19) | function getInstanceName(instance: any): string {
  function isFragment (line 48) | function isFragment(instance: any): boolean {
  function createRect (line 59) | function createRect() {
  function mergeRects (line 71) | function mergeRects(a: any, b: any) {
  function getTextRect (line 84) | function getTextRect(node: any) {
  function getFragmentRect (line 91) | function getFragmentRect(vnode: any): ComponentBoundingRect {
  function getComponentBoundingRect (line 114) | function getComponentBoundingRect(instance: any): ComponentBoundingRect {
  function isInViewport (line 127) | function isInViewport(bounds: ComponentBoundingRect): boolean {

FILE: packages/devpilot-plugin-vue-scan/src/client/index.ts
  function setup (line 13) | function setup() {

FILE: packages/devpilot-plugin-vue-scan/src/client/runtime-control.ts
  function initRuntimeControl (line 5) | function initRuntimeControl(client: DevpilotClient<VueScanServerMethods>...

FILE: packages/devpilot-plugin-vue-scan/src/client/types.ts
  type VueScanServerMethods (line 2) | interface VueScanServerMethods {
  type VueScanClientRpc (line 28) | interface VueScanClientRpc {
  type RuntimeControl (line 32) | interface RuntimeControl {
  type Window (line 43) | interface Window {

FILE: packages/devpilot-plugin-vue-scan/src/client/vue-injector.ts
  type VueInstance (line 12) | interface VueInstance {
  function getSourceLocation (line 44) | function getSourceLocation(instance: VueInstance): string | undefined {
  function isFromUserCode (line 61) | function isFromUserCode(source: string | undefined): boolean {
  function sendReportEvent (line 67) | function sendReportEvent(
  function createBeforeUpdateHook (line 109) | function createBeforeUpdateHook(instance: VueInstance) {
  function createMountedReportHook (line 118) | function createMountedReportHook(instance: VueInstance, client: Devpilot...
  function createUpdatedReportHook (line 128) | function createUpdatedReportHook(instance: VueInstance, client: Devpilot...
  function injectVueScan (line 142) | function injectVueScan(node: HTMLElement, client: DevpilotClient<VueScan...
  function getMountDoms (line 283) | function getMountDoms() {
  function createDomMutationObserver (line 290) | function createDomMutationObserver(
  function injectVueScanWithRuntimeControl (line 314) | function injectVueScanWithRuntimeControl(client: DevpilotClient<VueScanS...

FILE: packages/devpilot-plugin-vue-scan/src/data-store.ts
  function normalize (line 3) | function normalize(name: string): string {
  class VueScanDataStore (line 9) | class VueScanDataStore {
    method addEvent (line 14) | addEvent(event: ComponentUpdateEvent) {
    method query (line 25) | query(params: QueryParams): QueryResult {
    method clear (line 153) | clear() {
    method startRecording (line 157) | startRecording() {
    method stopRecording (line 161) | stopRecording() {
    method exportAll (line 165) | exportAll() {

FILE: packages/devpilot-plugin-vue-scan/src/index.ts
  method mcpSetup (line 45) | mcpSetup() {

FILE: packages/devpilot-plugin-vue-scan/src/types.ts
  type ComponentUpdateEvent (line 2) | interface ComponentUpdateEvent {
  type ComponentSummary (line 22) | interface ComponentSummary {
  type QueryParams (line 33) | interface QueryParams {
  type QueryResult (line 45) | interface QueryResult {

FILE: packages/extension/entrypoints/content.ts
  method main (line 5) | async main() {

FILE: packages/nuxt/src/module.ts
  type ModuleOptions (line 6) | interface ModuleOptions extends VueScanBaseOptions {}
  method setup (line 17) | setup(_options, _nuxt) {

FILE: src/auto.ts
  function injectVueScan (line 32) | function injectVueScan(node: HTMLElement) {
  function getMountDoms (line 203) | function getMountDoms() {

FILE: src/core/fps.ts
  type RenderPhase (line 1) | type RenderPhase = 'mount' | 'update'
  type RenderMeta (line 3) | interface RenderMeta {
  constant WINDOW_MS (line 9) | const WINDOW_MS = 1000
  function tick (line 14) | function tick() {
  function ensureRunning (line 27) | function ensureRunning() {
  function getCurrentFps (line 33) | function getCurrentFps(): number {
  function stopFpsMonitor (line 40) | function stopFpsMonitor() {

FILE: src/core/highlight.ts
  type ComponentBoundingRect (line 5) | interface ComponentBoundingRect {
  function isInViewport (line 14) | function isInViewport(bounds: ComponentBoundingRect): boolean {
  type HighlightItem (line 27) | interface HighlightItem {
  type HighlightCanvasOptions (line 38) | interface HighlightCanvasOptions {
  class HighlightCanvas (line 47) | class HighlightCanvas {
    method constructor (line 57) | constructor(options?: HighlightCanvasOptions) {
    method updateCanvasSize (line 76) | private updateCanvasSize() {
    method drawHighlight (line 81) | drawHighlight(bounds: ComponentBoundingRect, uuid: string, name: strin...
    method scheduleRender (line 113) | private scheduleRender() {
    method render (line 119) | private render() {
    method drawBorder (line 175) | private drawBorder(item: HighlightItem) {
    method drawLabel (line 187) | private drawLabel(item: HighlightItem, opacity: number) {
    method getTextMetrics (line 224) | private getTextMetrics(text: string): TextMetrics {
    method clear (line 234) | clear(uuid: string) {
    method clearAll (line 243) | clearAll() {
    method destroy (line 253) | destroy() {
  function getHighlightCanvas (line 263) | function getHighlightCanvas(options?: HighlightCanvasOptions) {
  type UpdateHighlightFn (line 277) | type UpdateHighlightFn = (
  function createUpdateHighlight (line 284) | function createUpdateHighlight(): UpdateHighlightFn {
  function highlight (line 295) | function highlight(
  function unhighlight (line 325) | function unhighlight(uuid: string) {
  function clearhighlight (line 329) | function clearhighlight(uuid: string) {

FILE: src/core/hook.ts
  type BACE_VUE_INSTANCE (line 12) | interface BACE_VUE_INSTANCE extends VueAppInstance {
  function createOnBeforeUpdateHook (line 31) | function createOnBeforeUpdateHook(instance?: BACE_VUE_INSTANCE) {
  function createOnMountedHook (line 47) | function createOnMountedHook(instance?: BACE_VUE_INSTANCE, options?: {
  function createOnUpdatedHook (line 95) | function createOnUpdatedHook(instance?: BACE_VUE_INSTANCE, options?: {
  function createOnBeforeUnmountHook (line 148) | function createOnBeforeUnmountHook(instance?: BACE_VUE_INSTANCE) {

FILE: src/core/utils.ts
  type ComponentBoundingRect (line 4) | interface ComponentBoundingRect {
  function createRect (line 13) | function createRect() {
  constant DEFAULT_RECT (line 25) | const DEFAULT_RECT = {
  function getTextRect (line 35) | function getTextRect(node: any) {
  function mergeRects (line 44) | function mergeRects(a: any, b: any) {
  function getAppRecord (line 60) | function getAppRecord(instance: VueAppInstance) {
  function isFragment (line 67) | function isFragment(instance: VueAppInstance) {
  function getFragmentRect (line 79) | function getFragmentRect(vnode: any) {
  function getComponentBoundingRect (line 105) | function getComponentBoundingRect(instance: VueAppInstance): ComponentBo...
  function getComponentTypeName (line 127) | function getComponentTypeName(options: VueAppInstance['type']) {
  function saveComponentGussedName (line 135) | function saveComponentGussedName(instance: VueAppInstance, name: string) {
  function getComponentFileName (line 140) | function getComponentFileName(options: VueAppInstance['type']) {
  function getInstanceName (line 146) | function getInstanceName(instance: VueAppInstance) {

FILE: src/global.d.ts
  type Window (line 2) | interface Window {

FILE: src/index.ts
  method mounted (line 16) | mounted() {
  method beforeUpdate (line 40) | beforeUpdate() {
  method updated (line 47) | updated() {
  method beforeUnmount (line 54) | beforeUnmount() {

FILE: src/index_vue2.ts
  method mounted (line 16) | mounted() {
  method beforeUpdate (line 38) | beforeUpdate() {
  method updated (line 43) | updated() {
  method beforeDestroy (line 48) | beforeDestroy() {

FILE: src/types.ts
  type VueScanBaseOptions (line 3) | interface VueScanBaseOptions extends HighlightCanvasOptions {
  type VueScanOptions (line 9) | type VueScanOptions = [

FILE: src/utils.ts
  function isDev (line 1) | function isDev() {

FILE: src/utils/MutationObserverDom.ts
  function createDomMutationObserver (line 3) | function createDomMutationObserver<T extends Element>(
Condensed preview — 97 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (144K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 75,
    "preview": "github: [antfu]\nko_fi: huali58081\ncustom: ['https://afdian.com/a/huali08']\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 587,
    "preview": "name: CI\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches:\n      - master\n\njobs:\n  test:\n    runs"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3426,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      id"
  },
  {
    "path": ".gitignore",
    "chars": 383,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\n"
  },
  {
    "path": "README.md",
    "chars": 2648,
    "preview": "# z-vue-scan\n\nA Vue scanning plugin that works with both Vue 2 and Vue 3. The component will flash with a red border whe"
  },
  {
    "path": "build.config.ts",
    "chars": 858,
    "preview": "import { defineBuildConfig } from 'unbuild'\n\nexport default defineBuildConfig(\n  [\n    {\n      entries: [\n        'src/i"
  },
  {
    "path": "eslint.config.js",
    "chars": 272,
    "preview": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  ignores: [\n    'docs',\n    'dist',\n    'packages/exte"
  },
  {
    "path": "examples/vue2/.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\n.DS_Stor"
  },
  {
    "path": "examples/vue2/README.md",
    "chars": 1606,
    "preview": "# vue2\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup\n\n[VSCode](htt"
  },
  {
    "path": "examples/vue2/env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vue2/index.html",
    "chars": 337,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <"
  },
  {
    "path": "examples/vue2/package.json",
    "chars": 651,
    "preview": "{\n  \"name\": \"vue2\",\n  \"version\": \"0.0.37\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p type-check build-only\","
  },
  {
    "path": "examples/vue2/src/App.vue",
    "chars": 268,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport HelloWorld from './components/HelloWorld.vue'\n\nconst b = ref('"
  },
  {
    "path": "examples/vue2/src/assets/base.css",
    "chars": 2041,
    "preview": "/* color palette from <https://github.com/vuejs/theme> */\n:root {\n  --vt-c-white: #ffffff;\n  --vt-c-white-soft: #f8f8f8;"
  },
  {
    "path": "examples/vue2/src/assets/main.css",
    "chars": 477,
    "preview": "@import \"./base.css\";\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n\n  font-weight: normal;\n}\n\na,\n.gre"
  },
  {
    "path": "examples/vue2/src/components/HelloWorld.vue",
    "chars": 462,
    "preview": "<script lang=\"ts\">\nimport { defineComponent } from 'vue'\n\nexport default defineComponent({\n  name: 'HelloWorld',\n  props"
  },
  {
    "path": "examples/vue2/src/main.ts",
    "chars": 239,
    "preview": "import Vue from 'vue'\nimport VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'\nimport App from './App.vue'\n\nim"
  },
  {
    "path": "examples/vue2/tsconfig.config.json",
    "chars": 196,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"types\": [\"node\"]\n "
  },
  {
    "path": "examples/vue2/tsconfig.json",
    "chars": 275,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\":"
  },
  {
    "path": "examples/vue2/vite.config.ts",
    "chars": 474,
    "preview": "import { fileURLToPath, URL } from 'node:url'\n\nimport legacy from '@vitejs/plugin-legacy'\nimport vue2 from '@vitejs/plug"
  },
  {
    "path": "examples/vue3/.gitignore",
    "chars": 317,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
  },
  {
    "path": "examples/vue3/.vscode/extensions.json",
    "chars": 39,
    "preview": "{\n  \"recommendations\": [\"Vue.volar\"]\n}\n"
  },
  {
    "path": "examples/vue3/README.md",
    "chars": 850,
    "preview": "# vue3\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup\n\n[VSCode](htt"
  },
  {
    "path": "examples/vue3/env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vue3/index.html",
    "chars": 329,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta n"
  },
  {
    "path": "examples/vue3/package.json",
    "chars": 671,
    "preview": "{\n  \"name\": \"vue3\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    "
  },
  {
    "path": "examples/vue3/src/App.vue",
    "chars": 244,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport HelloWorld from './components/HelloWorld.vue'\n\nconst b = ref('"
  },
  {
    "path": "examples/vue3/src/assets/base.css",
    "chars": 2067,
    "preview": "/* color palette from <https://github.com/vuejs/theme> */\n:root {\n  --vt-c-white: #ffffff;\n  --vt-c-white-soft: #f8f8f8;"
  },
  {
    "path": "examples/vue3/src/assets/main.css",
    "chars": 492,
    "preview": "@import './base.css';\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  font-weight: normal;\n}\n\na,\n.gree"
  },
  {
    "path": "examples/vue3/src/components/HelloWorld.vue",
    "chars": 197,
    "preview": "<script setup lang=\"ts\">\nconst msg = defineModel('msg', {\n  default: '',\n})\n</script>\n\n<template>\n  <div>\n    <div>\n    "
  },
  {
    "path": "examples/vue3/src/main.ts",
    "chars": 229,
    "preview": "import { createApp } from 'vue'\nimport VueScan, { type VueScanOptions } from 'z-vue-scan'\n\nimport App from './App.vue'\ni"
  },
  {
    "path": "examples/vue3/tsconfig.app.json",
    "chars": 332,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \""
  },
  {
    "path": "examples/vue3/tsconfig.json",
    "chars": 139,
    "preview": "{\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    }\n  "
  },
  {
    "path": "examples/vue3/tsconfig.node.json",
    "chars": 414,
    "preview": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"."
  },
  {
    "path": "examples/vue3/vite.config.ts",
    "chars": 380,
    "preview": "import { fileURLToPath, URL } from 'node:url'\n\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\n"
  },
  {
    "path": "package.json",
    "chars": 1757,
    "preview": "{\n  \"name\": \"z-vue-scan\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"The component will flash with a r"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/README.md",
    "chars": 3296,
    "preview": "# devpilot-plugin-vue-scan\n\nA [DevPilot](https://github.com/zcf0508/unplugin-devpilot) plugin that exposes Vue component"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/package.json",
    "chars": 1202,
    "preview": "{\n  \"name\": \"devpilot-plugin-vue-scan\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"Vue Scan plugin for"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/control-panel.ts",
    "chars": 5622,
    "preview": "import type { DevpilotClient } from 'unplugin-devpilot/client'\nimport type { VueScanServerMethods } from './types'\n\n// E"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/fps.ts",
    "chars": 763,
    "preview": "const WINDOW_MS = 1000\n\nlet frameTimestamps: number[] = []\nlet rafId: number | null = null\n\nfunction tick() {\n  const no"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/helpers.ts",
    "chars": 3536,
    "preview": "export interface ComponentBoundingRect {\n  top: number\n  left: number\n  width: number\n  height: number\n  right: number\n "
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/index.ts",
    "chars": 2284,
    "preview": "import type { VueScanClientRpc, VueScanServerMethods } from './types'\n// Client-side entry point for Vue Scan DevPilot P"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/runtime-control.ts",
    "chars": 1186,
    "preview": "import type { DevpilotClient } from 'unplugin-devpilot/client'\nimport type { RuntimeControl, VueScanServerMethods } from"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/types.ts",
    "chars": 1207,
    "preview": "// Server-side RPC methods that client can call\nexport interface VueScanServerMethods {\n  'vue-scan:recordUpdate': (data"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/vue-injector.ts",
    "chars": 11427,
    "preview": "/**\n * Vue component injection for data collection.\n * Follows the same injection pattern as src/auto.ts,\n * adding a da"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/data-store.ts",
    "chars": 4969,
    "preview": "import type { ComponentSummary, ComponentUpdateEvent, QueryParams, QueryResult } from './types'\n\nfunction normalize(name"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/index.ts",
    "chars": 3607,
    "preview": "import type { DevpilotPlugin } from 'unplugin-devpilot'\nimport type { ComponentUpdateEvent, QueryParams } from './types'"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/skill.md",
    "chars": 3896,
    "preview": "---\nname: vue-scan\ndescription: Vue component render performance monitoring. Captures mount/update events with render ti"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/types.ts",
    "chars": 1283,
    "preview": "// Server-side types\nexport interface ComponentUpdateEvent {\n  timestamp: number\n  componentName: string\n  componentId: "
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsconfig.client.json",
    "chars": 548,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"modu"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsconfig.json",
    "chars": 526,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"p"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsdown.config.ts",
    "chars": 473,
    "preview": "import { copyFileSync, mkdirSync } from 'node:fs'\nimport { defineConfig } from 'tsdown'\n\nexport default defineConfig([\n "
  },
  {
    "path": "packages/extension/.gitignore",
    "chars": 286,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\n"
  },
  {
    "path": "packages/extension/README.md",
    "chars": 232,
    "preview": "# WXT + Vue 3\n\nThis template should help get you started developing with Vue 3 in WXT.\n\n## Recommended IDE Setup\n\n- [VS "
  },
  {
    "path": "packages/extension/entrypoints/background.ts",
    "chars": 225,
    "preview": "import { autoInject, blacklist } from '../utils/storage'\n\nexport default defineBackground(() => {\n  console.log('Backgro"
  },
  {
    "path": "packages/extension/entrypoints/content.ts",
    "chars": 1351,
    "preview": "import { autoInject, blacklist } from '../utils/storage'\n\nexport default defineContentScript({\n  matches: ['<all_urls>']"
  },
  {
    "path": "packages/extension/entrypoints/popup/App.vue",
    "chars": 2981,
    "preview": "<script lang=\"ts\" setup>\nimport { onMounted, ref, watch } from 'vue'\nimport { autoInject, blacklist } from '../../utils/"
  },
  {
    "path": "packages/extension/entrypoints/popup/index.html",
    "chars": 360,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "packages/extension/entrypoints/popup/main.ts",
    "chars": 107,
    "preview": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport 'uno.css'\n\ncreateApp(App).mount('#app')\n"
  },
  {
    "path": "packages/extension/package.json",
    "chars": 658,
    "preview": "{\n  \"name\": \"vue-scan-ext\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"private\": true,\n  \"description\": \"manifest.jso"
  },
  {
    "path": "packages/extension/tsconfig.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"./.wxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/extension/uno.config.ts",
    "chars": 568,
    "preview": "import { defineConfig, presetIcons, presetWind } from 'unocss'\n\nexport default defineConfig({\n  content: { filesystem: ["
  },
  {
    "path": "packages/extension/utils/storage.ts",
    "chars": 232,
    "preview": "import { storage } from 'wxt/storage'\n\nexport const autoInject = storage.defineItem<boolean>('local:autoInject', {\n  fal"
  },
  {
    "path": "packages/extension/wxt.config.ts",
    "chars": 439,
    "preview": "import UnoCSS from 'unocss/vite'\nimport { defineConfig } from 'wxt'\n\n// See https://wxt.dev/api/config.html\nexport defau"
  },
  {
    "path": "packages/nuxt/.gitignore",
    "chars": 543,
    "preview": "# Dependencies\nnode_modules\n\n# Logs\n*.log*\n\n# Temp directories\n.temp\n.tmp\n.cache\n\n# Yarn\n**/.yarn/cache\n**/.yarn/*state*"
  },
  {
    "path": "packages/nuxt/Readme.md",
    "chars": 60,
    "preview": "Reference [vue-scan](https://github.com/zcf0508/vue-scan) .\n"
  },
  {
    "path": "packages/nuxt/package.json",
    "chars": 1444,
    "preview": "{\n  \"name\": \"z-vue-scan-nuxt-module\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"z-vue-scan Nuxt modul"
  },
  {
    "path": "packages/nuxt/playground/app.vue",
    "chars": 191,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst b = ref('')\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ b"
  },
  {
    "path": "packages/nuxt/playground/components/HelloWorld.vue",
    "chars": 197,
    "preview": "<script setup lang=\"ts\">\nconst msg = defineModel('msg', {\n  default: '',\n})\n</script>\n\n<template>\n  <div>\n    <div>\n    "
  },
  {
    "path": "packages/nuxt/playground/nuxt.config.ts",
    "chars": 169,
    "preview": "export default defineNuxtConfig({\n  modules: ['../src/module'],\n  vueScan: {\n    enable: true,\n  },\n  devtools: { enable"
  },
  {
    "path": "packages/nuxt/playground/package.json",
    "chars": 238,
    "preview": "{\n  \"name\": \"z-vue-scan-nuxt-module-playground\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"nuxi "
  },
  {
    "path": "packages/nuxt/playground/server/tsconfig.json",
    "chars": 49,
    "preview": "{\n  \"extends\": \"../.nuxt/tsconfig.server.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/playground/tsconfig.json",
    "chars": 41,
    "preview": "{\n  \"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/src/module.ts",
    "chars": 891,
    "preview": "import type { VueScanBaseOptions } from 'z-vue-scan'\nimport process from 'node:process'\nimport { addPluginTemplate, defi"
  },
  {
    "path": "packages/nuxt/src/runtime/plugin.ts",
    "chars": 152,
    "preview": "import { defineNuxtPlugin } from '#app'\n\nexport default defineNuxtPlugin((_nuxtApp) => {\n  console.log('Plugin injected "
  },
  {
    "path": "packages/nuxt/src/runtime/server/tsconfig.json",
    "chars": 55,
    "preview": "{\n  \"extends\": \"../../../.nuxt/tsconfig.server.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/test/basic.test.ts",
    "chars": 463,
    "preview": "import { fileURLToPath } from 'node:url'\nimport { $fetch, setup } from '@nuxt/test-utils/e2e'\nimport { describe, expect,"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/app.vue",
    "chars": 68,
    "preview": "<script setup>\n</script>\n\n<template>\n  <div>basic</div>\n</template>\n"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/nuxt.config.ts",
    "chars": 113,
    "preview": "import MyModule from '../../../src/module'\n\nexport default defineNuxtConfig({\n  modules: [\n    MyModule,\n  ],\n})\n"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/package.json",
    "chars": 61,
    "preview": "{\n  \"name\": \"basic\",\n  \"type\": \"module\",\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/nuxt/tsconfig.json",
    "chars": 110,
    "preview": "{\n  \"extends\": \"./.nuxt/tsconfig.json\",\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"playground\"\n  ]\n}\n"
  },
  {
    "path": "patches/@vue__devtools-kit.patch",
    "chars": 18620,
    "preview": "diff --git a/dist/index.cjs b/dist/index.cjs\nindex 0004140e1a340e020f3227a4c0eb63ae04ee7a4c..70ad9a77e82fc0605f21064c7ba"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 40,
    "preview": "packages:\n  - examples/*\n  - packages/*\n"
  },
  {
    "path": "src/auto.ts",
    "chars": 7547,
    "preview": "import type { VNodeNormalizedChildren } from 'vue-demi'\nimport { throttle } from 'lodash-es'\nimport { type BACE_VUE_INST"
  },
  {
    "path": "src/core/fps.ts",
    "chars": 925,
    "preview": "export type RenderPhase = 'mount' | 'update'\n\nexport interface RenderMeta {\n  phase: RenderPhase\n  renderTime?: number\n "
  },
  {
    "path": "src/core/highlight.ts",
    "chars": 9052,
    "preview": "import type { RenderMeta } from './fps'\nimport { throttle } from 'lodash-es'\nimport { getComponentBoundingRect, getInsta"
  },
  {
    "path": "src/core/hook.ts",
    "chars": 3772,
    "preview": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport { getCurrentFps } from './fps'\nimport {\n  clearhighlight,"
  },
  {
    "path": "src/core/index.ts",
    "chars": 73,
    "preview": "export * from './fps'\nexport * from './highlight'\nexport * from './hook'\n"
  },
  {
    "path": "src/core/utils.ts",
    "chars": 4199,
    "preview": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport { basename, classify } from '@vue/devtools-shared'\n\ninter"
  },
  {
    "path": "src/global.d.ts",
    "chars": 455,
    "preview": "declare global {\n  interface Window {\n    __VUE_SCAN__?: {\n      plugin: typeof import('./index').default\n      createOn"
  },
  {
    "path": "src/index.ts",
    "chars": 1656,
    "preview": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport type { Plugin } from 'vue-demi'\nimport type { VueScanBase"
  },
  {
    "path": "src/index_vue2.ts",
    "chars": 1444,
    "preview": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport type { Plugin } from 'vue-demi'\nimport type { VueScanBase"
  },
  {
    "path": "src/types.ts",
    "chars": 262,
    "preview": "import type { HighlightCanvasOptions } from './core'\n\nexport interface VueScanBaseOptions extends HighlightCanvasOptions"
  },
  {
    "path": "src/utils/MutationObserverDom.ts",
    "chars": 644,
    "preview": "import { throttle } from 'lodash-es'\n\nexport function createDomMutationObserver<T extends Element>(\n  getTarget: () => T"
  },
  {
    "path": "src/utils.ts",
    "chars": 194,
    "preview": "export function isDev() {\n  return (import.meta.env && import.meta.env.DEV === true)\n    // eslint-disable-next-line nod"
  },
  {
    "path": "tsconfig.json",
    "chars": 447,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"moduleDetection\": \"force\",\n    \"root"
  },
  {
    "path": "uno.config.ts",
    "chars": 76,
    "preview": "import config from './packages/extension/uno.config'\n\nexport default config\n"
  }
]

About this extraction

This page contains the full source code of the zcf0508/vue-scan GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 97 files (128.6 KB), approximately 37.0k tokens, and a symbol index with 115 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!