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