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