[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [antfu]\nko_fi: huali58081\ncustom: ['https://afdian.com/a/huali08']\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n\n  pull_request:\n    branches:\n      - master\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20.x\n\n      - name: Setup\n        run: npm i -g pnpm@9\n\n      - name: Install\n        run: pnpm i\n\n      - name: Lint\n        run: pnpm run lint\n\n      - name: Typecheck devpilot plugin\n        run: pnpm -C packages/devpilot-plugin-vue-scan run typecheck\n\n      # - name: Test\n      #   run: pnpm run test\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-node@v5\n        with:\n          node-version: lts/*\n          registry-url: 'https://registry.npmjs.org'\n\n      - run: npm i -g npm@latest\n\n      - run: npx changelogithub\n        env:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n\n      - name: Setup\n        run: npm i -g pnpm@9\n\n      - name: Install Dependencies\n        run: pnpm i\n\n      - name: PNPM build\n        run: pnpm run build\n\n      - name: Publish vue scan to NPM\n        run: pnpm publish --access public --no-git-checks\n\n      # publish devpilot plugin\n\n      - name: Build devpilot plugin\n        working-directory: packages/devpilot-plugin-vue-scan\n        run: pnpm run build\n\n      - name: Publish devpilot plugin to NPM\n        working-directory: packages/devpilot-plugin-vue-scan\n        run: pnpm publish --access public --no-git-checks\n\n      # publish nuxt module\n\n      - name: Prepare vue scan nuxt module\n        working-directory: packages/nuxt\n        run: pnpm run dev:prepare\n\n      - name: Build vue scan nuxt module\n        working-directory: packages/nuxt\n        run: pnpm run prepack\n\n      - name: Publish vue scan nuxt module to NPM\n        working-directory: packages/nuxt\n        run: pnpm publish --access public --no-git-checks\n\n  createrelease:\n    name: Create Release\n    runs-on: [ubuntu-latest]\n    steps:\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: Release ${{ github.ref }}\n          draft: false\n          prerelease: false\n      - name: Output Release URL File\n        run: echo \"${{ steps.create_release.outputs.upload_url }}\" > release_url.txt\n      - name: Save Release URL File for publish\n        uses: actions/upload-artifact@v4\n        with:\n          name: release_url\n          path: release_url.txt\n\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20.x\n\n      - name: Setup\n        run: npm i -g pnpm@9\n\n      - name: Install\n        run: pnpm i\n\n      - name: Build vue scan\n        run: pnpm run build\n\n      - name: Build Extension\n        run: pnpm -C packages/extension run zip\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Load Release URL File from release job\n        uses: actions/download-artifact@v4\n        with:\n          name: release_url\n\n      - name: Get Release File Name & Upload URL\n        id: get_release_info\n        shell: bash\n        run: |\n          value=`cat release_url.txt`\n          echo ::set-output name=upload_url::$value\n\n      - name: Set env\n        shell: bash\n        run: echo \"RELEASE_VERSION=${GITHUB_REF#refs/tags/v}\" >> $GITHUB_ENV\n\n      - name: Upload Release Asset\n        id: upload-release-asset\n        uses: softprops/action-gh-release@v2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          files: |\n            ./packages/extension/.output/vue-scan-ext-${{ env.RELEASE_VERSION }}-chrome.zip\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n\ndist\ntest/**/pnpm-lock.yaml\npackage.back.json\ncache.json\npackages/extension/public/auto.[c|m]js"
  },
  {
    "path": "README.md",
    "content": "# 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 when it will update.\n\n[![NPM version](https://img.shields.io/npm/v/z-vue-scan?color=a1b858&label=)](https://www.npmjs.com/package/z-vue-scan)\n\n## Features\n\n- 🎯 Works with both Vue 2 and Vue 3\n- 🔄 Powered by [vue-demi](https://github.com/vueuse/vue-demi)\n- 📦 Lightweight\n- 💪 Written in TypeScript\n\n## Installation\n\n```bash\n# npm\nnpm install z-vue-scan\n\n# yarn\nyarn add z-vue-scan\n\n# pnpm\npnpm add z-vue-scan\n```\n\n## Usage\n\n```ts\ninterface Options {\n  enable?: boolean\n  hideCompnentName?: boolean\n}\n```\n\n### Vue 3\n\n```ts\n// vue3\nimport { createApp } from 'vue'\nimport VueScan, { type VueScanOptions } from 'z-vue-scan'\n\nimport App from './App.vue'\n\nconst isProduction = import.meta.env.PROD // or `process.env.NODE_ENV === 'production'`\n\nconst app = createApp(App)\n\nif (!isProduction) {\n  app.use<VueScanOptions>(VueScan, {})\n}\n\napp.mount('#app')\n```\n\n### Vue 2\n\n```ts\n// vue2\nimport Vue from 'vue'\nimport VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'\nimport App from './App.vue'\n\nconst isProduction = import.meta.env.PROD // or `process.env.NODE_ENV === 'production'`\n\nif (!isProduction) {\n  Vue.use<VueScanBaseOptions>(VueScan, {})\n}\n\nnew Vue({\n  render: h => h(App),\n}).$mount('#app')\n```\n\n### Nuxt Module\n\n```bash\n# npm\nnpm install z-vue-scan-nuxt-module\n\n# yarn\nyarn add z-vue-scan-nuxt-module\n\n# pnpm\npnpm add z-vue-scan-nuxt-module\n```\n\nYou can use z-vue-scan in your Nuxt project by adding it to the `modules` section in your `nuxt.config.ts`:\n\n```ts\nexport default defineNuxtConfig({\n  modules: ['z-vue-scan-nuxt-module'],\n  vueScan: {\n    // options\n    enable: true,\n    hideCompnentName: false\n  }\n})\n```\n\n### DevPilot Plugin (MCP for LLMs)\n\n```bash\npnpm add unplugin-devpilot devpilot-plugin-vue-scan -D\n```\n\nThe [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.\n\nSee [devpilot-plugin-vue-scan README](./packages/devpilot-plugin-vue-scan/README.md) for details.\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Run development server with Vue 3 example\npnpm dev\n\n# Run development server with Vue 2 example\npnpm dev:vue2\n\n# Build the package\npnpm build\n\n# Run type check\npnpm typecheck\n\n# Run linting\npnpm lint\n```\n\n## License\n\n[MIT](./LICENSE) License  2024 [zcf0508](https://github.com/zcf0508)\n"
  },
  {
    "path": "build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild'\n\nexport default defineBuildConfig(\n  [\n    {\n      entries: [\n        'src/index',\n        'src/index_vue2',\n      ],\n      rollup: {\n        emitCJS: true,\n        inlineDependencies: true,\n        json: {\n          compact: true,\n          namedExports: false,\n          preferConst: false,\n        },\n        commonjs: {\n          requireReturnsDefault: 'auto',\n        },\n        dts: {\n          respectExternal: false,\n        },\n      },\n      externals: ['vue-demi'],\n      clean: true,\n      declaration: true,\n    },\n    {\n      entries: [\n        'src/auto',\n      ],\n      outDir: 'packages/extension/public',\n      rollup: {\n        emitCJS: true,\n        output: {\n          format: 'iife',\n        },\n      },\n      externals: ['vue-demi'],\n      clean: true,\n      failOnWarn: false,\n    },\n  ],\n)\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  ignores: [\n    'docs',\n    'dist',\n    'packages/extension/.output',\n    'packages/extension/.wxt',\n    'packages/extension/public',\n  ],\n}, [\n  {\n    rules: {\n      'no-console': ['warn'],\n    },\n  },\n])\n"
  },
  {
    "path": "examples/vue2/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode\n!.vscode/extensions.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "examples/vue2/README.md",
    "content": "# vue2\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup\n\n[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).\n\n## Type Support for `.vue` Imports in TS\n\nTypeScript 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.\n\nIf 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:\n\n1. Disable the built-in TypeScript Extension\n    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette\n    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`\n2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.\n\n## Customize configuration\n\nSee [Vite Configuration Reference](https://vitejs.dev/config/).\n\n## Project Setup\n\n```sh\nnpm install\n```\n\n### Compile and Hot-Reload for Development\n\n```sh\nnpm run dev\n```\n\n### Type-Check, Compile and Minify for Production\n\n```sh\nnpm run build\n```\n"
  },
  {
    "path": "examples/vue2/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vue2/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite App</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/vue2/package.json",
    "content": "{\n  \"name\": \"vue2\",\n  \"version\": \"0.0.37\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p type-check build-only\",\n    \"preview\": \"vite preview --port 4173\",\n    \"build-only\": \"vite build\",\n    \"type-check\": \"vue-tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^2.7.7\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^16.11.45\",\n    \"@vitejs/plugin-legacy\": \"^2.0.0\",\n    \"@vitejs/plugin-vue2\": \"^1.1.2\",\n    \"@vue/composition-api\": \"^1.7.2\",\n    \"@vue/tsconfig\": \"^0.1.3\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"terser\": \"^5.14.2\",\n    \"typescript\": \"~4.7.4\",\n    \"vite\": \"^3.0.2\",\n    \"vue-tsc\": \"^0.38.8\",\n    \"z-vue-scan\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "examples/vue2/src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport HelloWorld from './components/HelloWorld.vue'\n\nconst b = ref('')\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ b }}\n    </div>\n    <HelloWorld :value=\"b\" @update:value=\"(v) => b = v\" />\n  </div>\n</template>\n"
  },
  {
    "path": "examples/vue2/src/assets/base.css",
    "content": "/* color palette from <https://github.com/vuejs/theme> */\n:root {\n  --vt-c-white: #ffffff;\n  --vt-c-white-soft: #f8f8f8;\n  --vt-c-white-mute: #f2f2f2;\n\n  --vt-c-black: #181818;\n  --vt-c-black-soft: #222222;\n  --vt-c-black-mute: #282828;\n\n  --vt-c-indigo: #2c3e50;\n\n  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);\n  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);\n  --vt-c-divider-dadarkrk-1: rgba(84, 84, 84, 0.65);\n  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);\n\n  --vt-c-text-light-1: var(--vt-c-indigo);\n  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);\n  --vt-c-text-dark-1: var(--vt-c-white);\n  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);\n}\n\n/* semantic color variables for this project */\n:root {\n  --color-background: var(--vt-c-white);\n  --color-background-soft: var(--vt-c-white-soft);\n  --color-background-mute: var(--vt-c-white-mute);\n\n  --color-border: var(--vt-c-divider-light-2);\n  --color-border-hover: var(--vt-c-divider-light-1);\n\n  --color-heading: var(--vt-c-text-light-1);\n  --color-text: var(--vt-c-text-light-1);\n\n  --section-gap: 160px;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --color-background: var(--vt-c-black);\n    --color-background-soft: var(--vt-c-black-soft);\n    --color-background-mute: var(--vt-c-black-mute);\n\n    --color-border: var(--vt-c-divider-dark-2);\n    --color-border-hover: var(--vt-c-divider-dark-1);\n\n    --color-heading: var(--vt-c-text-dark-1);\n    --color-text: var(--vt-c-text-dark-2);\n  }\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  position: relative;\n  font-weight: normal;\n}\n\nbody {\n  min-height: 100vh;\n  color: var(--color-text);\n  background: var(--color-background);\n  transition: color 0.5s, background-color 0.5s;\n  line-height: 1.6;\n  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n  font-size: 15px;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n"
  },
  {
    "path": "examples/vue2/src/assets/main.css",
    "content": "@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.green {\n  text-decoration: none;\n  color: hsla(160, 100%, 37%, 1);\n  transition: 0.4s;\n}\n\n@media (hover: hover) {\n  a:hover {\n    background-color: hsla(160, 100%, 37%, 0.2);\n  }\n}\n\n@media (min-width: 1024px) {\n  body {\n    display: flex;\n    place-items: center;\n  }\n\n  #app {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    padding: 0 2rem;\n  }\n}\n"
  },
  {
    "path": "examples/vue2/src/components/HelloWorld.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue'\n\nexport default defineComponent({\n  name: 'HelloWorld',\n  props: {\n    value: String,\n  },\n  emits: ['update:value'],\n  methods: {\n    handleInput(e: Event) {\n      this.$emit('update:value', (e.target as HTMLInputElement)?.value)\n    },\n  },\n})\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ $props.value }}\n    </div>\n    input: <input :value=\"$props.value\" @input=\"handleInput\">\n  </div>\n</template>\n"
  },
  {
    "path": "examples/vue2/src/main.ts",
    "content": "import Vue from 'vue'\nimport VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'\nimport App from './App.vue'\n\nimport './assets/main.css'\n\nVue.use<VueScanBaseOptions>(VueScan, {})\n\nnew Vue({\n  render: h => h(App),\n}).$mount('#app')\n"
  },
  {
    "path": "examples/vue2/tsconfig.config.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"vite.config.*\", \"vitest.config.*\", \"cypress.config.*\"]\n}\n"
  },
  {
    "path": "examples/vue2/tsconfig.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.config.json\"\n    }\n  ],\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"]\n}\n"
  },
  {
    "path": "examples/vue2/vite.config.ts",
    "content": "import { fileURLToPath, URL } from 'node:url'\n\nimport legacy from '@vitejs/plugin-legacy'\nimport vue2 from '@vitejs/plugin-vue2'\nimport { defineConfig } from 'vite'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    vue2(),\n    legacy({\n      targets: ['ie >= 11'],\n      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],\n    }),\n  ],\n  resolve: {\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url)),\n    },\n  },\n})\n"
  },
  {
    "path": "examples/vue3/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\ncoverage\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n*.tsbuildinfo\n"
  },
  {
    "path": "examples/vue3/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"Vue.volar\"]\n}\n"
  },
  {
    "path": "examples/vue3/README.md",
    "content": "# vue3\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup\n\n[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).\n\n## Type Support for `.vue` Imports in TS\n\nTypeScript 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.\n\n## Customize configuration\n\nSee [Vite Configuration Reference](https://vite.dev/config/).\n\n## Project Setup\n\n```sh\nnpm install\n```\n\n### Compile and Hot-Reload for Development\n\n```sh\nnpm run dev\n```\n\n### Type-Check, Compile and Minify for Production\n\n```sh\nnpm run build\n```\n"
  },
  {
    "path": "examples/vue3/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/vue3/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Vite App</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/vue3/package.json",
    "content": "{\n  \"name\": \"vue3\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p type-check \\\"build-only {@}\\\" --\",\n    \"preview\": \"vite preview\",\n    \"build-only\": \"vite build\",\n    \"type-check\": \"vue-tsc --build --force\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^3.5.12\"\n  },\n  \"devDependencies\": {\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"@types/node\": \"^22.9.0\",\n    \"@vitejs/plugin-vue\": \"^5.1.4\",\n    \"@vue/tsconfig\": \"^0.5.1\",\n    \"npm-run-all2\": \"^7.0.1\",\n    \"typescript\": \"~5.6.3\",\n    \"vite\": \"^5.4.10\",\n    \"vite-plugin-vue-devtools\": \"^7.5.4\",\n    \"vue-tsc\": \"^2.1.10\",\n    \"z-vue-scan\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "examples/vue3/src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport HelloWorld from './components/HelloWorld.vue'\n\nconst b = ref('')\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ b }}\n    </div>\n    <HelloWorld v-model:msg=\"b\" />\n  </div>\n</template>\n"
  },
  {
    "path": "examples/vue3/src/assets/base.css",
    "content": "/* color palette from <https://github.com/vuejs/theme> */\n:root {\n  --vt-c-white: #ffffff;\n  --vt-c-white-soft: #f8f8f8;\n  --vt-c-white-mute: #f2f2f2;\n\n  --vt-c-black: #181818;\n  --vt-c-black-soft: #222222;\n  --vt-c-black-mute: #282828;\n\n  --vt-c-indigo: #2c3e50;\n\n  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);\n  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);\n  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);\n  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);\n\n  --vt-c-text-light-1: var(--vt-c-indigo);\n  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);\n  --vt-c-text-dark-1: var(--vt-c-white);\n  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);\n}\n\n/* semantic color variables for this project */\n:root {\n  --color-background: var(--vt-c-white);\n  --color-background-soft: var(--vt-c-white-soft);\n  --color-background-mute: var(--vt-c-white-mute);\n\n  --color-border: var(--vt-c-divider-light-2);\n  --color-border-hover: var(--vt-c-divider-light-1);\n\n  --color-heading: var(--vt-c-text-light-1);\n  --color-text: var(--vt-c-text-light-1);\n\n  --section-gap: 160px;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --color-background: var(--vt-c-black);\n    --color-background-soft: var(--vt-c-black-soft);\n    --color-background-mute: var(--vt-c-black-mute);\n\n    --color-border: var(--vt-c-divider-dark-2);\n    --color-border-hover: var(--vt-c-divider-dark-1);\n\n    --color-heading: var(--vt-c-text-dark-1);\n    --color-text: var(--vt-c-text-dark-2);\n  }\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  font-weight: normal;\n}\n\nbody {\n  min-height: 100vh;\n  color: var(--color-text);\n  background: var(--color-background);\n  transition:\n    color 0.5s,\n    background-color 0.5s;\n  line-height: 1.6;\n  font-family:\n    Inter,\n    -apple-system,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    Oxygen,\n    Ubuntu,\n    Cantarell,\n    'Fira Sans',\n    'Droid Sans',\n    'Helvetica Neue',\n    sans-serif;\n  font-size: 15px;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n"
  },
  {
    "path": "examples/vue3/src/assets/main.css",
    "content": "@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.green {\n  text-decoration: none;\n  color: hsla(160, 100%, 37%, 1);\n  transition: 0.4s;\n  padding: 3px;\n}\n\n@media (hover: hover) {\n  a:hover {\n    background-color: hsla(160, 100%, 37%, 0.2);\n  }\n}\n\n@media (min-width: 1024px) {\n  body {\n    display: flex;\n    place-items: center;\n  }\n\n  #app {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    padding: 0 2rem;\n  }\n}\n"
  },
  {
    "path": "examples/vue3/src/components/HelloWorld.vue",
    "content": "<script setup lang=\"ts\">\nconst msg = defineModel('msg', {\n  default: '',\n})\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ msg }}\n    </div>\n    input: <input v-model=\"msg\">\n  </div>\n</template>\n"
  },
  {
    "path": "examples/vue3/src/main.ts",
    "content": "import { createApp } from 'vue'\nimport VueScan, { type VueScanOptions } from 'z-vue-scan'\n\nimport App from './App.vue'\nimport './assets/main.css'\n\nconst app = createApp(App)\napp.use<VueScanOptions>(VueScan, {})\napp.mount('#app')\n"
  },
  {
    "path": "examples/vue3/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\"src/**/__tests__/*\"]\n}\n"
  },
  {
    "path": "examples/vue3/tsconfig.json",
    "content": "{\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    }\n  ],\n  \"files\": []\n}\n"
  },
  {
    "path": "examples/vue3/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"types\": [\"node\"],\n    \"noEmit\": true\n  },\n  \"include\": [\n    \"vite.config.*\",\n    \"vitest.config.*\",\n    \"cypress.config.*\",\n    \"nightwatch.conf.*\",\n    \"playwright.config.*\"\n  ]\n}\n"
  },
  {
    "path": "examples/vue3/vite.config.ts",
    "content": "import { fileURLToPath, URL } from 'node:url'\n\nimport vue from '@vitejs/plugin-vue'\nimport { defineConfig } from 'vite'\nimport vueDevTools from 'vite-plugin-vue-devtools'\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [\n    vue(),\n    vueDevTools(),\n  ],\n  resolve: {\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url)),\n    },\n  },\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"z-vue-scan\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"The component will flash with a red border when it will update.\",\n  \"author\": \"zcf0508\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/zcf0508/vue-scan#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/zcf0508/vue-scan.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/zcf0508/vue-scan/issues\"\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./vue2\": {\n      \"types\": \"./dist/index_vue2.d.mts\",\n      \"import\": \"./dist/index_vue2.mjs\",\n      \"require\": \"./dist/index_vue2.cjs\"\n    }\n  },\n  \"main\": \"dist/index.cjs\",\n  \"module\": \"dist/index.mjs\",\n  \"types\": \"dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"lint\": \"eslint .\",\n    \"dev\": \"pnpm -C examples/vue3 dev\",\n    \"dev:vue2\": \"pnpm -C examples/vue2 dev\",\n    \"build\": \"unbuild\",\n    \"typecheck\": \"tsc\",\n    \"release\": \"bumpp -r\"\n  },\n  \"peerDependencies\": {\n    \"@vue/composition-api\": \"^1.0.0-rc.1\",\n    \"vue\": \"^2.0.0 || >=3.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@vue/composition-api\": {\n      \"optional\": true\n    }\n  },\n  \"dependencies\": {\n    \"vue-demi\": \"latest\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^3.9.2\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^22.10.0\",\n    \"@vue/devtools-kit\": \"^7.6.4\",\n    \"@vue/devtools-shared\": \"^7.6.7\",\n    \"bumpp\": \"^9.8.1\",\n    \"consola\": \"^3.2.3\",\n    \"lodash-es\": \"^4.17.21\",\n    \"typescript\": \"~5.6.3\",\n    \"unbuild\": \"^2.0.0\",\n    \"vue\": \"^3.0.0\"\n  },\n  \"pnpm\": {\n    \"patchedDependencies\": {\n      \"@vue/devtools-kit\": \"patches/@vue__devtools-kit.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/README.md",
    "content": "# devpilot-plugin-vue-scan\n\nA [DevPilot](https://github.com/zcf0508/unplugin-devpilot) plugin that exposes Vue component render performance data to LLMs via MCP.\n\n[![NPM version](https://img.shields.io/npm/v/devpilot-plugin-vue-scan?color=a1b858&label=)](https://www.npmjs.com/package/devpilot-plugin-vue-scan)\n\n## What It Does\n\n- Injects hooks into Vue 2/3 component `beforeUpdate` lifecycle to track re-renders in real time\n- Stores update events server-side in a circular buffer (10,000 events max)\n- Provides a `queryVueScanData` MCP tool that returns **per-component aggregated summaries** with source code locations\n- Enables LLMs to analyze render performance, identify hot components, and pinpoint the source file\n\n## Installation\n\n```bash\npnpm add devpilot-plugin-vue-scan\n```\n\n## Usage\n\nRegister as a DevPilot plugin in your Vite config:\n\n```ts\nimport vueScanPlugin from 'devpilot-plugin-vue-scan'\nimport DevPilot from 'unplugin-devpilot/vite'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [\n    DevPilot({\n      plugins: [vueScanPlugin],\n    }),\n  ],\n})\n```\n\n## How It Works\n\n1. **Start recording** — Press `Ctrl+Shift+R` or click the control panel's Start button\n2. **Interact with the page** — Component updates are captured automatically\n3. **Query via MCP** — LLM calls `queryVueScanData` to get aggregated performance data\n\n## MCP Tool: `queryVueScanData`\n\nReturns per-component render performance summaries.\n\n### Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `limit` | number | 50 | Max components to return |\n| `componentName` | string | - | Filter by name (partial match) |\n| `timeRange` | `{ start, end }` | - | Filter by timestamp range (ms) |\n| `minUpdateCount` | number | - | Only components updated ≥ N times |\n| `onlyInViewport` | boolean | - | Exclude off-screen renders |\n| `onlyUserComponents` | boolean | true | Filter out library components |\n| `includeRawEvents` | boolean | false | Include individual event details |\n| `sortBy` | string | totalUpdates | `totalUpdates` / `updatesPerSecond` / `componentName` |\n\n### Response\n\n```jsonc\n{\n  \"components\": [\n    {\n      \"componentName\": \"UserList\",\n      \"sourceLocation\": \"src/views/users/UserList.vue:25:5:div\",\n      \"totalUpdates\": 42,\n      \"updatesPerSecond\": 14.0,\n      \"firstUpdate\": 1772592779510,\n      \"lastUpdate\": 1772592782451\n    }\n  ],\n  \"metadata\": {\n    \"recordingStatus\": \"active\",\n    \"totalEvents\": 997,\n    \"uniqueComponents\": 20,\n    \"bufferSize\": 997,\n    \"bufferCapacity\": 10000,\n    \"timeRange\": { \"start\": 1772592779510, \"end\": 1772592782453 }\n  }\n}\n```\n\n### User vs Library Components\n\nComponents 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.\n\n## Control Panel\n\nA floating panel at the bottom-right corner of the page provides:\n\n- **Start/Pause** recording toggle\n- **Clear** recorded data\n- **Event counter** showing buffer usage\n- **Keyboard shortcuts**: `Ctrl+Shift+V` (toggle panel), `Ctrl+Shift+R` (toggle recording)\n\n## License\n\n[MIT](../../LICENSE)\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/package.json",
    "content": "{\n  \"name\": \"devpilot-plugin-vue-scan\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"Vue Scan plugin for DevPilot\",\n  \"author\": \"zcf0508 <zcf0508@live.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/zcf0508/vue-scan.git\"\n  },\n  \"exports\": {\n    \".\": \"./dist/index.mjs\",\n    \"./client\": \"./dist/client/index.mjs\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./dist/*.d.ts\",\n        \"./*\"\n      ]\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"dev\": \"tsdown --watch\",\n    \"typecheck:client\": \"tsc --project tsconfig.client.json --noEmit\",\n    \"typecheck:node\": \"tsc --noEmit\",\n    \"typecheck\": \"pnpm run typecheck:client && pnpm run typecheck:node\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.25.3\",\n    \"@types/ws\": \"^8.18.1\",\n    \"birpc\": \"^4.0.0\",\n    \"es-toolkit\": \"^1.44.0\",\n    \"hookable\": \"^6.0.1\",\n    \"mitt\": \"^3.0.1\",\n    \"unplugin\": \"^3.0.0\",\n    \"unplugin-devpilot\": \"^0.0.11\",\n    \"ws\": \"^8.19.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"tsdown\": \"^0.20.1\"\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/control-panel.ts",
    "content": "import type { DevpilotClient } from 'unplugin-devpilot/client'\nimport type { VueScanServerMethods } from './types'\n\n// Extend global interface for storing listener reference\ndeclare global {\n  interface Document {\n    __vueScanKeydownListener?: (e: KeyboardEvent) => void\n  }\n}\n\n// Update control panel UI\nfunction updatePanelContent(panel: HTMLDivElement, client: DevpilotClient<VueScanServerMethods>): void {\n  const runtime = window.__VUE_SCAN_RUNTIME__\n  const isRecording = runtime?.isRecording || false\n\n  panel.innerHTML = `\n    <div style=\"margin-bottom: 8px; font-weight: bold;\">\n      🔍 Vue Scan\n    </div>\n    <div style=\"margin-bottom: 8px; display: flex; align-items: center;\">\n      <span style=\"\n        width: 8px;\n        height: 8px;\n        border-radius: 50%;\n        background: ${isRecording ? '#00ff00' : '#666'};\n        margin-right: 6px;\n        display: inline-block;\n      \"></span>\n      <span>${isRecording ? 'Recording' : 'Paused'}</span>\n    </div>\n    <div style=\"margin-bottom: 8px; color: #888;\">\n      Events: <span id=\"event-count\">-</span>\n    </div>\n    <div style=\"display: flex; gap: 4px; flex-wrap: wrap;\">\n      <button id=\"toggle-btn\" style=\"\n        padding: 4px 8px;\n        border: none;\n        border-radius: 4px;\n        cursor: pointer;\n        background: ${isRecording ? '#ff6b6b' : '#51cf66'};\n        color: white;\n      \">\n        ${isRecording ? '⏸️ Pause' : '▶️ Start'}\n      </button>\n      <button id=\"clear-btn\" style=\"\n        padding: 4px 8px;\n        border: none;\n        border-radius: 4px;\n        cursor: pointer;\n        background: #495057;\n        color: white;\n      \">\n        🗑️ Clear\n      </button>\n      <button id=\"stats-btn\" style=\"\n        padding: 4px 8px;\n        border: none;\n        border-radius: 4px;\n        cursor: pointer;\n        background: #495057;\n        color: white;\n      \">\n        📊 Stats\n      </button>\n    </div>\n    <div style=\"margin-top: 8px; font-size: 10px; color: #666;\">\n      Ctrl+Shift+V: Toggle Panel<br>\n      Ctrl+Shift+R: Toggle Recording\n    </div>\n  `\n\n  // Bind events\n  const toggleBtn = panel.querySelector('#toggle-btn')\n  if (toggleBtn) {\n    toggleBtn.addEventListener('click', () => {\n      runtime?.toggleRecording()\n      updatePanelContent(panel, client)\n    })\n  }\n\n  const clearBtn = panel.querySelector('#clear-btn')\n  if (clearBtn) {\n    clearBtn.addEventListener('click', () => {\n      runtime?.clearData()\n    })\n  }\n\n  const statsBtn = panel.querySelector('#stats-btn')\n  if (statsBtn) {\n    statsBtn.addEventListener('click', async () => {\n      const data = await client.rpcCall('vue-scan:exportData')\n      // eslint-disable-next-line no-alert\n      alert(JSON.stringify(data, null, 2))\n    })\n  }\n}\n\n// Get current page's panel from DOM\nfunction getCurrentPanel(): HTMLDivElement | null {\n  return document.getElementById('vue-scan-control-panel') as HTMLDivElement | null\n}\n\n// Register keyboard shortcuts\nexport function registerKeyboardShortcuts(client: DevpilotClient<VueScanServerMethods>): void {\n  // Remove existing listener if any to avoid duplicates\n  if (document.__vueScanKeydownListener) {\n    document.removeEventListener('keydown', document.__vueScanKeydownListener)\n  }\n\n  const keydownListener = (e: KeyboardEvent) => {\n    // Ctrl+Shift+V: Toggle panel visibility\n    if (e.ctrlKey && e.shiftKey && e.key === 'V') {\n      const panel = getCurrentPanel()\n      if (panel) {\n        panel.style.display = panel.style.display === 'none' ? 'block' : 'none'\n        // eslint-disable-next-line no-console\n        console.log(`[Vue Scan] Panel ${panel.style.display === 'none' ? 'hidden' : 'shown'}`)\n      }\n      e.preventDefault()\n    }\n\n    // Ctrl+Shift+R: Toggle recording\n    if (e.ctrlKey && e.shiftKey && e.key === 'R') {\n      window.__VUE_SCAN_RUNTIME__?.toggleRecording()\n      const panel = getCurrentPanel()\n      if (panel) {\n        updatePanelContent(panel, client)\n      }\n      e.preventDefault()\n    }\n  }\n\n  document.addEventListener('keydown', keydownListener)\n  document.__vueScanKeydownListener = keydownListener\n\n  // eslint-disable-next-line no-console\n  console.log('[Vue Scan] Keyboard shortcuts registered (Ctrl+Shift+V/R)')\n}\n\n// Create control panel UI\nexport function createControlPanel(client: DevpilotClient<VueScanServerMethods>): HTMLDivElement | null {\n  // Check if panel already exists in current document\n  if (document.getElementById('vue-scan-control-panel')) {\n    return null\n  }\n\n  const panel = document.createElement('div')\n  panel.id = 'vue-scan-control-panel'\n  panel.style.cssText = `\n    position: fixed;\n    bottom: 20px;\n    right: 20px;\n    background: rgba(0, 0, 0, 0.9);\n    color: white;\n    padding: 12px;\n    border-radius: 8px;\n    font-family: monospace;\n    font-size: 12px;\n    z-index: 10000;\n    min-width: 200px;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.3);\n  `\n\n  document.body.appendChild(panel)\n  panel.style.display = 'none'\n  updatePanelContent(panel, client)\n\n  // Update event count periodically (only when panel is visible)\n  setInterval(async () => {\n    const currentPanel = getCurrentPanel()\n    if (!currentPanel || currentPanel.style.display === 'none') {\n      return\n    }\n    try {\n      const data = await client.rpcCall('vue-scan:exportData') as any\n      const countEl = currentPanel.querySelector('#event-count')\n      if (countEl && data) {\n        countEl.textContent = `${data.events?.length || 0} / 10000`\n      }\n    }\n    catch {\n      // Ignore errors\n    }\n  }, 1000)\n\n  // eslint-disable-next-line no-console\n  console.log('[Vue Scan] Control panel created')\n\n  return panel\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/fps.ts",
    "content": "const WINDOW_MS = 1000\n\nlet frameTimestamps: number[] = []\nlet rafId: number | null = null\n\nfunction tick() {\n  const now = performance.now()\n  frameTimestamps.push(now)\n\n  const cutoff = now - WINDOW_MS\n  while (frameTimestamps.length > 0 && frameTimestamps[0] < cutoff) {\n    frameTimestamps.shift()\n  }\n\n  rafId = requestAnimationFrame(tick)\n}\n\nfunction ensureRunning() {\n  if (rafId === null) {\n    rafId = requestAnimationFrame(tick)\n  }\n}\n\nexport function getCurrentFps(): number {\n  ensureRunning()\n  if (frameTimestamps.length < 2)\n    return -1\n  return Math.round(frameTimestamps.length * 1000 / WINDOW_MS)\n}\n\nexport function stopFpsMonitor(): void {\n  if (rafId !== null) {\n    cancelAnimationFrame(rafId)\n    rafId = null\n  }\n  frameTimestamps = []\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/helpers.ts",
    "content": "export interface ComponentBoundingRect {\n  top: number\n  left: number\n  width: number\n  height: number\n  right: number\n  bottom: number\n}\n\nconst DEFAULT_RECT: ComponentBoundingRect = {\n  top: 0,\n  left: 0,\n  right: 0,\n  bottom: 0,\n  width: 0,\n  height: 0,\n}\n\nexport function getInstanceName(instance: any): string {\n  const type = instance?.type || instance?.$vnode || {}\n  const name = type?.name || type?._componentTag || type?.tag\n    || type?.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ || type?.__name\n  if (name)\n    return name\n  if (instance?.root === instance || instance?.$root === instance)\n    return 'Root'\n\n  // guess from parent\n  for (const key in instance?.parent?.type?.components) {\n    if (instance.parent.type.components[key] === instance?.type)\n      return key\n  }\n  for (const key in instance?.appContext?.components) {\n    if (instance.appContext.components[key] === instance?.type)\n      return key\n  }\n\n  // from filename\n  const file = type?.__file\n  if (file) {\n    const base = file.split(/[/\\\\]/).pop() || ''\n    return base.replace(/\\.vue$/, '')\n  }\n\n  return 'Anonymous Component'\n}\n\nfunction isFragment(instance: any): boolean {\n  const subTreeType = instance?.subTree?.type\n  if (!subTreeType)\n    return false\n  const appRecord = instance?.__VUE_DEVTOOLS_NEXT_APP_RECORD__\n    || instance?.root?.appContext?.app?.__VUE_DEVTOOLS_NEXT_APP_RECORD__\n  if (appRecord)\n    return appRecord?.types?.Fragment === subTreeType\n  return false\n}\n\nfunction createRect() {\n  const rect = {\n    top: 0,\n    bottom: 0,\n    left: 0,\n    right: 0,\n    get width() { return rect.right - rect.left },\n    get height() { return rect.bottom - rect.top },\n  }\n  return rect\n}\n\nfunction mergeRects(a: any, b: any) {\n  if (!a.top || b.top < a.top)\n    a.top = b.top\n  if (!a.bottom || b.bottom > a.bottom)\n    a.bottom = b.bottom\n  if (!a.left || b.left < a.left)\n    a.left = b.left\n  if (!a.right || b.right > a.right)\n    a.right = b.right\n  return a\n}\n\nlet range: Range | null = null\nfunction getTextRect(node: any) {\n  if (!range)\n    range = document.createRange()\n  range.selectNode(node)\n  return range.getBoundingClientRect()\n}\n\nfunction getFragmentRect(vnode: any): ComponentBoundingRect {\n  const rect = createRect()\n  if (!vnode.children)\n    return rect\n  for (let i = 0; i < vnode.children.length; i++) {\n    const childVnode = vnode.children[i]\n    let childRect\n    if (childVnode.component) {\n      childRect = getComponentBoundingRect(childVnode.component)\n    }\n    else if (childVnode.el) {\n      const el = childVnode.el\n      if (el.nodeType === 1 || el.getBoundingClientRect)\n        childRect = el.getBoundingClientRect()\n      else if (el.nodeType === 3 && el.data.trim())\n        childRect = getTextRect(el)\n    }\n    if (childRect)\n      mergeRects(rect, childRect)\n  }\n  return rect\n}\n\nexport function getComponentBoundingRect(instance: any): ComponentBoundingRect {\n  const el = instance?.subTree?.el || instance?.$el\n  if (typeof window === 'undefined')\n    return DEFAULT_RECT\n  if (isFragment(instance))\n    return getFragmentRect(instance?.subTree)\n  if (el?.nodeType === 1)\n    return el.getBoundingClientRect()\n  if (instance?.subTree?.component || instance?.$vnode)\n    return getComponentBoundingRect(instance?.subTree?.component || instance?.$vnode)\n  return DEFAULT_RECT\n}\n\nexport function isInViewport(bounds: ComponentBoundingRect): boolean {\n  return !(\n    bounds.left >= window.innerWidth\n    || bounds.right <= 0\n    || bounds.top >= window.innerHeight\n    || bounds.bottom <= 0\n  )\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/index.ts",
    "content": "import type { VueScanClientRpc, VueScanServerMethods } from './types'\n// Client-side entry point for Vue Scan DevPilot Plugin\nimport { defineRpcHandlers, getDevpilotClient } from 'unplugin-devpilot/client'\nimport { createControlPanel, registerKeyboardShortcuts } from './control-panel'\nimport { initRuntimeControl } from './runtime-control'\nimport { injectVueScanWithRuntimeControl } from './vue-injector'\n\n// Define RPC handlers (currently empty as we don't need server-to-client RPC)\nexport const rpcHandlers: VueScanClientRpc = defineRpcHandlers<VueScanClientRpc>({\n  // Add client-side RPC handlers here if needed\n})\n\nfunction setup() {\n  const client = getDevpilotClient<VueScanServerMethods>()\n  if (!client) {\n    // Virtual module not initialized yet, retry\n    // eslint-disable-next-line no-console\n    console.log('[Vue Scan] Waiting for DevPilot client...')\n    setTimeout(setup, 100)\n    return\n  }\n\n  // eslint-disable-next-line no-console\n  console.log('[Vue Scan] DevPilot client found, connecting...')\n\n  // Track connection timeout\n  let connectionTimeout: ReturnType<typeof setTimeout> | null = null\n\n  // Wait for WebSocket connection to be ready\n  client.onConnected(() => {\n    if (connectionTimeout) {\n      clearTimeout(connectionTimeout)\n    }\n\n    // eslint-disable-next-line no-console\n    console.log('[Vue Scan] Connected to DevPilot server')\n\n    // 1. Initialize runtime control\n    initRuntimeControl(client)\n\n    // 2. Inject Vue monitoring with runtime control\n    injectVueScanWithRuntimeControl(client)\n\n    // 3. Create control panel\n    createControlPanel(client)\n\n    // 4. Register keyboard shortcuts (now that client is available)\n    registerKeyboardShortcuts(client)\n\n    // eslint-disable-next-line no-console\n    console.log(\n      '%c 🔍 Vue Scan DevPilot %c Ctrl+Shift+V → Toggle Panel | Ctrl+Shift+R → Toggle Recording ',\n      'background:#35495e;color:#fff;padding:2px 4px;border-radius:3px 0 0 3px;',\n      'background:#41b883;color:#fff;padding:2px 4px;border-radius:0 3px 3px 0;',\n    )\n  })\n\n  // Set connection timeout warning\n  connectionTimeout = setTimeout(() => {\n    // eslint-disable-next-line no-console\n    console.warn('[Vue Scan] WebSocket connection timeout - DevPilot server may not be running')\n  }, 5000)\n}\n\nsetup()\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/runtime-control.ts",
    "content": "import type { DevpilotClient } from 'unplugin-devpilot/client'\nimport type { RuntimeControl, VueScanServerMethods } from './types'\n\n// Initialize runtime control\nexport function initRuntimeControl(client: DevpilotClient<VueScanServerMethods>): RuntimeControl {\n  if (!window.__VUE_SCAN_RUNTIME__) {\n    window.__VUE_SCAN_RUNTIME__ = {\n      isRecording: false,\n      showHighlight: false,\n\n      startRecording() {\n        this.isRecording = true\n        this.showHighlight = true\n        client.rpcCall('vue-scan:startRecording').catch(() => {})\n        console.log('📊 Vue Scan: Recording started')\n      },\n\n      stopRecording() {\n        this.isRecording = false\n        this.showHighlight = false\n        client.rpcCall('vue-scan:stopRecording').catch(() => {})\n        console.log('⏸️ Vue Scan: Recording stopped')\n      },\n\n      toggleRecording() {\n        if (this.isRecording) {\n          this.stopRecording()\n        }\n        else {\n          this.startRecording()\n        }\n      },\n\n      clearData() {\n        client.rpcCall('vue-scan:clearData').catch(() => {})\n        console.log('🗑️ Vue Scan: Data cleared')\n      },\n    }\n  }\n\n  return window.__VUE_SCAN_RUNTIME__\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/types.ts",
    "content": "// Server-side RPC methods that client can call\nexport interface VueScanServerMethods {\n  'vue-scan:recordUpdate': (data: {\n    timestamp: number\n    componentName: string\n    componentId: string\n    phase: 'mount' | 'update'\n    renderTime?: number\n    fps: number\n    updateCount: number\n    bounds: {\n      width: number\n      height: number\n      top: number\n      left: number\n    }\n    isInViewport: boolean\n    isUserComponent: boolean\n    sourceLocation?: string\n  }) => Promise<void>\n  'vue-scan:startRecording': () => Promise<{ status: string }>\n  'vue-scan:stopRecording': () => Promise<{ status: string }>\n  'vue-scan:clearData': () => Promise<{ status: string }>\n  'vue-scan:exportData': () => Promise<unknown>\n}\n\n// Client-side RPC interface (methods that server can call on client)\nexport interface VueScanClientRpc {\n  // Add client-side RPC methods here if needed in the future\n}\n\nexport interface RuntimeControl {\n  isRecording: boolean\n  showHighlight: boolean\n  startRecording: () => void\n  stopRecording: () => void\n  toggleRecording: () => void\n  clearData: () => void\n}\n\n// Extend window interface\ndeclare global {\n  interface Window {\n    __VUE_SCAN_RUNTIME__?: RuntimeControl\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/client/vue-injector.ts",
    "content": "/**\n * Vue component injection for data collection.\n * Follows the same injection pattern as src/auto.ts,\n * adding a data-report hook that sends update events via RPC.\n */\nimport type { DevpilotClient } from 'unplugin-devpilot/client'\nimport type { VueScanServerMethods } from './types'\nimport { throttle } from 'lodash-es'\nimport { getCurrentFps } from './fps'\nimport { getComponentBoundingRect, getInstanceName, isInViewport } from './helpers'\n\ninterface VueInstance {\n  uid?: number\n  _uid?: number\n  $el?: HTMLElement\n  subTree?: {\n    el?: HTMLElement\n    type?: any\n    component?: VueInstance\n    children?: any\n  }\n  children?: any\n  $options?: any\n  $children?: VueInstance[]\n  $vnode?: { componentInstance?: VueInstance }\n  $set?: (target: any, key: string, value: any) => void\n  type?: any\n  root?: any\n  $root?: any\n  appContext?: any\n  parent?: any\n  __vue_scan_injected__?: boolean\n  __flashCount?: number\n  __flashTimeout?: ReturnType<typeof setTimeout> | null\n  __renderStartTime?: number | null\n  __dataReportHook?: (() => void) | null\n  __beforeUpdateHook?: (() => void) | null\n  __updatedHook?: (() => void) | null\n  bu?: Array<() => void> | null\n  u?: Array<() => void> | null\n  bum?: Array<() => void> | null\n}\n\nfunction getSourceLocation(instance: VueInstance): string | undefined {\n  // 1. Check data-insp-path on root element (injected by unplugin-devpilot)\n  const el = instance?.subTree?.el || instance.$el\n  if (el instanceof HTMLElement) {\n    const inspPath = el.getAttribute('data-insp-path')\n    if (inspPath)\n      return inspPath\n  }\n\n  // 2. Fallback to __file from Vue compiler\n  const file = instance?.type?.__file\n  if (file)\n    return file\n\n  return undefined\n}\n\nfunction isFromUserCode(source: string | undefined): boolean {\n  if (!source)\n    return false\n  return !source.includes('node_modules')\n}\n\nfunction sendReportEvent(\n  instance: VueInstance,\n  client: DevpilotClient<VueScanServerMethods>,\n  phase: 'mount' | 'update',\n  renderTime?: number,\n) {\n  const runtime = window.__VUE_SCAN_RUNTIME__\n  if (!runtime?.isRecording)\n    return\n\n  const name = getInstanceName(instance)\n  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')\n  const sourceLocation = getSourceLocation(instance)\n  const isUserComponent = isFromUserCode(sourceLocation)\n\n  if (!instance.__flashCount) {\n    instance.__flashCount = 0\n  }\n  instance.__flashCount++\n\n  const bounds = getComponentBoundingRect(instance)\n\n  client.rpcCall('vue-scan:recordUpdate', {\n    timestamp: Date.now(),\n    componentName: name,\n    componentId: uuid,\n    phase,\n    renderTime,\n    fps: getCurrentFps(),\n    updateCount: instance.__flashCount,\n    bounds: {\n      width: bounds.width,\n      height: bounds.height,\n      top: bounds.top,\n      left: bounds.left,\n    },\n    isInViewport: isInViewport(bounds),\n    isUserComponent,\n    sourceLocation,\n  }).catch(() => {})\n}\n\nfunction createBeforeUpdateHook(instance: VueInstance) {\n  return () => {\n    const runtime = window.__VUE_SCAN_RUNTIME__\n    if (!runtime?.isRecording)\n      return\n    instance.__renderStartTime = performance.now()\n  }\n}\n\nfunction createMountedReportHook(instance: VueInstance, client: DevpilotClient<VueScanServerMethods>) {\n  const el = instance?.subTree?.el || instance.$el\n  if (!el)\n    return\n\n  return () => {\n    sendReportEvent(instance, client, 'mount')\n  }\n}\n\nfunction createUpdatedReportHook(instance: VueInstance, client: DevpilotClient<VueScanServerMethods>) {\n  const el = instance?.subTree?.el || instance.$el\n  if (!el)\n    return\n\n  return () => {\n    const renderTime = instance.__renderStartTime != null\n      ? performance.now() - instance.__renderStartTime\n      : undefined\n    instance.__renderStartTime = null\n    sendReportEvent(instance, client, 'update', renderTime)\n  }\n}\n\nfunction injectVueScan(node: HTMLElement, client: DevpilotClient<VueScanServerMethods>) {\n  // @ts-expect-error vue internal\n  if (node.__vue_app__) {\n    // Vue 3\n    // @ts-expect-error vue internal\n    const vueApp = node.__vue_app__\n\n    // Register mixin to cover future-mounted components\n    if (!vueApp.__vue_scan_mixin_installed__) {\n      vueApp.mixin({\n        mounted(this: any) {\n          const instance = this.$ as VueInstance\n          if (!instance.__dataReportHook) {\n            instance.__dataReportHook = createMountedReportHook(instance, client)\n          }\n          if (!instance.__beforeUpdateHook) {\n            instance.__beforeUpdateHook = createBeforeUpdateHook(instance)\n          }\n          if (!instance.__updatedHook) {\n            instance.__updatedHook = createUpdatedReportHook(instance, client)\n          }\n          instance.__dataReportHook?.()\n        },\n        beforeUpdate(this: any) {\n          const instance = this.$ as VueInstance\n          instance.__beforeUpdateHook?.()\n        },\n        updated(this: any) {\n          const instance = this.$ as VueInstance\n          instance.__updatedHook?.()\n        },\n      })\n      vueApp.__vue_scan_mixin_installed__ = true\n    }\n\n    const vueInstance = vueApp._container._vnode.component as VueInstance\n\n    function mixinChildren(children: any) {\n      if (!children || typeof children === 'string' || !Array.isArray(children))\n        return\n\n      children.forEach((item: any) => {\n        if (typeof item !== 'object')\n          return\n        if (item && 'component' in item && item.component) {\n          mixin(item.component as VueInstance)\n        }\n        else if (item && 'children' in item) {\n          mixinChildren(item.children)\n        }\n      })\n    }\n\n    function mixin(instance: VueInstance) {\n      if (instance.subTree?.el && instance.__vue_scan_injected__ !== true) {\n        const beforeUpdate = createBeforeUpdateHook(instance)\n        const updated = createUpdatedReportHook(instance, client)\n\n        if (beforeUpdate) {\n          if (instance.bu) {\n            instance.bu.push(beforeUpdate)\n          }\n          else {\n            instance.bu = [beforeUpdate]\n          }\n        }\n\n        if (updated) {\n          if (instance.u) {\n            instance.u.push(updated)\n          }\n          else {\n            instance.u = [updated]\n          }\n        }\n\n        instance.__vue_scan_injected__ = true\n      }\n\n      if (!instance.subTree?.component && instance.subTree?.children) {\n        mixinChildren(instance.subTree.children)\n      }\n      else if (instance.subTree?.component) {\n        mixin(instance.subTree.component as VueInstance)\n      }\n      else if (!instance.subTree && instance.children) {\n        mixinChildren(instance.children)\n      }\n    }\n\n    mixin(vueInstance)\n    vueInstance.__vue_scan_injected__ = true\n  }\n  // @ts-expect-error vue internal\n  else if (node.__vue__) {\n    // Vue 2\n    // @ts-expect-error vue internal\n    const vueInstance = (node.__vue__?.$vnode?.componentInstance || node.__vue__) as VueInstance\n\n    function mixin(instance: VueInstance) {\n      if (instance?.$el && instance.__vue_scan_injected__ !== true) {\n        const beforeUpdate = createBeforeUpdateHook(instance)\n        const updated = createUpdatedReportHook(instance, client)\n\n        if (beforeUpdate) {\n          if (instance.$options?.beforeUpdate) {\n            const arr = [...instance.$options.beforeUpdate]\n            arr.push(beforeUpdate)\n            instance.$set!(instance.$options, 'beforeUpdate', arr)\n          }\n          else if (instance.$options) {\n            instance.$set!(instance.$options, 'beforeUpdate', [beforeUpdate])\n          }\n        }\n\n        if (updated) {\n          if (instance.$options?.updated) {\n            const arr = [...instance.$options.updated]\n            arr.push(updated)\n            instance.$set!(instance.$options, 'updated', arr)\n          }\n          else if (instance.$options) {\n            instance.$set!(instance.$options, 'updated', [updated])\n          }\n        }\n\n        instance.__vue_scan_injected__ = true\n      }\n\n      if (instance.$children) {\n        (instance.$children as Array<VueInstance>).forEach((child) => {\n          mixin(child)\n        })\n      }\n    }\n\n    mixin(vueInstance)\n    vueInstance.__vue_scan_injected__ = true\n  }\n}\n\nfunction getMountDoms() {\n  return Array.from(document.body.children).filter((element) => {\n    // @ts-expect-error vue internal\n    return !!(element.__vue_app__ || element.__vue__)\n  }) as HTMLElement[]\n}\n\nfunction createDomMutationObserver(\n  getTarget: () => HTMLElement | null,\n  callback: MutationCallback,\n  options: MutationObserverInit,\n  throttleWait: number,\n) {\n  const targetObserver = new MutationObserver(throttle(callback, throttleWait))\n\n  const findTargetObserver = new MutationObserver(throttle(() => {\n    const target = getTarget()\n    if (target) {\n      findTargetObserver.disconnect()\n      targetObserver.observe(target, options)\n    }\n  }, 200))\n\n  findTargetObserver.observe(document.body, {\n    childList: true,\n    subtree: true,\n  })\n\n  return targetObserver\n}\n\nexport function injectVueScanWithRuntimeControl(client: DevpilotClient<VueScanServerMethods>): void {\n  const vue2ObserverMap = new WeakMap<HTMLElement, MutationObserver>()\n  let vue3Found = false\n  let checkCount = 0\n  const maxChecks = 100 // ~60 seconds with 600ms throttle\n\n  // eslint-disable-next-line no-console\n  console.log('[Vue Scan] Starting Vue app detection...')\n\n  const documentObserver = new MutationObserver(throttle(() => {\n    const mountDoms = getMountDoms()\n    checkCount++\n\n    if (mountDoms.length === 0) {\n      if (checkCount === 1) {\n        // eslint-disable-next-line no-console\n        console.log('[Vue Scan] No Vue app found yet, waiting for mount...')\n      }\n      if (checkCount >= maxChecks) {\n        // eslint-disable-next-line no-console\n        console.warn('[Vue Scan] Timeout: No Vue app detected after 60s')\n        documentObserver.disconnect()\n      }\n      return\n    }\n\n    // eslint-disable-next-line no-console\n    console.log(`[Vue Scan] Found ${mountDoms.length} Vue mount point(s)`)\n\n    const isVue3 = mountDoms.some((mountDom) => {\n      // @ts-expect-error vue internal\n      return !!mountDom.__vue_app__\n    })\n\n    if (isVue3 && !vue3Found) {\n      // eslint-disable-next-line no-console\n      console.log('[Vue Scan] Vue 3 app detected, injecting scanner...')\n      vue3Found = true\n    }\n\n    if (isVue3) {\n      documentObserver.disconnect()\n    }\n\n    mountDoms.forEach((mountDom) => {\n      // @ts-expect-error vue internal\n      if (mountDom.__vue_app__) {\n        documentObserver.disconnect()\n        injectVueScan(mountDom, client)\n      }\n      else {\n        if (!vue2ObserverMap.get(mountDom)) {\n          // eslint-disable-next-line no-console\n          console.log('[Vue Scan] Vue 2 app detected, injecting scanner...')\n          vue2ObserverMap.set(mountDom, createDomMutationObserver(\n            () => mountDom,\n            () => injectVueScan(mountDom, client),\n            { childList: true, subtree: true },\n            600,\n          ))\n        }\n      }\n    })\n  }, 600))\n\n  documentObserver.observe(document.body, {\n    attributes: true,\n    childList: true,\n    subtree: true,\n  })\n\n  // Try immediate injection\n  const mountDoms = getMountDoms()\n  if (mountDoms.length > 0) {\n    // eslint-disable-next-line no-console\n    console.log(`[Vue Scan] ${mountDoms.length} Vue mount point(s) found immediately`)\n    mountDoms.forEach((mountDom) => {\n      injectVueScan(mountDom, client)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/data-store.ts",
    "content": "import type { ComponentSummary, ComponentUpdateEvent, QueryParams, QueryResult } from './types'\n\nfunction normalize(name: string): string {\n  return name\n    .replace(/[-_]/g, '')\n    .toLowerCase()\n}\n\nexport class VueScanDataStore {\n  private events: ComponentUpdateEvent[] = []\n  private maxEvents = 10000\n  public isRecording = false\n\n  addEvent(event: ComponentUpdateEvent) {\n    if (!this.isRecording)\n      return\n\n    if (this.events.length >= this.maxEvents) {\n      this.events.shift()\n    }\n\n    this.events.push(event)\n  }\n\n  query(params: QueryParams): QueryResult {\n    let filtered = [...this.events]\n\n    if (params.componentName) {\n      const normalizedQuery = normalize(params.componentName)\n      filtered = filtered.filter(e =>\n        normalize(e.componentName).includes(normalizedQuery),\n      )\n    }\n\n    if (params.timeRange && params.timeRange.start > 0 && params.timeRange.end > 0) {\n      filtered = filtered.filter(e =>\n        e.timestamp >= params.timeRange!.start\n        && e.timestamp <= params.timeRange!.end,\n      )\n    }\n\n    if (params.minUpdateCount) {\n      filtered = filtered.filter(e =>\n        e.updateCount >= params.minUpdateCount!,\n      )\n    }\n\n    if (params.minRenderTime) {\n      filtered = filtered.filter(e =>\n        e.renderTime != null && e.renderTime >= params.minRenderTime!,\n      )\n    }\n\n    if (params.onlyInViewport) {\n      filtered = filtered.filter(e => e.isInViewport)\n    }\n\n    if (params.onlyUserComponents !== false) {\n      filtered = filtered.filter(e => e.isUserComponent)\n    }\n\n    // Aggregate by component\n    const componentMap = new Map<string, {\n      sourceLocation?: string\n      timestamps: number[]\n      renderTimes: number[]\n      fpsValues: number[]\n    }>()\n\n    for (const event of filtered) {\n      const key = event.componentId\n      const existing = componentMap.get(key)\n      if (existing) {\n        existing.timestamps.push(event.timestamp)\n        if (event.renderTime != null)\n          existing.renderTimes.push(event.renderTime)\n        if (event.fps > 0)\n          existing.fpsValues.push(event.fps)\n      }\n      else {\n        componentMap.set(key, {\n          sourceLocation: event.sourceLocation,\n          timestamps: [event.timestamp],\n          renderTimes: event.renderTime != null ? [event.renderTime] : [],\n          fpsValues: event.fps > 0 ? [event.fps] : [],\n        })\n      }\n    }\n\n    let components: ComponentSummary[] = Array.from(componentMap.entries())\n      .map(([componentId, data]) => {\n        const totalUpdates = data.timestamps.length\n        const firstUpdate = Math.min(...data.timestamps)\n        const lastUpdate = Math.max(...data.timestamps)\n        const durationSec = (lastUpdate - firstUpdate) / 1000\n        const rawName = componentId.split('__')[0] || componentId\n        const componentName = rawName === 'Anonymous Component' && data.sourceLocation\n          ? `Anonymous (${data.sourceLocation})`\n          : rawName\n        const avgRenderTime = data.renderTimes.length > 0\n          ? data.renderTimes.reduce((a, b) => a + b, 0) / data.renderTimes.length\n          : undefined\n        const avgFps = data.fpsValues.length > 0\n          ? Math.round(data.fpsValues.reduce((a, b) => a + b, 0) / data.fpsValues.length)\n          : undefined\n        return {\n          componentName,\n          sourceLocation: data.sourceLocation,\n          totalUpdates,\n          updatesPerSecond: durationSec > 0 ? totalUpdates / durationSec : totalUpdates,\n          avgRenderTime,\n          avgFps,\n          firstUpdate,\n          lastUpdate,\n        }\n      })\n\n    // Sort\n    components.sort((a, b) => {\n      switch (params.sortBy) {\n        case 'totalUpdates': return b.totalUpdates - a.totalUpdates\n        case 'updatesPerSecond': return b.updatesPerSecond - a.updatesPerSecond\n        case 'renderTime': return (b.avgRenderTime ?? 0) - (a.avgRenderTime ?? 0)\n        case 'componentName': return a.componentName.localeCompare(b.componentName)\n        default: return b.totalUpdates - a.totalUpdates\n      }\n    })\n\n    components = components.slice(0, params.limit)\n\n    // Time range for all filtered events\n    const timeRange = filtered.length > 0\n      ? {\n          start: filtered[0].timestamp,\n          end: filtered[filtered.length - 1].timestamp,\n        }\n      : null\n\n    return {\n      components,\n      events: params.includeRawEvents ? filtered.slice(0, params.limit) : undefined,\n      metadata: {\n        recordingStatus: this.isRecording ? 'active' : 'paused',\n        totalEvents: filtered.length,\n        uniqueComponents: componentMap.size,\n        bufferSize: this.events.length,\n        bufferCapacity: this.maxEvents,\n        timeRange,\n      },\n    }\n  }\n\n  clear() {\n    this.events = []\n  }\n\n  startRecording() {\n    this.isRecording = true\n  }\n\n  stopRecording() {\n    this.isRecording = false\n  }\n\n  exportAll() {\n    return {\n      events: this.events,\n      exportedAt: Date.now(),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/index.ts",
    "content": "import type { DevpilotPlugin } from 'unplugin-devpilot'\nimport type { ComponentUpdateEvent, QueryParams } from './types'\nimport { defineMcpToolRegister, resolveClientModule, resolveSkillModule } from 'unplugin-devpilot'\nimport { z } from 'zod'\nimport { VueScanDataStore } from './data-store'\n\n// Create data store instance\nconst dataStore = new VueScanDataStore()\n\n// Define the DevPilot plugin\nexport const vueScanPlugin: DevpilotPlugin = {\n  namespace: 'vue-scan',\n\n  // Client-side module for auto-injection and data collection\n  clientModule: resolveClientModule(import.meta.url, './client/index.mjs'),\n  skillModule: resolveSkillModule(import.meta.url, './skill.md'),\n\n  // Server-side RPC methods\n  serverSetup: () => ({\n    'vue-scan:recordUpdate': async (data: ComponentUpdateEvent) => {\n      dataStore.addEvent(data)\n    },\n\n    'vue-scan:startRecording': async () => {\n      dataStore.startRecording()\n      return { status: 'active' }\n    },\n\n    'vue-scan:stopRecording': async () => {\n      dataStore.stopRecording()\n      return { status: 'paused' }\n    },\n\n    'vue-scan:clearData': async () => {\n      dataStore.clear()\n      return { status: 'cleared' }\n    },\n\n    'vue-scan:exportData': async () => {\n      return dataStore.exportAll()\n    },\n  }),\n\n  // MCP tools for LLM integration\n  mcpSetup() {\n    const tools = [\n      defineMcpToolRegister(\n        'queryVueScanData',\n        {\n          title: 'Query Vue Component Render Performance',\n          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.',\n          inputSchema: z.object({\n            limit: z.number()\n              .default(50)\n              .describe('Maximum number of components to return'),\n\n            componentName: z.string()\n              .optional()\n              .describe('Filter by component name (supports partial match)'),\n\n            timeRange: z.object({\n              start: z.number().describe('Start timestamp (ms)'),\n              end: z.number().describe('End timestamp (ms)'),\n            }).optional().describe('Filter by time range'),\n\n            minUpdateCount: z.number()\n              .optional()\n              .describe('Only return components with update count >= this value'),\n\n            minRenderTime: z.number()\n              .optional()\n              .describe('Only return events with render time >= this value (ms)'),\n\n            onlyInViewport: z.boolean()\n              .optional()\n              .describe('Only return updates that occurred in viewport'),\n\n            onlyUserComponents: z.boolean()\n              .default(true)\n              .describe('Only return user code components, filter out library components. Default true'),\n\n            includeRawEvents: z.boolean()\n              .default(false)\n              .describe('Include raw update events in addition to component summaries. Usually not needed'),\n\n            sortBy: z.enum(['totalUpdates', 'updatesPerSecond', 'renderTime', 'componentName'])\n              .default('totalUpdates')\n              .describe('Sort components by field'),\n          }),\n        },\n        async (params) => {\n          const data = dataStore.query(params as QueryParams)\n          return {\n            content: [{\n              type: 'text' as const,\n              text: JSON.stringify(data, null, 2),\n            }],\n          }\n        },\n      ),\n    ]\n\n    return tools\n  },\n}\n\nexport default vueScanPlugin\nexport * from './types'\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/skill.md",
    "content": "---\nname: vue-scan\ndescription: 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.\nallowed-tools: [\n  \"queryVueScanData\"\n]\n---\n\n# Vue Scan Skill\n\nMonitor and analyze Vue component render performance via real-time update event tracking.\n\n## ⚠️ CRITICAL: Recording Must Be Active\n\n**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.\n\n## How It Works\n\n- Client-side hooks are injected into Vue 2/3 component `mounted` and `updated` lifecycle\n- Each render emits an event with: phase (mount/update), render time (ms), FPS, component name, source location, update count, viewport visibility\n- Render time is measured between `beforeUpdate` and `updated` hooks via `performance.now()`\n- FPS is tracked using a `requestAnimationFrame` sliding window (1s)\n- Events are stored server-side in a circular buffer (max 10,000 events)\n- `queryVueScanData` returns **per-component aggregated summaries** (not raw events) by default\n\n## Workflow\n\n1. **Ensure recording**: Ask user to start recording and interact with the page\n2. **Query overview**: Call `queryVueScanData()` — returns components sorted by `totalUpdates`\n3. **Drill down**: Filter by `componentName` or sort by `updatesPerSecond` to find hot spots\n4. **Locate source**: Use `sourceLocation` (format: `file:line:col:tag`) to pinpoint code\n\n## queryVueScanData Parameters\n\n| Parameter | Default | Purpose |\n|-----------|---------|---------|\n| `limit` | 50 | Max components to return |\n| `componentName` | - | Filter by name (partial match, case-insensitive, ignores `-` and `_`) |\n| `timeRange` | - | `{ start, end }` timestamps in ms (0 values ignored) |\n| `minUpdateCount` | - | Only components updated >= N times |\n| `minRenderTime` | - | Only events with render time >= N ms |\n| `onlyInViewport` | - | Exclude off-screen renders |\n| `onlyUserComponents` | true | Filter out library components (no source location) |\n| `includeRawEvents` | false | Include individual event details (usually not needed) |\n| `sortBy` | totalUpdates | `totalUpdates` / `updatesPerSecond` / `renderTime` / `componentName` |\n\n## Response Structure\n\n- `components[]`: `{ componentName, sourceLocation, totalUpdates, updatesPerSecond, avgRenderTime, avgFps, firstUpdate, lastUpdate }`\n- `metadata`: `{ recordingStatus, totalEvents, uniqueComponents, bufferSize, bufferCapacity, timeRange }`\n- `events[]`: Only present when `includeRawEvents: true`\n\n## Best Practices\n\n- **Don't set timeRange to zeros**: Omit it entirely if not filtering by time\n- **Sort by renderTime**: Find the slowest components causing frame drops\n- **Sort by updatesPerSecond**: Reveals components re-rendering too rapidly\n- **Check avgFps**: Low avgFps (< 30) indicates the component causes jank\n- **Use sourceLocation**: Directly identifies the source file and line for fixes\n- **Check onlyUserComponents=false**: If results seem sparse, library components may be the source\n\n## Example: Find Performance Issues\n\n```\n1. queryVueScanData({ sortBy: \"renderTime\" })\n   → Find slowest components by average render time\n2. queryVueScanData({ sortBy: \"totalUpdates\" })\n   → Review top re-rendering components with source locations\n3. queryVueScanData({ minRenderTime: 16, sortBy: \"renderTime\" })\n   → Find components exceeding one frame budget (16ms)\n4. queryVueScanData({ componentName: \"UserList\", sortBy: \"updatesPerSecond\" })\n   → Find the most frequently re-rendering component among partial matches\n5. queryVueScanData({ onlyUserComponents: false, sortBy: \"totalUpdates\" })\n   → Include library components to see full picture\n```\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/src/types.ts",
    "content": "// Server-side types\nexport interface ComponentUpdateEvent {\n  timestamp: number\n  componentName: string\n  componentId: string\n  phase: 'mount' | 'update'\n  renderTime?: number\n  fps: number\n  updateCount: number\n  bounds: {\n    width: number\n    height: number\n    top: number\n    left: number\n  }\n  isInViewport: boolean\n  isUserComponent: boolean\n  sourceLocation?: string\n  parentComponent?: string\n}\n\nexport interface ComponentSummary {\n  componentName: string\n  sourceLocation?: string\n  totalUpdates: number\n  updatesPerSecond: number\n  avgRenderTime?: number\n  avgFps?: number\n  firstUpdate: number\n  lastUpdate: number\n}\n\nexport interface QueryParams {\n  limit: number\n  componentName?: string\n  timeRange?: { start: number, end: number }\n  minUpdateCount?: number\n  minRenderTime?: number\n  onlyInViewport?: boolean\n  onlyUserComponents?: boolean\n  includeRawEvents?: boolean\n  sortBy: 'totalUpdates' | 'updatesPerSecond' | 'renderTime' | 'componentName'\n}\n\nexport interface QueryResult {\n  components: ComponentSummary[]\n  events?: ComponentUpdateEvent[]\n  metadata: {\n    recordingStatus: 'active' | 'paused'\n    totalEvents: number\n    uniqueComponents: number\n    bufferSize: number\n    bufferCapacity: number\n    timeRange: { start: number, end: number } | null\n  }\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsconfig.client.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\", \"dom\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"unplugin-devpilot/virtual\",\n      \"node\"\n    ],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"isolatedDeclarations\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/client\", \"tests/client\"]\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"es2023\"],\n    \"moduleDetection\": \"force\",\n    \"module\": \"preserve\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"isolatedDeclarations\": false,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\", \"tests\"],\n  \"exclude\": [\"src/client\", \"tests/client\"]\n}\n"
  },
  {
    "path": "packages/devpilot-plugin-vue-scan/tsdown.config.ts",
    "content": "import { copyFileSync, mkdirSync } from 'node:fs'\nimport { defineConfig } from 'tsdown'\n\nexport default defineConfig([\n  {\n    entry: [\n      'src/*.ts',\n    ],\n    dts: true,\n    inlineOnly: false,\n  },\n  {\n    entry: [\n      'src/client/index.ts',\n    ],\n    outDir: 'dist/client',\n    inlineOnly: false,\n    hooks: {\n      'build:done': () => {\n        mkdirSync('dist', { recursive: true })\n        copyFileSync('src/skill.md', 'dist/skill.md')\n      },\n    },\n  },\n])\n"
  },
  {
    "path": "packages/extension/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "packages/extension/README.md",
    "content": "# 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 Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).\n"
  },
  {
    "path": "packages/extension/entrypoints/background.ts",
    "content": "import { autoInject, blacklist } from '../utils/storage'\n\nexport default defineBackground(() => {\n  console.log('Background script started', { id: browser.runtime.id })\n\n  console.log({\n    autoInject,\n    blacklist,\n  })\n})\n"
  },
  {
    "path": "packages/extension/entrypoints/content.ts",
    "content": "import { autoInject, blacklist } from '../utils/storage'\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  async main() {\n    const [isAutoInject, _blacklistPatterns] = await Promise.all([\n      autoInject.getValue(),\n      blacklist.getValue(),\n    ])\n\n    const blacklistPatterns = Object.values(_blacklistPatterns)\n\n    // Check if current URL is blacklisted\n    const currentUrl = window.location.href\n    const isBlacklisted = blacklistPatterns.some((pattern) => {\n      if (pattern === '<all_urls>')\n        return true\n\n      try {\n        const regexPattern = pattern\n          .replace(/\\./g, '\\\\.')\n          .replace(/\\*/g, '.*')\n          .replace(/\\?/g, '.')\n        const regex = new RegExp(`^${regexPattern}$`)\n        return regex.test(currentUrl)\n      }\n      catch {\n        return false\n      }\n    })\n\n    if (isAutoInject && !isBlacklisted) {\n      console.log('Auto inject enabled and not blacklisted, injecting script...')\n      const script = document.createElement('script')\n      script.src = browser.runtime.getURL('/auto.cjs')\n      script.onload = function () {\n        console.log('script injected.')\n      }\n      document.body.appendChild(script)\n    }\n    else {\n      console.log('Script injection skipped:', {\n        isAutoInject,\n        isBlacklisted,\n        currentUrl,\n      })\n    }\n  },\n})\n"
  },
  {
    "path": "packages/extension/entrypoints/popup/App.vue",
    "content": "<script lang=\"ts\" setup>\nimport { onMounted, ref, watch } from 'vue'\nimport { autoInject, blacklist } from '../../utils/storage'\n\nconst autoInjectEnabled = ref(false)\nconst blacklistPatterns = ref<string[]>([])\nconst newPattern = ref('')\n\nonMounted(async () => {\n  const [isAutoInject, _patterns] = await Promise.all([\n    autoInject.getValue(),\n    blacklist.getValue(),\n  ])\n\n  autoInjectEnabled.value = Boolean(isAutoInject)\n  blacklistPatterns.value = Object.values(_patterns)\n})\n\n// Watch autoInject changes\nwatch(autoInjectEnabled, async (value) => {\n  await autoInject.setValue(value)\n})\n\n// Watch blacklist changes\nwatch(blacklistPatterns, async (value) => {\n  await blacklist.setValue(value)\n}, { deep: true })\n\nfunction isValidPattern(pattern: string) {\n  try {\n    if (pattern === '<all_urls>')\n      return true\n    return /^(?:\\*|https?|file|ftp|urn):\\/\\/[^/]*\\/.*$/.test(pattern)\n  }\n  catch {\n    return false\n  }\n}\n\nasync function addPattern() {\n  if (!newPattern.value || !isValidPattern(newPattern.value)) {\n    // eslint-disable-next-line no-alert\n    alert('Invalid pattern! Format should be like: *://*.example.com/*')\n    return\n  }\n\n  if (!blacklistPatterns.value.includes(newPattern.value)) {\n    blacklistPatterns.value = [...blacklistPatterns.value, newPattern.value]\n    newPattern.value = ''\n  }\n}\n\nasync function removePattern(pattern: string) {\n  blacklistPatterns.value = blacklistPatterns.value.filter(p => p !== pattern)\n}\n</script>\n\n<template>\n  <div class=\"min-w-[300px] p-4 flex flex-col gap-4\">\n    <label class=\"flex gap-1 items-center justify-between cursor-pointer\">\n      <span>\n        Inject &nbsp;<a href=\"https://github.com/zcf0508/vue-scan\">Vue Scan</a>.\n      </span>\n      <input\n        v-model=\"autoInjectEnabled\"\n        class=\"cursor-pointer\"\n        type=\"checkbox\"\n      >\n    </label>\n\n    <div class=\"flex flex-col gap-2\">\n      <h3 class=\"font-medium\">\n        Blacklist patterns:\n      </h3>\n      <div class=\"flex gap-2\">\n        <input\n          v-model=\"newPattern\"\n          placeholder=\"e.g. *://*.example.com/*\"\n          class=\"flex-1 px-2 py-1 border rounded\"\n          @keyup.enter=\"addPattern\"\n        >\n        <button\n          class=\"px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 outline-none border-none cursor-pointer\"\n          @click=\"addPattern\"\n        >\n          Add\n        </button>\n      </div>\n\n      <ul class=\"flex flex-col gap-1 pl-0\">\n        <li\n          v-for=\"pattern in blacklistPatterns\"\n          :key=\"pattern\"\n          class=\"flex justify-between items-center gap-2 px-2 py-1 bg-gray-100 rounded\"\n        >\n          <span class=\"truncate\">{{ pattern }}</span>\n          <button\n            class=\"text-red-500 hover:text-red-600 cursor-pointer\"\n            @click=\"removePattern(pattern)\"\n          >\n            ✕\n          </button>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<style scoped>\ninput[type=\"checkbox\"] {\n  accent-color: #2563eb;\n}\n</style>\n"
  },
  {
    "path": "packages/extension/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/extension/entrypoints/popup/main.ts",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport 'uno.css'\n\ncreateApp(App).mount('#app')\n"
  },
  {
    "path": "packages/extension/package.json",
    "content": "{\n  \"name\": \"vue-scan-ext\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"private\": true,\n  \"description\": \"manifest.json description\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"compile\": \"vue-tsc --noEmit\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^3.5.12\"\n  },\n  \"devDependencies\": {\n    \"@types/chrome\": \"^0.0.280\",\n    \"@wxt-dev/module-vue\": \"^1.0.1\",\n    \"typescript\": \"5.6.3\",\n    \"unocss\": \"^0.65.1\",\n    \"vue-tsc\": \"^2.1.10\",\n    \"wxt\": \"^0.19.13\"\n  }\n}\n"
  },
  {
    "path": "packages/extension/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/extension/uno.config.ts",
    "content": "import { defineConfig, presetIcons, presetWind } from 'unocss'\n\nexport default defineConfig({\n  content: { filesystem: ['entrypoints/popup/*/*.{ts,tsx,vue}'] },\n  theme: {\n    colors: {\n      primary: '#0088ff',\n      success: '#00cca3',\n      warn: '#ff7f0f',\n      error: '#f54327',\n    },\n    boxShadow: {\n      default: '0 2px 16px rgba(0, 0, 0, 0.09)',\n    },\n  },\n  presets: [\n    presetWind(),\n    presetIcons({\n      scale: 1,\n      prefix: 'i-',\n      extraProperties: {\n        'display': 'inline-block',\n        'min-width': '1em',\n      },\n    }),\n  ],\n})\n"
  },
  {
    "path": "packages/extension/utils/storage.ts",
    "content": "import { storage } from 'wxt/storage'\n\nexport const autoInject = storage.defineItem<boolean>('local:autoInject', {\n  fallback: false,\n})\n\nexport const blacklist = storage.defineItem<string[]>('local:blacklist', {\n  fallback: [],\n})\n"
  },
  {
    "path": "packages/extension/wxt.config.ts",
    "content": "import UnoCSS from 'unocss/vite'\nimport { defineConfig } from 'wxt'\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  extensionApi: 'chrome',\n  modules: ['@wxt-dev/module-vue'],\n  manifest: {\n    permissions: ['storage'],\n    web_accessible_resources: [{\n      resources: ['auto.cjs'],\n      matches: ['<all_urls>'],\n    }],\n  },\n  vite: () => {\n    return {\n      plugins: [\n        UnoCSS(),\n      ],\n    }\n  },\n})\n"
  },
  {
    "path": "packages/nuxt/.gitignore",
    "content": "# 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*\n\n# Generated dirs\ndist\n\n# Nuxt\n.nuxt\n.output\n.data\n.vercel_build_output\n.build-*\n.netlify\n\n# Env\n.env\n\n# Testing\nreports\ncoverage\n*.lcov\n.nyc_output\n\n# VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Intellij idea\n*.iml\n.idea\n\n# OSX\n.DS_Store\n.AppleDouble\n.LSOverride\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n"
  },
  {
    "path": "packages/nuxt/Readme.md",
    "content": "Reference [vue-scan](https://github.com/zcf0508/vue-scan) .\n"
  },
  {
    "path": "packages/nuxt/package.json",
    "content": "{\n  \"name\": \"z-vue-scan-nuxt-module\",\n  \"type\": \"module\",\n  \"version\": \"0.0.37\",\n  \"description\": \"z-vue-scan Nuxt module\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/zcf0508/vue-scan\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/types.d.ts\",\n      \"import\": \"./dist/module.mjs\",\n      \"require\": \"./dist/module.cjs\"\n    }\n  },\n  \"main\": \"./dist/module.cjs\",\n  \"types\": \"./dist/types.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"prepack\": \"nuxt-module-build build\",\n    \"dev\": \"nuxi dev playground\",\n    \"dev:build\": \"nuxi build playground\",\n    \"dev:prepare\": \"nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground\",\n    \"release\": \"npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags\",\n    \"lint\": \"eslint .\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest watch\",\n    \"test:types\": \"vue-tsc --noEmit && cd playground && vue-tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@nuxt/kit\": \"^3.15.2\",\n    \"z-vue-scan\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@nuxt/devtools\": \"^1.7.0\",\n    \"@nuxt/eslint-config\": \"^0.7.5\",\n    \"@nuxt/module-builder\": \"^0.8.4\",\n    \"@nuxt/schema\": \"^3.15.2\",\n    \"@nuxt/test-utils\": \"^3.15.4\",\n    \"@types/node\": \"latest\",\n    \"changelogen\": \"^0.5.7\",\n    \"eslint\": \"^9.18.0\",\n    \"nuxt\": \"^3.15.2\",\n    \"typescript\": \"~5.6.3\",\n    \"vitest\": \"^3.0.2\",\n    \"vue-tsc\": \"^2.2.0\"\n  }\n}\n"
  },
  {
    "path": "packages/nuxt/playground/app.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst b = ref('')\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ b }}\n    </div>\n    <HelloWorld v-model:msg=\"b\" />\n  </div>\n</template>\n"
  },
  {
    "path": "packages/nuxt/playground/components/HelloWorld.vue",
    "content": "<script setup lang=\"ts\">\nconst msg = defineModel('msg', {\n  default: '',\n})\n</script>\n\n<template>\n  <div>\n    <div>\n      {{ msg }}\n    </div>\n    input: <input v-model=\"msg\">\n  </div>\n</template>\n"
  },
  {
    "path": "packages/nuxt/playground/nuxt.config.ts",
    "content": "export default defineNuxtConfig({\n  modules: ['../src/module'],\n  vueScan: {\n    enable: true,\n  },\n  devtools: { enabled: true },\n  compatibilityDate: '2025-01-22',\n})\n"
  },
  {
    "path": "packages/nuxt/playground/package.json",
    "content": "{\n  \"name\": \"z-vue-scan-nuxt-module-playground\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"nuxi dev\",\n    \"build\": \"nuxi build\",\n    \"generate\": \"nuxi generate\"\n  },\n  \"dependencies\": {\n    \"nuxt\": \"^3.15.2\"\n  }\n}\n"
  },
  {
    "path": "packages/nuxt/playground/server/tsconfig.json",
    "content": "{\n  \"extends\": \"../.nuxt/tsconfig.server.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/playground/tsconfig.json",
    "content": "{\n  \"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/src/module.ts",
    "content": "import type { VueScanBaseOptions } from 'z-vue-scan'\nimport process from 'node:process'\nimport { addPluginTemplate, defineNuxtModule } from '@nuxt/kit'\n\n// Module options TypeScript interface definition\nexport interface ModuleOptions extends VueScanBaseOptions {}\n\nexport default defineNuxtModule<ModuleOptions>({\n  meta: {\n    name: 'z-vue-scan-nuxt-module',\n    configKey: 'vueScan',\n  },\n  // Default configuration options of the Nuxt module\n  defaults: {\n    enable: process.env.NODE_ENV === 'development',\n  },\n  setup(_options, _nuxt) {\n    addPluginTemplate({\n      filename: 'vue-scan.plugin.mjs',\n      getContents: () => `\nimport { defineNuxtPlugin } from '#app'\nimport VueScan from 'z-vue-scan'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const options = ${JSON.stringify(_options)}\n  \n  nuxtApp.vueApp.use(VueScan, options)\n})\n      `,\n      mode: 'client',\n    })\n  },\n})\n"
  },
  {
    "path": "packages/nuxt/src/runtime/plugin.ts",
    "content": "import { defineNuxtPlugin } from '#app'\n\nexport default defineNuxtPlugin((_nuxtApp) => {\n  console.log('Plugin injected by z-vue-scan-nuxt-module!')\n})\n"
  },
  {
    "path": "packages/nuxt/src/runtime/server/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../.nuxt/tsconfig.server.json\"\n}\n"
  },
  {
    "path": "packages/nuxt/test/basic.test.ts",
    "content": "import { fileURLToPath } from 'node:url'\nimport { $fetch, setup } from '@nuxt/test-utils/e2e'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('ssr', async () => {\n  await setup({\n    rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),\n  })\n\n  it('renders the index page', async () => {\n    // Get response to a server-rendered page with `$fetch`.\n    const html = await $fetch('/')\n    expect(html).toContain('<div>basic</div>')\n  })\n})\n"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/app.vue",
    "content": "<script setup>\n</script>\n\n<template>\n  <div>basic</div>\n</template>\n"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/nuxt.config.ts",
    "content": "import MyModule from '../../../src/module'\n\nexport default defineNuxtConfig({\n  modules: [\n    MyModule,\n  ],\n})\n"
  },
  {
    "path": "packages/nuxt/test/fixtures/basic/package.json",
    "content": "{\n  \"name\": \"basic\",\n  \"type\": \"module\",\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/nuxt/tsconfig.json",
    "content": "{\n  \"extends\": \"./.nuxt/tsconfig.json\",\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"playground\"\n  ]\n}\n"
  },
  {
    "path": "patches/@vue__devtools-kit.patch",
    "content": "diff --git a/dist/index.cjs b/dist/index.cjs\nindex 0004140e1a340e020f3227a4c0eb63ae04ee7a4c..70ad9a77e82fc0605f21064c7bac0e1513de9984 100644\n--- a/dist/index.cjs\n+++ b/dist/index.cjs\n@@ -2179,6 +2179,24 @@ function update(options) {\n     indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;\n   }\n }\n+function updateHighlight(options) {\n+  const containerEl = options?.elementId ? document.getElementById(options.elementId) :getContainerElement();\n+  if (containerEl) {\n+    const cardEl = containerEl.querySelector('#__vue-devtools-component-inspector__card__');\n+    const nameEl = containerEl.querySelector('#__vue-devtools-component-inspector__name__');\n+    const indicatorEl = containerEl.querySelector('#__vue-devtools-component-inspector__indicator__');\n+    Object.assign(containerEl.style, {\n+      ...containerStyles,\n+      ...getStyles(options.bounds),\n+      ...options.style\n+    });\n+    Object.assign(cardEl.style, {\n+      top: options.bounds.top < 35 ? 0 : \"-35px\"\n+    });\n+    nameEl.innerHTML = `&lt;${options.name}&gt;&nbsp;&nbsp;`;\n+    indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;\n+  }\n+}\n function highlight(instance) {\n   const bounds = getComponentBoundingRect(instance);\n   if (!bounds.width && !bounds.height)\n@@ -6658,6 +6676,9 @@ var devtools = {\n     return devtoolsContext.api;\n   }\n };\n+\n+const createHighlight = create;\n+\n // Annotate the CommonJS export names for ESM import in node:\n 0 && (module.exports = {\n   DevToolsContextHookKeys,\n@@ -6740,5 +6761,9 @@ var devtools = {\n   toggleHighPerfMode,\n   updateDevToolsClientDetected,\n   updateDevToolsState,\n-  updateTimelineLayersState\n+  updateTimelineLayersState,\n+  getComponentBoundingRect,\n+  getInstanceName,\n+  createHighlight,\n+  updateHighlight\n });\ndiff --git a/dist/index.d.cts b/dist/index.d.cts\nindex d7e6b9b8c83d3178da6b41a054bb0da69e7e77cf..a5f91ba0a0f55cd9feb0bdc6cabf12d0bb2e30dd 100644\n--- a/dist/index.d.cts\n+++ b/dist/index.d.cts\n@@ -1099,4 +1099,9 @@ declare const devtools: {\n     };\n };\n \n-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 };\n+declare function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect\n+declare function getInstanceName(instance: VueAppInstance): string\n+declare function createHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): HTMLDivElement\n+declare function updateHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): void\n+\n+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 };\ndiff --git a/dist/index.d.ts b/dist/index.d.ts\nindex d7e6b9b8c83d3178da6b41a054bb0da69e7e77cf..a5f91ba0a0f55cd9feb0bdc6cabf12d0bb2e30dd 100644\n--- a/dist/index.d.ts\n+++ b/dist/index.d.ts\n@@ -1099,4 +1099,9 @@ declare const devtools: {\n     };\n };\n \n-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 };\n+declare function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect\n+declare function getInstanceName(instance: VueAppInstance): string\n+declare function createHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): HTMLDivElement\n+declare function updateHighlight(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial<CSSStyleDeclaration> }): void\n+\n+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 };\ndiff --git a/dist/index.js b/dist/index.js\nindex 530ba2998ca4220345085e1eb09872ddda9d8735..eb5240d07f2cc452461e30de811ed5dbf63fc7c6 100644\n--- a/dist/index.js\n+++ b/dist/index.js\n@@ -2088,6 +2088,26 @@ function update(options) {\n     indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;\n   }\n }\n+\n+function updateHighlight(options) {\n+  const containerEl = options?.elementId ? document.getElementById(options.elementId) :getContainerElement();\n+  if (containerEl) {\n+    const cardEl = containerEl.querySelector('#__vue-devtools-component-inspector__card__');\n+    const nameEl = containerEl.querySelector('#__vue-devtools-component-inspector__name__');\n+    const indicatorEl = containerEl.querySelector('#__vue-devtools-component-inspector__indicator__');\n+    Object.assign(containerEl.style, {\n+      ...containerStyles,\n+      ...getStyles(options.bounds),\n+      ...options.style\n+    });\n+    Object.assign(cardEl.style, {\n+      top: options.bounds.top < 35 ? 0 : \"-35px\"\n+    });\n+    nameEl.innerHTML = `&lt;${options.name}&gt;&nbsp;&nbsp;`;\n+    indicatorEl.innerHTML = `${Math.round(options.bounds.width * 100) / 100} x ${Math.round(options.bounds.height * 100) / 100}`;\n+  }\n+}\n+\n function highlight(instance) {\n   const bounds = getComponentBoundingRect(instance);\n   if (!bounds.width && !bounds.height)\n@@ -6648,5 +6668,9 @@ export {\n   toggleHighPerfMode,\n   updateDevToolsClientDetected,\n   updateDevToolsState,\n-  updateTimelineLayersState\n+  updateTimelineLayersState,\n+  getComponentBoundingRect,\n+  getInstanceName,\n+  create as createHighlight,\n+  updateHighlight\n };\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - examples/*\n  - packages/*\n"
  },
  {
    "path": "src/auto.ts",
    "content": "import type { VNodeNormalizedChildren } from 'vue-demi'\nimport { throttle } from 'lodash-es'\nimport { type BACE_VUE_INSTANCE, createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/hook'\nimport plugin from './index'\nimport { createDomMutationObserver } from './utils/MutationObserverDom'\n\n(() => {\n  // eslint-disable-next-line node/prefer-global/process\n  if (!window.process) {\n    // @ts-expect-error browser mock\n    // eslint-disable-next-line node/prefer-global/process\n    window.process = {\n      env: {\n        NODE_ENV: 'development',\n      },\n    }\n  }\n\n  if (!window.__VUE_SCAN__) {\n    window.__VUE_SCAN__ = {\n      plugin,\n      createOnBeforeUpdateHook,\n      createOnBeforeUnmountHook,\n      createOnMountedHook,\n      createOnUpdatedHook,\n    }\n  }\n})()\n\n// Check if the __vue_app__ property exists on the #app node of the page\n\nfunction injectVueScan(node: HTMLElement) {\n  // @ts-expect-error vue internal\n  if ((node.__vue_app__ || node.__vue__)) {\n    // @ts-expect-error vue internal\n    if (node.__vue_app__) { // VUE 3\n      // @ts-expect-error vue internal\n      const vueInstance = node.__vue_app__._container._vnode.component as BACE_VUE_INSTANCE\n\n      // @ts-expect-error vue internal\n      node.__vue_app__.use(window.__VUE_SCAN__.plugin)\n\n      const first = !vueInstance?.__vue_scan_injected__\n\n      if (!first) {\n        console.log(vueInstance)\n      }\n\n      function mixinChildren(children: VNodeNormalizedChildren) {\n        if (!children) {\n          return\n        }\n\n        if (typeof children === 'string') {\n          return\n        }\n\n        if (!Array.isArray(children)) {\n          return\n        }\n\n        children.forEach((item) => {\n          if (typeof item !== 'object') {\n            return\n          }\n\n          if (item && 'component' in item && item.component) {\n            mixin(item.component as BACE_VUE_INSTANCE)\n          }\n          else if (item && 'children' in item) {\n            mixinChildren(item.children)\n          }\n        })\n      }\n\n      function mixin(vueInstance: BACE_VUE_INSTANCE) {\n        if (vueInstance.subTree?.el && vueInstance?.__vue_scan_injected__ !== true) {\n          const onBeforeUpdate = createOnBeforeUpdateHook(vueInstance)\n          const onUpdated = createOnUpdatedHook(vueInstance)\n          const onBeforeUnmount = createOnBeforeUnmountHook(vueInstance)\n\n          if (onBeforeUpdate) {\n            if (vueInstance?.bu) {\n              vueInstance.bu.push(onBeforeUpdate)\n            }\n            else {\n              vueInstance!.bu = [onBeforeUpdate]\n            }\n          }\n\n          if (onUpdated) {\n            if (vueInstance?.u) {\n              vueInstance.u.push(onUpdated)\n            }\n            else {\n              vueInstance!.u = [onUpdated]\n            }\n          }\n\n          if (onBeforeUnmount) {\n            if (vueInstance?.bum) {\n              vueInstance.bum.push(onBeforeUnmount)\n            }\n            else {\n              vueInstance!.bum = [onBeforeUnmount]\n            }\n          }\n\n          vueInstance.__vue_scan_injected__ = true\n        }\n\n        if (!vueInstance?.subTree?.component && vueInstance?.subTree?.children) {\n          mixinChildren(vueInstance.subTree.children)\n        }\n        else if (vueInstance?.subTree?.component) {\n          mixin(vueInstance.subTree.component as BACE_VUE_INSTANCE)\n        }\n\n        else if (!vueInstance?.subTree && vueInstance?.children) {\n          mixinChildren(vueInstance.children)\n        }\n      }\n\n      mixin(vueInstance)\n\n      if (!first) {\n        console.log('vue scan inject success')\n      }\n\n      vueInstance.__vue_scan_injected__ = true\n    }\n    // @ts-expect-error vue internal\n    else if (node.__vue__) { // VUE 2\n      // @ts-expect-error vue internal\n      const vueInstance = (node.__vue__?.$vnode?.componentInstance || node.__vue__) as BACE_VUE_INSTANCE\n\n      const first = !vueInstance?.__vue_scan_injected__\n\n      if (first) {\n        console.log(vueInstance)\n      }\n\n      function mixin(vueInstance: BACE_VUE_INSTANCE) {\n        if (vueInstance?.$el && vueInstance?.__vue_scan_injected__ !== true) {\n          const onBeforeUpdate = createOnBeforeUpdateHook(vueInstance)\n          const onUpdated = createOnUpdatedHook(vueInstance)\n          const onBeforeUnmount = createOnBeforeUnmountHook(vueInstance)\n\n          if (onBeforeUpdate) {\n            if (vueInstance?.$options?.beforeUpdate) {\n              const newBeforeUpdate = [...vueInstance.$options.beforeUpdate]\n              newBeforeUpdate.push(onBeforeUpdate)\n              vueInstance.$set(vueInstance.$options, 'beforeUpdate', newBeforeUpdate)\n            }\n            else if (vueInstance?.$options) {\n              vueInstance.$set(vueInstance.$options, 'beforeUpdate', [onBeforeUpdate])\n            }\n          }\n\n          if (onUpdated) {\n            if (vueInstance?.$options?.updated) {\n              const newUpdated = [...vueInstance.$options.updated]\n              newUpdated.push(onUpdated)\n              vueInstance.$set(vueInstance.$options, 'updated', newUpdated)\n            }\n            else if (vueInstance?.$options) {\n              vueInstance.$set(vueInstance.$options, 'updated', [onUpdated])\n            }\n          }\n\n          if (onBeforeUnmount) {\n            if (vueInstance?.$options?.beforeDestroy) {\n              const newBeforeDestroy = [...vueInstance.$options.beforeDestroy]\n              newBeforeDestroy.push(onBeforeUnmount)\n              vueInstance.$set(vueInstance.$options, 'beforeDestroy', newBeforeDestroy)\n            }\n            else if (vueInstance?.$options) {\n              vueInstance.$set(vueInstance.$options, 'beforeDestroy', [onBeforeUnmount])\n            }\n          }\n\n          vueInstance.__vue_scan_injected__ = true\n        }\n\n        if (vueInstance?.$children) {\n          (vueInstance?.$children as Array<BACE_VUE_INSTANCE>).forEach((child) => {\n            mixin(child)\n          })\n        }\n      }\n\n      mixin(vueInstance)\n\n      if (first) {\n        console.log('vue scan inject success')\n      }\n\n      vueInstance.__vue_scan_injected__ = true\n    }\n  }\n}\n\nfunction getMountDoms() {\n  const elements = Array.from(document.body.children)\n\n  return elements.filter((element) => {\n    // @ts-expect-error vue internal\n    return (!!element.__vue_app__ || !!element.__vue__)\n  }) as HTMLElement[]\n}\n\nconst vue2ObserverMap = new WeakMap<HTMLElement, MutationObserver>()\n\nconst documentObserver = new MutationObserver(throttle(() => {\n  if (!window.__VUE_SCAN__) {\n    return\n  }\n\n  const mountDoms = getMountDoms()\n\n  if (mountDoms.length === 0) {\n    return\n  }\n\n  const isVue3 = mountDoms.some((mountDom) => {\n    // @ts-expect-error vue internal\n    return !!mountDom.__vue_app__\n  })\n\n  if (isVue3) {\n    documentObserver.disconnect()\n  }\n\n  mountDoms.forEach((mountDom) => {\n    // @ts-expect-error vue internal\n    if (mountDom.__vue_app__) {\n      // vue3\n      documentObserver.disconnect()\n      injectVueScan(mountDom)\n    }\n    else {\n      // vue2\n      if (!vue2ObserverMap.get(mountDom)) {\n        vue2ObserverMap.set(mountDom, createDomMutationObserver(\n          () => mountDom,\n          () => {\n            console.log('injectVueScan')\n            injectVueScan(mountDom)\n          },\n          {\n            childList: true,\n            subtree: true,\n          },\n          600,\n        ))\n      }\n    }\n  })\n}, 600))\n\ndocumentObserver.observe(document.body, {\n  attributes: true,\n  childList: true,\n  subtree: true,\n})\n"
  },
  {
    "path": "src/core/fps.ts",
    "content": "export type RenderPhase = 'mount' | 'update'\n\nexport interface RenderMeta {\n  phase: RenderPhase\n  renderTime?: number\n  fps: number\n}\n\nconst WINDOW_MS = 1000\n\nlet frameTimestamps: number[] = []\nlet rafId: number | null = null\n\nfunction tick() {\n  const now = performance.now()\n  frameTimestamps.push(now)\n\n  // Drop entries older than 1s\n  const cutoff = now - WINDOW_MS\n  while (frameTimestamps.length > 0 && frameTimestamps[0] < cutoff) {\n    frameTimestamps.shift()\n  }\n\n  rafId = requestAnimationFrame(tick)\n}\n\nfunction ensureRunning() {\n  if (rafId === null) {\n    rafId = requestAnimationFrame(tick)\n  }\n}\n\nexport function getCurrentFps(): number {\n  ensureRunning()\n  if (frameTimestamps.length < 2)\n    return -1\n  return Math.round(frameTimestamps.length * 1000 / WINDOW_MS)\n}\n\nexport function stopFpsMonitor() {\n  if (rafId !== null) {\n    cancelAnimationFrame(rafId)\n    rafId = null\n  }\n  frameTimestamps = []\n}\n"
  },
  {
    "path": "src/core/highlight.ts",
    "content": "import type { RenderMeta } from './fps'\nimport { throttle } from 'lodash-es'\nimport { getComponentBoundingRect, getInstanceName } from './utils'\n\nexport interface ComponentBoundingRect {\n  top: number\n  left: number\n  width: number\n  height: number\n  right: number\n  bottom: number\n}\n\nexport function isInViewport(bounds: ComponentBoundingRect): boolean {\n  const viewportWidth = window.innerWidth\n  const viewportHeight = window.innerHeight\n\n  // 只要元素和视口有交集，就认为是在视口内\n  return !(\n    bounds.left >= viewportWidth // 完全在视口右侧\n    || bounds.right <= 0 // 完全在视口左侧\n    || bounds.top >= viewportHeight // 完全在视口下方\n    || bounds.bottom <= 0 // 完全在视口上方\n  )\n}\n\ninterface HighlightItem {\n  bounds: ComponentBoundingRect\n  name: string\n  flashCount: number\n  hideComponentName: boolean\n  startTime: number\n  lastUpdateTime: number\n  opacity: number\n  state: 'fade-in' | 'visible' | 'fade-out'\n}\n\nexport interface HighlightCanvasOptions {\n  /** default 450ms */\n  displayDuration?: number\n  /** default 25ms */\n  fadeInDuration?: number\n  /** default 50ms */\n  fadeOutDuration?: number\n}\n\nclass HighlightCanvas {\n  private canvas: HTMLCanvasElement\n  private ctx: CanvasRenderingContext2D\n  private readonly DISPLAY_DURATION: number\n  private readonly FADE_IN_DURATION: number\n  private readonly FADE_OUT_DURATION: number\n  private highlights: Map<string, HighlightItem> = new Map()\n  private animationFrame: number | null = null\n  private textMetricsCache: Map<string, TextMetrics> = new Map()\n\n  constructor(options?: HighlightCanvasOptions) {\n    this.DISPLAY_DURATION = options?.displayDuration ?? 450\n    this.FADE_IN_DURATION = options?.fadeInDuration ?? 25\n    this.FADE_OUT_DURATION = options?.fadeOutDuration ?? 50\n    this.canvas = document.createElement('canvas')\n    this.canvas.style.cssText = `\n      position: fixed;\n      top: 0;\n      left: 0;\n      pointer-events: none;\n      z-index: 9999;\n    `\n    this.ctx = this.canvas.getContext('2d')!\n    document.body.appendChild(this.canvas)\n    this.updateCanvasSize()\n    window.addEventListener('resize', () => this.updateCanvasSize())\n    window.addEventListener('scroll', () => this.scheduleRender())\n  }\n\n  private updateCanvasSize() {\n    this.canvas.width = window.innerWidth\n    this.canvas.height = window.innerHeight\n  }\n\n  drawHighlight(bounds: ComponentBoundingRect, uuid: string, name: string, flashCount: number, hideComponentName = false) {\n    const now = Date.now()\n    const existingItem = this.highlights.get(uuid)\n\n    if (existingItem) {\n      existingItem.bounds = bounds\n      existingItem.name = name\n      existingItem.flashCount = flashCount\n      existingItem.hideComponentName = hideComponentName\n      existingItem.lastUpdateTime = now\n\n      if (existingItem.state === 'fade-out') {\n        existingItem.state = 'visible'\n        existingItem.opacity = 1\n      }\n    }\n    else {\n      this.highlights.set(uuid, {\n        bounds,\n        name,\n        flashCount,\n        hideComponentName,\n        startTime: now,\n        lastUpdateTime: now,\n        opacity: 0,\n        state: 'fade-in',\n      })\n    }\n\n    this.scheduleRender()\n  }\n\n  private scheduleRender() {\n    if (this.animationFrame)\n      return\n    this.animationFrame = requestAnimationFrame(() => this.render())\n  }\n\n  private render() {\n    const now = Date.now()\n\n    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)\n\n    this.ctx.font = '12px sans-serif'\n    this.ctx.textBaseline = 'middle'\n\n    for (const [uuid, item] of this.highlights.entries()) {\n      if (!isInViewport(item.bounds))\n        continue\n\n      const fadeInElapsed = now - item.startTime\n      const idleTime = now - item.lastUpdateTime\n      const fadeOutElapsed = now - item.startTime\n\n      switch (item.state) {\n        case 'fade-in':\n          item.opacity = Math.min(1, fadeInElapsed / this.FADE_IN_DURATION)\n          if (fadeInElapsed >= this.FADE_IN_DURATION) {\n            item.state = 'visible'\n            item.opacity = 1\n          }\n          break\n\n        case 'visible':\n          if (idleTime >= this.DISPLAY_DURATION) {\n            item.state = 'fade-out'\n            item.startTime = now\n          }\n          break\n\n        case 'fade-out':\n          item.opacity = Math.max(0, 1 - (fadeOutElapsed / this.FADE_OUT_DURATION))\n          if (fadeOutElapsed >= this.FADE_OUT_DURATION) {\n            this.highlights.delete(uuid)\n            continue\n          }\n          break\n      }\n\n      this.drawBorder(item)\n      if (!item.hideComponentName) {\n        this.drawLabel(item, item.opacity)\n      }\n    }\n\n    if (this.highlights.size > 0) {\n      this.animationFrame = requestAnimationFrame(() => this.render())\n    }\n    else {\n      this.animationFrame = null\n      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)\n    }\n  }\n\n  private drawBorder(item: HighlightItem) {\n    const { bounds, flashCount, opacity } = item\n    this.ctx.strokeStyle = `rgba(${Math.min(255, flashCount * 6)}, ${Math.max(0, 255 - flashCount * 6)}, 0, ${opacity})`\n    this.ctx.lineWidth = 2\n    this.ctx.strokeRect(\n      bounds.left,\n      bounds.top,\n      bounds.width,\n      bounds.height,\n    )\n  }\n\n  private drawLabel(item: HighlightItem, opacity: number) {\n    const { bounds, name, flashCount } = item\n    const labelMetrics = this.getTextMetrics(name)\n    const padding = 6\n    const labelHeight = 20\n\n    // 计算标签位置 - 移除额外的padding，直接贴在边框上\n    let labelX = bounds.left\n    let labelY = bounds.top\n\n    // 确保标签在视口内\n    const viewportHeight = window.innerHeight\n    const labelTotalHeight = labelHeight\n    const viewportWidth = window.innerWidth\n    const labelTotalWidth = labelMetrics.width + padding * 2\n\n    // 如果标签底部超出视口\n    if (labelY + labelTotalHeight > viewportHeight) {\n      labelY = viewportHeight - labelTotalHeight\n    }\n\n    // 如果标签右侧超出视口\n    if (labelX + labelTotalWidth > viewportWidth) {\n      labelX = viewportWidth - labelTotalWidth\n    }\n\n    // 绘制背景\n    this.ctx.fillStyle = `rgba(${Math.min(255, flashCount * 6)}, ${Math.max(0, 255 - flashCount * 6)}, 0, ${opacity * 0.8})`\n    this.ctx.fillRect(labelX, labelY, labelMetrics.width + padding * 2, labelHeight)\n\n    // 绘制文本 - 确保文本在背景中居中\n    this.ctx.fillStyle = Math.min(255, flashCount * 6) > 128\n      ? `rgba(255, 255, 255, ${opacity})`\n      : `rgba(0, 0, 0, ${opacity})`\n    this.ctx.fillText(name, labelX + padding, labelY + labelHeight / 2)\n  }\n\n  private getTextMetrics(text: string): TextMetrics {\n    const cached = this.textMetricsCache.get(text)\n    if (cached)\n      return cached\n\n    const metrics = this.ctx.measureText(text)\n    this.textMetricsCache.set(text, metrics)\n    return metrics\n  }\n\n  clear(uuid: string) {\n    const item = this.highlights.get(uuid)\n    if (item && item.state !== 'fade-out') {\n      item.state = 'fade-out'\n      item.startTime = Date.now()\n      this.scheduleRender()\n    }\n  }\n\n  clearAll() {\n    this.highlights.clear()\n    this.textMetricsCache.clear()\n    if (this.animationFrame) {\n      cancelAnimationFrame(this.animationFrame)\n      this.animationFrame = null\n    }\n    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)\n  }\n\n  destroy() {\n    this.clearAll()\n    if (this.canvas && this.canvas.parentNode) {\n      this.canvas.parentNode.removeChild(this.canvas)\n    }\n  }\n}\n\nlet highlightCanvas: HighlightCanvas | null = null\n\nfunction getHighlightCanvas(options?: HighlightCanvasOptions) {\n  if (highlightCanvas)\n    return highlightCanvas\n  highlightCanvas = new HighlightCanvas(options)\n  return highlightCanvas\n}\n\nwindow.addEventListener('unload', () => {\n  if (highlightCanvas) {\n    highlightCanvas.destroy()\n    highlightCanvas = null\n  }\n})\n\ntype UpdateHighlightFn = (\n  bounds: ComponentBoundingRect,\n  name: string,\n  flashCount: number,\n  hideComponentName?: boolean\n) => void\n\nexport function createUpdateHighlight(): UpdateHighlightFn {\n  return throttle<UpdateHighlightFn>(\n    (bounds, name, flashCount, hideComponentName) => {\n      if (!isInViewport(bounds) || !highlightCanvas)\n        return\n      highlightCanvas.drawHighlight(bounds, name, name, flashCount, hideComponentName)\n    },\n    500,\n  )\n}\n\nexport function highlight(\n  instance: any,\n  uuid: string,\n  flashCount: number,\n  meta?: RenderMeta,\n  options?: {\n    hideComponentName?: boolean\n  } & HighlightCanvasOptions,\n) {\n  const highlightCanvas = getHighlightCanvas(options)\n\n  const bounds = getComponentBoundingRect(instance)\n\n  if (!bounds.width && !bounds.height)\n    return\n  if (!isInViewport(bounds))\n    return\n\n  let name = `${getInstanceName(instance)} x ${flashCount}`\n  if (meta) {\n    const parts: string[] = [meta.phase]\n    if (meta.renderTime != null)\n      parts.push(`${meta.renderTime.toFixed(1)}ms`)\n    if (meta.fps > 0)\n      parts.push(`${meta.fps}fps`)\n    name += ` · ${parts.join(' · ')}`\n  }\n  highlightCanvas?.drawHighlight(bounds, uuid, name, flashCount, options?.hideComponentName)\n}\n\nexport function unhighlight(uuid: string) {\n  highlightCanvas?.clear(uuid)\n}\n\nexport function clearhighlight(uuid: string) {\n  highlightCanvas?.clear(uuid)\n}\n"
  },
  {
    "path": "src/core/hook.ts",
    "content": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport { getCurrentFps } from './fps'\nimport {\n  clearhighlight,\n  createUpdateHighlight,\n  highlight,\n  type HighlightCanvasOptions,\n  unhighlight,\n} from './highlight'\nimport { getInstanceName } from './utils'\n\nexport interface BACE_VUE_INSTANCE extends VueAppInstance {\n  __vue_scan_injected__?: boolean\n  /** beforeUpdate */\n  bu?: Array<() => void> | null\n  /** updated */\n  u?: Array<() => void> | null\n  /** beforeUnmount */\n  bum?: Array<() => void> | null\n  _uid?: number\n  __flashCount?: number\n  __flashTimeout?: ReturnType<typeof setTimeout> | null\n  __renderStartTime?: number | null\n  $options?: {\n    beforeUpdate?: Array<() => void> | null\n    updated?: Array<() => void> | null\n    beforeDestroy?: Array<() => void> | null\n  }\n}\n\nexport function createOnBeforeUpdateHook(instance?: BACE_VUE_INSTANCE) {\n  if (!instance) {\n    return\n  }\n\n  const el = instance?.subTree?.el || instance.$el\n\n  if (!el) {\n    return\n  }\n\n  return () => {\n    instance.__renderStartTime = performance.now()\n  }\n}\n\nexport function createOnMountedHook(instance?: BACE_VUE_INSTANCE, options?: {\n  hideComponentName?: boolean\n  interval?: number\n} & HighlightCanvasOptions) {\n  const {\n    interval = 1000,\n  } = options || {}\n\n  if (!instance) {\n    return\n  }\n\n  const el = instance?.subTree?.el || instance.$el\n\n  if (!el) {\n    return\n  }\n\n  const name = getInstanceName(instance)\n  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')\n\n  return () => {\n    if (!instance.__flashCount) {\n      instance.__flashCount = 0\n    }\n\n    if (!instance.__updateHighlight) {\n      instance.__updateHighlight = createUpdateHighlight()\n    }\n\n    instance.__flashCount++\n\n    const fps = getCurrentFps()\n    highlight(instance, uuid, instance.__flashCount, { phase: 'mount', fps }, options)\n\n    if (instance.__flashTimeout) {\n      clearTimeout(instance.__flashTimeout)\n      instance.__flashTimeout = null\n    }\n\n    instance.__flashTimeout = setTimeout(() => {\n      unhighlight(uuid)\n      instance.__flashTimeout = null\n      instance.__flashCount = 0\n    }, interval)\n  }\n}\n\nexport function createOnUpdatedHook(instance?: BACE_VUE_INSTANCE, options?: {\n  hideComponentName?: boolean\n  interval?: number\n} & HighlightCanvasOptions) {\n  const {\n    interval = 1000,\n  } = options || {}\n\n  if (!instance) {\n    return\n  }\n\n  const el = instance?.subTree?.el || instance.$el\n\n  if (!el) {\n    return\n  }\n\n  const name = getInstanceName(instance)\n  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')\n\n  return () => {\n    if (!instance.__flashCount) {\n      instance.__flashCount = 0\n    }\n\n    if (!instance.__updateHighlight) {\n      instance.__updateHighlight = createUpdateHighlight()\n    }\n\n    instance.__flashCount++\n\n    const renderTime = instance.__renderStartTime != null\n      ? performance.now() - instance.__renderStartTime\n      : undefined\n    instance.__renderStartTime = null\n    const fps = getCurrentFps()\n\n    highlight(instance, uuid, instance.__flashCount, { phase: 'update', renderTime, fps }, options)\n\n    if (instance.__flashTimeout) {\n      clearTimeout(instance.__flashTimeout)\n      instance.__flashTimeout = null\n    }\n\n    instance.__flashTimeout = setTimeout(() => {\n      unhighlight(uuid)\n      instance.__flashTimeout = null\n      instance.__flashCount = 0\n    }, interval)\n  }\n}\n\nexport function createOnBeforeUnmountHook(instance?: BACE_VUE_INSTANCE) {\n  if (!instance) {\n    return\n  }\n\n  const el = instance?.subTree?.el || instance.$el\n\n  if (!el) {\n    return\n  }\n\n  const name = getInstanceName(instance)\n  const uuid = `${name}__${instance.uid || instance._uid}`.replaceAll(' ', '_')\n\n  return () => {\n    clearhighlight(uuid)\n  }\n}\n"
  },
  {
    "path": "src/core/index.ts",
    "content": "export * from './fps'\nexport * from './highlight'\nexport * from './hook'\n"
  },
  {
    "path": "src/core/utils.ts",
    "content": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport { basename, classify } from '@vue/devtools-shared'\n\ninterface ComponentBoundingRect {\n  left: number\n  top: number\n  right: number\n  bottom: number\n  width: number\n  height: number\n}\n\nfunction createRect() {\n  const rect = {\n    top: 0,\n    bottom: 0,\n    left: 0,\n    right: 0,\n    get width() { return rect.right - rect.left },\n    get height() { return rect.bottom - rect.top },\n  }\n  return rect\n}\n\nconst DEFAULT_RECT = {\n  top: 0,\n  left: 0,\n  right: 0,\n  bottom: 0,\n  width: 0,\n  height: 0,\n}\n\nlet range: any\nfunction getTextRect(node: any) {\n  if (!range)\n    range = document.createRange()\n\n  range.selectNode(node)\n\n  return range.getBoundingClientRect()\n}\n\nfunction mergeRects(a: any, b: any) {\n  if (!a.top || b.top < a.top)\n    a.top = b.top\n\n  if (!a.bottom || b.bottom > a.bottom)\n    a.bottom = b.bottom\n\n  if (!a.left || b.left < a.left)\n    a.left = b.left\n\n  if (!a.right || b.right > a.right)\n    a.right = b.right\n\n  return a\n}\n\nfunction getAppRecord(instance: VueAppInstance) {\n  if (instance.__VUE_DEVTOOLS_NEXT_APP_RECORD__)\n    return instance.__VUE_DEVTOOLS_NEXT_APP_RECORD__\n  else if (instance.root)\n    return instance.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__\n}\n\nfunction isFragment(instance: VueAppInstance) {\n  const subTreeType = instance?.subTree?.type\n  if (!subTreeType) {\n    return false\n  }\n  const appRecord = getAppRecord(instance)\n  if (appRecord) {\n    return appRecord?.types?.Fragment === subTreeType\n  }\n  return false\n}\n\nfunction getFragmentRect(vnode: any) {\n  const rect = createRect()\n  if (!vnode.children)\n    return rect\n\n  for (let i = 0, l = vnode.children.length; i < l; i++) {\n    const childVnode = vnode.children[i]\n    let childRect\n    if (childVnode.component) {\n      childRect = getComponentBoundingRect(childVnode.component)\n    }\n    else if (childVnode.el) {\n      const el = childVnode.el\n      if (el.nodeType === 1 || el.getBoundingClientRect)\n        childRect = el.getBoundingClientRect()\n\n      else if (el.nodeType === 3 && el.data.trim())\n        childRect = getTextRect(el)\n    }\n    if (childRect)\n      mergeRects(rect, childRect)\n  }\n\n  return rect\n}\n\nexport function getComponentBoundingRect(instance: VueAppInstance): ComponentBoundingRect {\n  const el = instance?.subTree?.el || instance?.$el\n\n  if (typeof window === 'undefined') {\n    // @TODO: Find position from instance or a vnode (for functional components).\n    return DEFAULT_RECT\n  }\n\n  if (isFragment(instance))\n    return getFragmentRect(instance?.subTree)\n\n  else if (el?.nodeType === 1)\n    return el?.getBoundingClientRect()\n\n  else if (instance?.subTree?.component || instance?.$vnode)\n    return getComponentBoundingRect(instance?.subTree?.component || instance?.$vnode as VueAppInstance)\n  else\n    return DEFAULT_RECT\n}\n\n// ---\n\nfunction getComponentTypeName(options: VueAppInstance['type']) {\n  const name = options?.name || options?._componentTag || options?.tag || options.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ || options.__name\n  if (name === 'index' && options.__file?.endsWith('index.vue')) {\n    return ''\n  }\n  return name\n}\n\nfunction saveComponentGussedName(instance: VueAppInstance, name: string) {\n  instance.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__ = name\n  return name\n}\n\nfunction getComponentFileName(options: VueAppInstance['type']) {\n  const file = options.__file\n  if (file)\n    return classify(basename(file, '.vue'))\n}\n\nexport function getInstanceName(instance: VueAppInstance) {\n  const name = getComponentTypeName(instance?.type || instance?.$vnode || {})\n  if (name)\n    return name\n  if (instance?.root === instance || instance?.$root === instance)\n    return 'Root'\n  for (const key in instance.parent?.type?.components) {\n    if (instance.parent.type.components[key] === instance?.type)\n      return saveComponentGussedName(instance, key)\n  }\n\n  for (const key in instance.appContext?.components) {\n    if (instance.appContext.components[key] === instance?.type)\n      return saveComponentGussedName(instance, key)\n  }\n\n  const fileName = getComponentFileName(instance?.type || {})\n  if (fileName)\n    return fileName\n\n  return 'Anonymous Component'\n}\n"
  },
  {
    "path": "src/global.d.ts",
    "content": "declare global {\n  interface Window {\n    __VUE_SCAN__?: {\n      plugin: typeof import('./index').default\n      createOnBeforeUpdateHook: typeof import('./core/hook').createOnBeforeUpdateHook\n      createOnBeforeUnmountHook: typeof import('./core/hook').createOnBeforeUnmountHook\n      createOnMountedHook: typeof import('./core/hook').createOnMountedHook\n      createOnUpdatedHook: typeof import('./core/hook').createOnUpdatedHook\n    }\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport type { Plugin } from 'vue-demi'\nimport type { VueScanBaseOptions, VueScanOptions } from './types'\nimport { createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/index'\nimport { isDev } from './utils'\n\nconst plugin: Plugin<VueScanOptions> = {\n  install: (app, options?: VueScanBaseOptions) => {\n    const { enable = isDev() } = options || {}\n\n    if (!enable) {\n      return\n    }\n\n    app.mixin({\n      mounted() {\n        const instance = (() => {\n          return (this as any).$\n        })() as VueAppInstance\n\n        if (!instance.__m) {\n          instance.__m = createOnMountedHook(instance, options)\n        }\n\n        if (!instance.__bu) {\n          instance.__bu = createOnBeforeUpdateHook(instance)\n        }\n\n        if (!instance.__u) {\n          instance.__u = createOnUpdatedHook(instance, options)\n        }\n\n        if (!instance.__bum) {\n          instance.__bum = createOnBeforeUnmountHook(instance)\n        }\n\n        instance.__vue_scan_injected__ = true\n        instance.__m?.()\n      },\n      beforeUpdate() {\n        const instance = (() => {\n          return (this as any).$\n        })() as VueAppInstance\n\n        instance.__bu?.()\n      },\n      updated() {\n        const instance = (() => {\n          return (this as any).$\n        })() as VueAppInstance\n\n        instance.__u?.()\n      },\n      beforeUnmount() {\n        const instance = (() => {\n          return (this as any).$\n        })() as VueAppInstance\n\n        instance.__bum?.()\n      },\n    })\n  },\n}\n\nexport default plugin\n\nexport * from './types'\n"
  },
  {
    "path": "src/index_vue2.ts",
    "content": "import type { VueAppInstance } from '@vue/devtools-kit'\nimport type { Plugin } from 'vue-demi'\nimport type { VueScanBaseOptions } from './types'\nimport { createOnBeforeUnmountHook, createOnBeforeUpdateHook, createOnMountedHook, createOnUpdatedHook } from './core/index'\nimport { isDev } from './utils'\n\nconst plugin: Plugin<VueScanBaseOptions> = {\n  install: (app, options?: VueScanBaseOptions) => {\n    const { enable = isDev() } = options || {}\n\n    if (!enable) {\n      return\n    }\n\n    app.mixin({\n      mounted() {\n        const instance = this as VueAppInstance\n\n        if (!instance.__m) {\n          instance.__m = createOnMountedHook(instance, options)\n        }\n\n        if (!instance.__bu) {\n          instance.__bu = createOnBeforeUpdateHook(instance)\n        }\n\n        if (!instance.__u) {\n          instance.__u = createOnUpdatedHook(instance, options)\n        }\n\n        if (!instance.__bum) {\n          instance.__bum = createOnBeforeUnmountHook(instance)\n        }\n\n        instance.__vue_scan_injected__ = true\n        instance.__m?.()\n      },\n      beforeUpdate() {\n        const instance = this as VueAppInstance\n\n        instance.__bu?.()\n      },\n      updated() {\n        const instance = this as VueAppInstance\n\n        instance.__u?.()\n      },\n      beforeDestroy() {\n        const instance = this as VueAppInstance\n\n        instance.__bum?.()\n      },\n    })\n  },\n}\n\nexport default plugin\n\nexport * from './types'\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type { HighlightCanvasOptions } from './core'\n\nexport interface VueScanBaseOptions extends HighlightCanvasOptions {\n  enable?: boolean\n  hideComponentName?: boolean\n  interval?: number\n}\n\nexport type VueScanOptions = [\n  VueScanBaseOptions | undefined,\n]\n"
  },
  {
    "path": "src/utils/MutationObserverDom.ts",
    "content": "import { throttle } from 'lodash-es'\n\nexport function createDomMutationObserver<T extends Element>(\n  getTarget: () => T | null,\n  callback: MutationCallback,\n  options?: MutationObserverInit,\n  throttleWait: number = 200,\n) {\n  const targetObserver = new MutationObserver(throttle(callback, throttleWait))\n\n  const findTargetObserver = new MutationObserver(throttle(() => {\n    const target = getTarget()\n    if (target) {\n      findTargetObserver.disconnect()\n      targetObserver.observe(target, options)\n    }\n  }, 200))\n\n  findTargetObserver.observe(document.body, {\n    childList: true,\n    subtree: true,\n  })\n\n  return targetObserver\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "export function isDev() {\n  return (import.meta.env && import.meta.env.DEV === true)\n    // eslint-disable-next-line node/prefer-global/process\n    || (process.env.NODE_ENV === 'development')\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"moduleDetection\": \"force\",\n    \"rootDir\": \".\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipDefaultLibCheck\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "uno.config.ts",
    "content": "import config from './packages/extension/uno.config'\n\nexport default config\n"
  }
]