├── .nvmrc ├── license ├── packages ├── vscode │ ├── license │ ├── .vscodeignore │ ├── images │ │ ├── demo1.png │ │ ├── demo2.png │ │ ├── analyze.png │ │ └── analyze-light.png │ ├── src │ │ ├── themes │ │ │ ├── index.ts │ │ │ ├── dark.ts │ │ │ └── light.ts │ │ ├── config.ts │ │ ├── generated-meta.ts │ │ └── analyze.ts │ ├── nodemon.json │ ├── tsconfig.json │ ├── README.md │ ├── tsdown.config.ts │ ├── script │ │ └── index.ts │ ├── syntaxes │ │ └── vho.output.tmLanguage │ └── package.json ├── core │ ├── test │ │ ├── output │ │ │ ├── vue │ │ │ │ ├── options-setup-render.vue.nodes.txt │ │ │ │ ├── options-base-jsx.vue.nodes.txt │ │ │ │ ├── options-base-jsx2.vue.nodes.txt │ │ │ │ ├── tsx2.vue.nodes.txt │ │ │ │ ├── options-setup-jsx.vue.nodes.txt │ │ │ │ ├── tsx1.vue.nodes.txt │ │ │ │ ├── options-base.vue.nodes.txt │ │ │ │ ├── options-base-2.vue.nodes.txt │ │ │ │ ├── options-base-defineComponent.vue.nodes.txt │ │ │ │ ├── options-setup.vue.nodes.txt │ │ │ │ ├── setup-block.vue.nodes.txt │ │ │ │ ├── options-base-jsx.vue.graph.txt │ │ │ │ ├── options-base-jsx2.vue.graph.txt │ │ │ │ ├── tsx2.vue.graph.txt │ │ │ │ ├── options-base-defineComponent.vue.graph.txt │ │ │ │ ├── options-base-2.vue.graph.txt │ │ │ │ ├── options-base.vue.graph.txt │ │ │ │ ├── options-setup-jsx.vue.graph.txt │ │ │ │ └── options-setup-render.vue.graph.txt │ │ │ └── react │ │ │ │ ├── tsx4.jsx.nodes.txt │ │ │ │ ├── tsx3.jsx.nodes.txt │ │ │ │ └── tsx4.jsx.graph.txt │ │ ├── suggest │ │ │ ├── index.spec.ts │ │ │ ├── utils.test.ts │ │ │ └── splitGraph.test.ts │ │ ├── mermaid.test.ts │ │ ├── utils.test.ts │ │ └── fixtures.test.ts │ ├── nodemon.json │ ├── src │ │ ├── index.ts │ │ ├── analyze │ │ │ ├── index.ts │ │ │ ├── template.ts │ │ │ ├── style.ts │ │ │ └── utils.ts │ │ ├── mermaid.ts │ │ ├── suggest │ │ │ ├── utils.ts │ │ │ ├── split.ts │ │ │ ├── index.ts │ │ │ └── filter.ts │ │ └── vis.ts │ ├── tsdown.config.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── package.json ├── playground │ ├── server │ │ ├── tsconfig.json │ │ └── api │ │ │ └── analyze.ts │ ├── public │ │ └── favicon.ico │ ├── components │ │ └── codemirror │ │ │ ├── README.md │ │ │ ├── codemirror.ts │ │ │ └── CodeMirror.vue │ ├── tsconfig.json │ ├── unocss.config.ts │ ├── nuxt.config.ts │ ├── package.json │ ├── composables │ │ ├── useSearch.ts │ │ └── useResize.ts │ ├── README.md │ └── default-codes │ │ ├── optionsBase.vue │ │ ├── reactFunction.jsx │ │ ├── reactClass.jsx │ │ ├── compositionBase.vue │ │ └── tsx.vue ├── eslint │ ├── src │ │ ├── types.d.ts │ │ ├── rules │ │ │ ├── not-used.md │ │ │ ├── loop-call.md │ │ │ ├── linear-path.md │ │ │ ├── loop-call.ts │ │ │ ├── linear-path.ts │ │ │ └── not-used.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── tsdown.config.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md └── mcp │ ├── tsdown.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── src │ ├── index.ts │ └── analyze.ts ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── images ├── sponsor.png └── playground1.png ├── unocss.config.ts ├── .npmrc ├── .vscode ├── extensions.json ├── tasks.json ├── launch.json └── settings.json ├── .gitignore ├── fixtures ├── react │ ├── tsx4.jsx │ └── tsx3.jsx └── vue │ ├── tsx2.vue │ ├── options-base-jsx2.vue │ ├── options-base-jsx.vue │ ├── options-setup-jsx.vue │ ├── tsx1.vue │ ├── options-base-defineComponent.vue │ ├── options-base.vue │ ├── options-base-2.vue │ ├── setup-block.vue │ └── options-setup.vue ├── package.json ├── pnpm-workspace.yaml ├── README_cn.md ├── eslint.config.mjs └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT -------------------------------------------------------------------------------- /packages/vscode/license: -------------------------------------------------------------------------------- 1 | MIT -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: huali58081 2 | custom: ['https://afdian.com/a/huali08'] 3 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-setup-render.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "count", 3 | } -------------------------------------------------------------------------------- /images/sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/images/sponsor.png -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-jsx.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "count", 3 | "plus", 4 | } -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-jsx2.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "count", 3 | "plus", 4 | } -------------------------------------------------------------------------------- /packages/playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /images/playground1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/images/playground1.png -------------------------------------------------------------------------------- /packages/eslint/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export type PluginOptions = [{ 2 | framework: 'vue' | 'react' 3 | }]; 4 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import config from './packages/playground/unocss.config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/tsx2.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "a-modal", 3 | "open", 4 | "focus", 5 | "aa", 6 | } -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | images/demo*.png 4 | node_modules 5 | script/*.ts 6 | src 7 | test -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | public-hoist-pattern[]=@vue* 5 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-setup-jsx.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "count", 3 | "methods", 4 | "ComponentD", 5 | } -------------------------------------------------------------------------------- /packages/vscode/images/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/packages/vscode/images/demo1.png -------------------------------------------------------------------------------- /packages/vscode/images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/packages/vscode/images/demo2.png -------------------------------------------------------------------------------- /packages/core/test/output/vue/tsx1.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "open", 3 | "writeBaseInfo", 4 | "processInfo", 5 | "xx", 6 | } -------------------------------------------------------------------------------- /packages/vscode/images/analyze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/packages/vscode/images/analyze.png -------------------------------------------------------------------------------- /packages/vscode/src/themes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as dark } from './dark'; 2 | export { default as light } from './light'; 3 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "msg", 3 | "plus", 4 | "count", 5 | "number", 6 | "add", 7 | } -------------------------------------------------------------------------------- /packages/playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/packages/playground/public/favicon.ico -------------------------------------------------------------------------------- /packages/core/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts,json", 6 | "exec": "ts-node ./src/index.ts" 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-2.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "msg", 3 | "plus", 4 | "count", 5 | "number", 6 | "add", 7 | } -------------------------------------------------------------------------------- /packages/vscode/images/analyze-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/vue-hook-optimizer/HEAD/packages/vscode/images/analyze-light.png -------------------------------------------------------------------------------- /packages/core/test/output/react/tsx4.jsx.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "open", 3 | "writeBaseInfo", 4 | "processInfo", 5 | "setOpen", 6 | "xx", 7 | } -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-defineComponent.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "msg", 3 | "plus", 4 | "count", 5 | "number", 6 | "add", 7 | } -------------------------------------------------------------------------------- /packages/playground/components/codemirror/README.md: -------------------------------------------------------------------------------- 1 | Reference 2 | 3 | [vuejs/repl/codemirror](https://github.com/vuejs/repl/tree/main/src/codemirror) 4 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-setup.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "count", 3 | "ComponentD", 4 | "msgRef", 5 | "msg", 6 | "plus", 7 | "number", 8 | "add", 9 | } -------------------------------------------------------------------------------- /packages/vscode/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src", 4 | "./script/index.ts" 5 | ], 6 | "ext": "ts,json", 7 | "exec": "tsc ./script/index.ts & tsdown --env.NODE_ENV development" 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/setup-block.vue.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "lmsg", 3 | "path", 4 | "age", 5 | "userinfo", 6 | "data", 7 | "updateName", 8 | "addAge", 9 | "add1111", 10 | "add2222", 11 | "add333", 12 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "bradlc.vscode-tailwindcss", 5 | "antfu.unocss", 6 | "yoavbls.pretty-ts-errors", 7 | "amodio.tsl-problem-matcher", 8 | "emeraldwalk.runonsave" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/test/output/react/tsx3.jsx.nodes.txt: -------------------------------------------------------------------------------- 1 | Set { 2 | "uiThemeConfig", 3 | "ext1", 4 | "wsInited", 5 | "terminalFullScreen", 6 | "confsCss", 7 | "themeProps", 8 | "store", 9 | "cpConf", 10 | "upgradeInfo", 11 | "installSrc", 12 | "outerProps", 13 | } -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './analyze'; 2 | export type { TypedNode } from './analyze/utils'; 3 | export type { NodeType, RelationType } from './analyze/utils'; 4 | export * from './mermaid'; 5 | export * from './suggest'; 6 | export { getVisData } from './vis'; 7 | export { parse } from '@vue/compiler-sfc'; 8 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "paths": { 7 | "vue-hook-optimizer": [ 8 | "../../packages/core/src" 9 | ] 10 | } 11 | }, 12 | "exclude": [ 13 | "./default-codes" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/analyze/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | analyze as analyzeOptions, 3 | } from './options'; 4 | export { 5 | analyze as analyzeSetupScript, 6 | } from './setupScript'; 7 | export { 8 | analyze as analyzeStyle, 9 | } from './style'; 10 | export { 11 | analyze as analyzeTemplate, 12 | } from './template'; 13 | export { 14 | analyze as analyzeTsx, 15 | } from './tsx'; 16 | -------------------------------------------------------------------------------- /packages/core/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | sourcemap: true, 6 | clean: true, 7 | dts: true, 8 | shims: true, 9 | format: ['cjs', 'esm'], 10 | external: [ 11 | '@babel/core', 12 | '@babel/parser', 13 | '@babel/traverse', 14 | '@babel/types', 15 | '@vue/compiler-sfc', 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/playground/unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'; 2 | 3 | export default defineConfig({ 4 | presets: [ 5 | presetUno(), 6 | presetIcons({ 7 | // 其他选项 8 | prefix: 'i-', 9 | extraProperties: { 10 | display: 'inline-block', 11 | }, 12 | }), 13 | presetAttributify({ 14 | prefix: 'w:', 15 | prefixedOnly: false, 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /packages/mcp/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { defineConfig } from 'tsdown'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | export default defineConfig({ 9 | entry: ['src/index.ts'], 10 | format: ['esm'], 11 | dts: false, 12 | clean: true, 13 | alias: { 14 | 'vue-hook-optimizer': path.resolve(__dirname, '../../packages/core/src'), 15 | }, 16 | minify: true, 17 | }); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | .cache 6 | packages/playground/dist 7 | 8 | # Node dependencies 9 | node_modules 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Misc 16 | .DS_Store 17 | .fleet 18 | .idea 19 | 20 | # Local env files 21 | .env 22 | .env.* 23 | !.env.example 24 | 25 | dist 26 | node_modules 27 | **/node_modules/* 28 | .nuxt 29 | packages/vscode/dist 30 | *.vsix 31 | 32 | !packages/vscode/script/*.min.js 33 | packages/vscode/script/*.js 34 | coverage 35 | 36 | eslint-typegen.d.ts 37 | **/*/.tsbuildinfo -------------------------------------------------------------------------------- /fixtures/react/tsx4.jsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const [open, setOpen] = useState(false) 3 | 4 | const [xx] = useState('') 5 | 6 | function processInfo() { 7 | console.log(open) 8 | } 9 | 10 | const writeBaseInfo = () => { 11 | console.log(xx) 12 | } 13 | 14 | return ( 15 | processInfo()} 20 | onOk={(a) => setOpen(a)} 21 | > 22 |

hello

23 |

{xx}

24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /packages/mcp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./.tsbuildinfo", 5 | "target": "es2020", 6 | "lib": ["esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "Bundler", 9 | "paths": { 10 | "vue-hook-optimizer": [ 11 | "../../packages/core/src" 12 | ] 13 | }, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "esModuleInterop": true, 18 | "skipDefaultLibCheck": true, 19 | "skipLibCheck": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./.tsbuildinfo", 5 | "target": "es2017", 6 | "lib": ["esnext", "dom"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": [ 11 | "vitest/globals" 12 | ], 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "esModuleInterop": true, 16 | "skipDefaultLibCheck": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": [ 20 | "script/index.js" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | import * as path from 'node:path'; 3 | 4 | export default defineNuxtConfig({ 5 | devtools: false, 6 | modules: [ 7 | '@vueuse/nuxt', 8 | '@unocss/nuxt', 9 | ], 10 | alias: { 11 | 'vue-hook-optimizer': path.resolve(__dirname, '../../packages/core/src'), 12 | // https://github.com/vuejs/core/issues/10278#issuecomment-1950783863 13 | '@vue/compiler-sfc': path.resolve(__dirname, '../../node_modules/@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js'), 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/eslint/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { defineConfig } from 'tsdown'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | export default defineConfig({ 9 | entry: ['src/index.ts'], 10 | format: ['esm', 'cjs'], 11 | dts: true, 12 | clean: true, 13 | alias: { 14 | 'vue-hook-optimizer': path.resolve(__dirname, '../../packages/core/src'), 15 | }, 16 | external: [ 17 | '@typescript-eslint/utils', 18 | ], 19 | minify: true, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./.tsbuildinfo", 5 | "target": "es2020", 6 | "lib": ["esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "Bundler", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ] 13 | }, 14 | "resolveJsonModule": true, 15 | "types": [ 16 | "vitest/globals" 17 | ], 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "esModuleInterop": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true 23 | }, 24 | "exclude": ["test/output"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./.tsbuildinfo", 5 | "target": "es2020", 6 | "lib": ["esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "Bundler", 9 | "paths": { 10 | "vue-hook-optimizer": [ 11 | "../../packages/core/src" 12 | ] 13 | }, 14 | "resolveJsonModule": true, 15 | "types": [ 16 | "vitest/globals" 17 | ], 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "esModuleInterop": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | export default defineConfig({ 9 | test: { 10 | include: ['test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 11 | alias: { 12 | '@': path.resolve(__dirname, './src'), 13 | }, 14 | globals: true, 15 | coverage: { 16 | include: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 17 | provider: 'istanbul', 18 | reporter: ['text'], 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "version": "0.0.80", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt dev", 7 | "build": "nuxt build", 8 | "start": "cross-env PORT=3003 node .output/server/index.mjs" 9 | }, 10 | "dependencies": { 11 | "@iconify/json": "catalog:", 12 | "codemirror": "catalog:", 13 | "vis-network": "catalog:" 14 | }, 15 | "devDependencies": { 16 | "@types/codemirror": "catalog:", 17 | "@types/node": "catalog:", 18 | "@unocss/nuxt": "catalog:", 19 | "@vueuse/core": "catalog:", 20 | "@vueuse/nuxt": "catalog:", 21 | "nuxt": "catalog:", 22 | "unocss": "catalog:" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vscode/README.md: -------------------------------------------------------------------------------- 1 | Visual Studio Marketplace Version 2 | 3 | This is a extension for [vue-hook-optimizer](https://github.com/zcf0508/vue-hook-optimizer). And you can use it to analyze your vue code. 4 | 5 | ## Demo 6 | 7 | ![demo1](https://github.com/zcf0508/vue-hook-optimizer/raw/master/packages/vscode/images/demo1.png) 8 | 9 | ![demo2](https://github.com/zcf0508/vue-hook-optimizer/raw/master/packages/vscode/images/demo2.png) 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "dev", 9 | "path": "packages/vscode", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "problemMatcher": [ 15 | { 16 | "base": "$ts-webpack-watch", 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Build start", 20 | "endsPattern": "Build success" 21 | } 22 | } 23 | ], 24 | "group": "build" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/not-used.md: -------------------------------------------------------------------------------- 1 | # not-used 2 | 3 | Powered by [`vue-hook-optimizer`](https://github.com/zcf0508/vue-hook-optimizer). 4 | 5 | ## Rule Details 6 | ```vue 7 | // 👎 bad 8 | 14 | 15 | 18 | ``` 19 | 20 | ```vue 21 | // 👍 good 22 | 28 | 29 | 32 | ``` 33 | 34 | ## Rule Config 35 | ``` 36 | { 37 | "not-used": ['warn', { 38 | framework: 'vue', // vue or react 39 | }] 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/vscode/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { defineConfig } from 'tsdown'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | export default defineConfig(options => ( 9 | { 10 | entry: ['src/index.ts'], 11 | format: ['cjs'], 12 | sourcemap: options.env?.NODE_ENV === 'development', 13 | clean: true, 14 | alias: { 15 | 'vue-hook-optimizer': path.resolve(__dirname, '../../packages/core/src'), 16 | '@vue/compiler-sfc': path.resolve(__dirname, '../../node_modules/@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js'), 17 | }, 18 | external: [ 19 | 'vscode', 20 | ], 21 | minify: true, 22 | }), 23 | ); 24 | -------------------------------------------------------------------------------- /packages/eslint/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Linter } from 'eslint'; 2 | import { version } from '../package.json'; 3 | import linearPath from './rules/linear-path'; 4 | import loopCall from './rules/loop-call'; 5 | import notUsed from './rules/not-used'; 6 | 7 | const plugin = { 8 | meta: { 9 | name: 'vue-hook-optimizer', 10 | version, 11 | }, 12 | rules: { 13 | 'not-used': notUsed, 14 | 'loop-call': loopCall, 15 | 'linear-path': linearPath, 16 | }, 17 | } satisfies ESLint.Plugin; 18 | 19 | export default plugin; 20 | 21 | type RuleDefinitions = typeof plugin['rules']; 22 | 23 | export type RuleOptions = { 24 | [K in keyof RuleDefinitions]: RuleDefinitions[K]['defaultOptions'] 25 | }; 26 | 27 | export type Rules = { 28 | [K in keyof RuleOptions]: Linter.RuleEntry 29 | }; 30 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/loop-call.md: -------------------------------------------------------------------------------- 1 | # loop-call 2 | 3 | Powered by [`vue-hook-optimizer`](https://github.com/zcf0508/vue-hook-optimizer). 4 | 5 | ## Rule Details 6 | ```vue 7 | // 👎 bad 8 | 19 | 20 | 23 | ``` 24 | 25 | ```vue 26 | // 👍 good 27 | 36 | 37 | 40 | ``` 41 | 42 | ## Rule Config 43 | ``` 44 | { 45 | "loop-call": ['error', { 46 | framework: 'vue', // vue or react 47 | }] 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /fixtures/vue/tsx2.vue: -------------------------------------------------------------------------------- 1 | 11 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.80", 3 | "private": true, 4 | "license": "MIT", 5 | "sideEffects": false, 6 | "scripts": { 7 | "build": "pnpm -r run build", 8 | "play": "npm -C packages/playground run dev", 9 | "lint": "eslint .", 10 | "test": "pnpm -r run test --run", 11 | "coverage": "pnpm -r run coverage", 12 | "typecheck": "pnpm -r run typecheck", 13 | "release": "bumpp -r" 14 | }, 15 | "devDependencies": { 16 | "@antfu/eslint-config": "catalog:", 17 | "@rolldown/pluginutils": "catalog:", 18 | "@vitest/coverage-istanbul": "catalog:", 19 | "bumpp": "catalog:", 20 | "cross-env": "catalog:", 21 | "eslint": "catalog:", 22 | "eslint-plugin-security": "catalog:", 23 | "eslint-plugin-vue-hook-optimizer": "workspace:*", 24 | "tsdown": "catalog:", 25 | "typescript": "catalog:", 26 | "vitest": "catalog:" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/test/suggest/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { analyzeSetupScript, analyzeStyle, analyzeTemplate, gen, parse } from '@/index'; 3 | 4 | const testFile = '../../fixtures/vue/setup-block.vue'; 5 | 6 | describe('suggest gen', () => { 7 | const source = readFileSync(testFile, 'utf-8'); 8 | const sfc = parse(source); 9 | it('base', () => { 10 | const graph = analyzeSetupScript( 11 | sfc.descriptor.scriptSetup?.content || '', 12 | (sfc.descriptor.scriptSetup?.loc.start.line || 1) - 1, 13 | ); 14 | 15 | const nodesUsedInStyle = analyzeStyle(sfc.descriptor.styles || []); 16 | 17 | const nodesUsedInTemplate = sfc.descriptor.template?.content 18 | ? analyzeTemplate(sfc.descriptor.template!.content) 19 | : new Set(); 20 | 21 | expect(gen(graph, nodesUsedInTemplate, nodesUsedInStyle)).toMatchFileSnapshot('../output/suggent-gen.txt'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/linear-path.md: -------------------------------------------------------------------------------- 1 | # loop-call 2 | 3 | Powered by [`vue-hook-optimizer`](https://github.com/zcf0508/vue-hook-optimizer). 4 | 5 | ## Rule Details 6 | ```vue 7 | // 👎 bad 8 | 25 | 26 | 31 | ``` 32 | 33 | ```vue 34 | // 👍 good 35 | 44 | 45 | 50 | ``` 51 | 52 | ## Rule Config 53 | ``` 54 | { 55 | "linear-path": ['error', { 56 | framework: 'vue', // vue or react 57 | }] 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/playground/composables/useSearch.ts: -------------------------------------------------------------------------------- 1 | export function useSearch( 2 | searchInputRef: Ref, 3 | chartRef: Ref, 4 | ) { 5 | const showSearchInput = ref(false); 6 | 7 | const { isOutside } = useMouseInElement(chartRef); 8 | onKeyStroke(['F', 'f', 'Command', 'Ctrl'], (e) => { 9 | // 判断是不是同时按下 10 | if (!e.metaKey && !e.ctrlKey) { 11 | return; 12 | } 13 | 14 | if (isOutside.value) { 15 | return; 16 | } 17 | e.preventDefault(); 18 | showSearchInput.value = true; 19 | nextTick(() => { 20 | if (searchInputRef.value) { 21 | searchInputRef.value.focus(); 22 | } 23 | }); 24 | }); 25 | 26 | const searchkey = ref(''); 27 | 28 | function closeSearch() { 29 | searchkey.value = ''; 30 | showSearchInput.value = false; 31 | } 32 | 33 | return { 34 | showSearchInput, 35 | searchkey, 36 | closeSearch, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "dev-in-chrome", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}", 13 | "sourceMaps": true, 14 | "runtimeArgs": ["--remote-debugging-port=9232"] 15 | }, 16 | { 17 | "name": "Extension", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode" 23 | ], 24 | "outFiles": [ 25 | "${workspaceFolder}/packages/vscode/dist/**/*.js" 26 | ], 27 | "preLaunchTask": "npm: dev" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/mcp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-vue-hook-optimizer", 3 | "type": "module", 4 | "version": "0.0.80", 5 | "description": "MCP server for vue-hook-optimizer", 6 | "author": "zcf0508 ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/zcf0508/vue-hook-optimizer", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/zcf0508/vue-hook-optimizer.git" 12 | }, 13 | "keywords": [ 14 | "vue", 15 | "hooks", 16 | "mcp" 17 | ], 18 | "sideEffects": false, 19 | "bin": { 20 | "mcp-server-vue-hook-optimizer": "dist/index.js" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "tsdown", 27 | "prepublishOnly": "npm run build", 28 | "typecheck": "tsc --noEmit" 29 | }, 30 | "devDependencies": { 31 | "@modelcontextprotocol/sdk": "catalog:", 32 | "tsdown": "catalog:", 33 | "vue-hook-optimizer": "workspace:*", 34 | "zod": "catalog:" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/playground/components/codemirror/codemirror.ts: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import 'codemirror/addon/dialog/dialog.css'; 3 | import './codemirror.css'; 4 | 5 | // modes 6 | import 'codemirror/mode/javascript/javascript.js'; 7 | import 'codemirror/mode/css/css.js'; 8 | import 'codemirror/mode/htmlmixed/htmlmixed.js'; 9 | 10 | // addons 11 | import 'codemirror/addon/edit/closebrackets.js'; 12 | import 'codemirror/addon/edit/closetag.js'; 13 | import 'codemirror/addon/comment/comment.js'; 14 | import 'codemirror/addon/fold/foldcode.js'; 15 | import 'codemirror/addon/fold/foldgutter.js'; 16 | import 'codemirror/addon/fold/brace-fold.js'; 17 | import 'codemirror/addon/fold/indent-fold.js'; 18 | import 'codemirror/addon/fold/comment-fold.js'; 19 | import 'codemirror/addon/search/search.js'; 20 | import 'codemirror/addon/search/searchcursor.js'; 21 | import 'codemirror/addon/dialog/dialog.js'; 22 | 23 | // keymap 24 | import 'codemirror/keymap/sublime.js'; 25 | 26 | export default CodeMirror; 27 | -------------------------------------------------------------------------------- /packages/playground/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on `http://localhost:3000`: 23 | 24 | ```bash 25 | # npm 26 | npm run dev 27 | 28 | # pnpm 29 | pnpm run dev 30 | 31 | # yarn 32 | yarn dev 33 | ``` 34 | 35 | ## Production 36 | 37 | Build the application for production: 38 | 39 | ```bash 40 | # npm 41 | npm run build 42 | 43 | # pnpm 44 | pnpm run build 45 | 46 | # yarn 47 | yarn build 48 | ``` 49 | 50 | Locally preview production build: 51 | 52 | ```bash 53 | # npm 54 | npm run preview 55 | 56 | # pnpm 57 | pnpm run preview 58 | 59 | # yarn 60 | yarn preview 61 | ``` 62 | 63 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 64 | -------------------------------------------------------------------------------- /packages/mcp/README.md: -------------------------------------------------------------------------------- 1 | # Vue Hook Optimizer MCP Server 2 | 3 | Node.js server implementing Model Context Protocol (MCP) for analyzing and optimizing Vue component hooks. 4 | 5 | ## Features 6 | 7 | - Generate Mermaid diagrams for analyze Vue component hooks and their relationships 8 | - Provide optimization suggestions 9 | - Support for `vue` and `react` 10 | 11 | ## API 12 | 13 | ### Tools 14 | 15 | - **analyze** 16 | - Analyze Vue component hooks and provide optimization suggestions 17 | - Input: 18 | - `filepath` (string): Path to component file 19 | - `framework` (string): `vue` or `react` 20 | - Returns: 21 | - Mermaid diagram showing hook relationships 22 | - List of optimization suggestions 23 | 24 | ## Usage with Claude Desktop 25 | 26 | Add this to your `claude_desktop_config.json`: 27 | 28 | ```json 29 | { 30 | "mcpServers": { 31 | "vho": { 32 | "command": "npx", 33 | "args": [ 34 | "-y", 35 | "mcp-server-vue-hook-optimizer" 36 | ] 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /packages/playground/composables/useResize.ts: -------------------------------------------------------------------------------- 1 | export function useResize(containerRef: Ref) { 2 | const state = reactive({ 3 | dragging: false, 4 | split: 50, 5 | }); 6 | 7 | const boundSplit = computed(() => { 8 | const { split } = state; 9 | return split < 10 10 | ? 10 11 | : split > 50 12 | ? 50 13 | : split; 14 | }); 15 | 16 | let startPosition = 0; 17 | let startSplit = 0; 18 | 19 | function dragStart(e: MouseEvent) { 20 | state.dragging = true; 21 | startPosition = e.pageX; 22 | startSplit = boundSplit.value; 23 | } 24 | 25 | function dragMove(e: MouseEvent) { 26 | if (state.dragging) { 27 | const position = e.pageX; 28 | const totalSize = containerRef.value!.offsetWidth; 29 | const dp = position - startPosition; 30 | state.split = startSplit + ~~((dp / totalSize) * 100); 31 | } 32 | } 33 | 34 | function dragEnd() { 35 | state.dragging = false; 36 | } 37 | 38 | return { 39 | boundSplit, 40 | dragStart, 41 | dragMove, 42 | dragEnd, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | catalog: 4 | '@antfu/eslint-config': ^5.2.2 5 | '@babel/preset-typescript': ^7.24.1 6 | '@iconify/json': ^2.2.205 7 | '@modelcontextprotocol/sdk': ^1.15.1 8 | '@rolldown/pluginutils': 1.0.0-beta.34 9 | '@types/babel__core': ^7.20.5 10 | '@types/babel__traverse': ^7.20.5 11 | '@types/codemirror': ^5.60.15 12 | '@types/eslint': ^9.6.1 13 | '@types/lodash-es': ^4.17.12 14 | '@types/node': ^20.12.7 15 | '@types/uuid': ^9.0.8 16 | '@typescript-eslint/utils': ^8.42.0 17 | '@unocss/nuxt': ^0.54.3 18 | '@vitest/coverage-istanbul': ^3.2.4 19 | '@vscode/vsce': ^2.26.0 20 | '@vueuse/core': ^10.9.0 21 | '@vueuse/nuxt': ^10.9.0 22 | bumpp: ^9.4.1 23 | codemirror: ^5.65.16 24 | cross-env: ^7.0.3 25 | eslint: ^9.34.0 26 | eslint-define-config: ^2.1.0 27 | eslint-plugin-security: ^3.0.1 28 | fast-glob: ^3.3.2 29 | lodash-es: ^4.17.21 30 | nodemon: ^3.1.0 31 | nuxt: ^3.11.2 32 | rimraf: ^5.0.5 33 | ts-node: ^10.9.2 34 | tsdown: ^0.14.2 35 | typescript: ^5.4.5 36 | unocss: ^0.54.3 37 | vis-network: ^9.1.9 38 | vitest: ^3.2.4 39 | vscode-ext-gen: ^0.3.1 40 | zod: ^3.24.2 41 | -------------------------------------------------------------------------------- /fixtures/vue/options-base-jsx2.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | -------------------------------------------------------------------------------- /fixtures/vue/options-base-jsx.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | -------------------------------------------------------------------------------- /packages/vscode/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as meta from './generated-meta'; 3 | import { dark, light } from './themes'; 4 | 5 | export function getVisConfigByTheme() { 6 | const config = vscode.workspace.getConfiguration(); 7 | const theme = config.get(meta.configs.vhoTheme.key, meta.configs.vhoTheme.default); 8 | 9 | if (theme === 'auto') { 10 | const themeKind = vscode.window.activeColorTheme.kind; 11 | if (themeKind === 2 || themeKind === 3) { 12 | return dark; 13 | } 14 | else if (themeKind === 1 || themeKind === 4) { 15 | return light; 16 | } 17 | else { 18 | return light; 19 | } 20 | } 21 | else if (theme === 'light') { 22 | return light; 23 | } 24 | else if (theme === 'dark') { 25 | return dark; 26 | } 27 | } 28 | 29 | export function getHighlightConfig() { 30 | const config = vscode.workspace.getConfiguration(); 31 | return config.get(meta.configs.vhoHighlight.key, meta.configs.vhoHighlight.default); 32 | } 33 | 34 | export function getLauguageConfig() { 35 | const config = vscode.workspace.getConfiguration(); 36 | return config.get(meta.configs.vhoLanguage.key, meta.configs.vhoLanguage.default) || 'vue'; 37 | } 38 | -------------------------------------------------------------------------------- /fixtures/vue/options-setup-jsx.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | -------------------------------------------------------------------------------- /packages/vscode/src/themes/dark.ts: -------------------------------------------------------------------------------- 1 | const used_bg = '#9dc2f9'; 2 | const used_border = '#3d7de4'; 3 | const used_highlight_bg = '#9dc2f9'; 4 | const used_highlight_border = '#3d7de4'; 5 | const used_font_color = '#fff'; 6 | 7 | const normal_bg = '#ddd'; 8 | const normal_border = '#ccc'; 9 | const normal_highlight_bg = '#ddd'; 10 | const normal_highlight_border = '#ccc'; 11 | const normal_font_color = '#c8c9cc'; 12 | 13 | const options = { 14 | vis: { 15 | groups: { 16 | used: { 17 | color: { 18 | border: used_border, 19 | background: used_bg, 20 | highlight: { 21 | border: used_highlight_border, 22 | background: used_highlight_bg, 23 | }, 24 | }, 25 | font: { 26 | color: used_font_color, 27 | }, 28 | }, 29 | normal: { 30 | color: { 31 | border: normal_border, 32 | background: normal_bg, 33 | highlight: { 34 | border: normal_highlight_border, 35 | background: normal_highlight_bg, 36 | }, 37 | }, 38 | font: { 39 | color: normal_font_color, 40 | }, 41 | }, 42 | }, 43 | }, 44 | legend: { 45 | used: used_bg, 46 | normal: normal_bg, 47 | variant: '#fff', 48 | func: '#fff', 49 | }, 50 | }; 51 | 52 | export default options; 53 | -------------------------------------------------------------------------------- /packages/vscode/src/themes/light.ts: -------------------------------------------------------------------------------- 1 | const used_bg = '#9dc2f9'; 2 | const used_border = '#3d7de4'; 3 | const used_highlight_bg = '#9dc2f9'; 4 | const used_highlight_border = '#3d7de4'; 5 | const used_font_color = '#000'; 6 | 7 | const normal_bg = '#ddd'; 8 | const normal_border = '#ccc'; 9 | const normal_highlight_bg = '#ddd'; 10 | const normal_highlight_border = '#ccc'; 11 | const normal_font_color = '#000'; 12 | 13 | const options = { 14 | vis: { 15 | groups: { 16 | used: { 17 | color: { 18 | border: used_border, 19 | background: used_bg, 20 | highlight: { 21 | border: used_highlight_border, 22 | background: used_highlight_bg, 23 | }, 24 | }, 25 | font: { 26 | color: used_font_color, 27 | }, 28 | }, 29 | normal: { 30 | color: { 31 | border: normal_border, 32 | background: normal_bg, 33 | highlight: { 34 | border: normal_highlight_border, 35 | background: normal_highlight_bg, 36 | }, 37 | }, 38 | font: { 39 | color: normal_font_color, 40 | }, 41 | }, 42 | }, 43 | }, 44 | legend: { 45 | used: used_bg, 46 | normal: normal_bg, 47 | variant: '#000', 48 | func: '#000', 49 | }, 50 | }; 51 | 52 | export default options; 53 | -------------------------------------------------------------------------------- /packages/vscode/script/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error vscode 2 | const vscode = acquireVsCodeApi(); 3 | 4 | let network: any; 5 | 6 | function init(dataString: string, theme: string) { 7 | // 获取容器 8 | const container = document.getElementById('mynetwork'); 9 | 10 | // 将数据赋值给vis 数据格式化器 11 | const data = JSON.parse(dataString); 12 | const options = Object.assign({ 13 | physics: { 14 | solver: 'forceAtlas2Based', 15 | forceAtlas2Based: { 16 | gravitationalConstant: -100, 17 | }, 18 | }, 19 | }, JSON.parse(theme)); 20 | 21 | // 初始化关系图 22 | // @ts-expect-error window.vis 23 | network = new vis.Network(container, data, options); 24 | 25 | // 监听节点点击事件 26 | network.on('click', (event: any) => { 27 | const { nodes } = event; 28 | if (nodes.length) { 29 | onNodeClick(data.nodes.find((node: any) => node.id === nodes[0])?.info); 30 | } 31 | }); 32 | } 33 | 34 | function findSearchContainer() { 35 | return document.getElementById('SearchInputContainer'); 36 | } 37 | 38 | function findSearchInput() { 39 | return document.getElementById('searchInput') as HTMLInputElement | null; 40 | } 41 | 42 | function onNodeClick(info?: { line: number, column: number }) { 43 | console.log(info); 44 | if (!info) { 45 | return; 46 | } 47 | vscode.postMessage({ 48 | command: 'nodeClick', 49 | info, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/test/mermaid.test.ts: -------------------------------------------------------------------------------- 1 | import type { RelationType, TypedNode } from '@/analyze/utils'; 2 | import { NodeType } from '@/analyze/utils'; 3 | import { getMermaidText } from '@/mermaid'; 4 | 5 | describe('getMermaidText', () => { 6 | it('应该生成正确的 Mermaid 流程图文本', () => { 7 | const node1: TypedNode = { 8 | label: 'count', 9 | type: NodeType.var, 10 | info: { used: new Set() }, 11 | }; 12 | const node2: TypedNode = { 13 | label: 'increment', 14 | type: NodeType.fun, 15 | info: { used: new Set() }, 16 | }; 17 | 18 | const nodes: Set = new Set([node1, node2]); 19 | const edges: Map> = new Map(); 20 | edges.set(node2, new Set([{ node: node1, type: 'get' }])); 21 | 22 | const nodesUsedInTemplate: Set = new Set(['count']); 23 | const nodesUsedInStyle: Set = new Set(); 24 | 25 | const result: string = getMermaidText( 26 | { nodes, edges }, 27 | nodesUsedInTemplate, 28 | nodesUsedInStyle, 29 | { direction: 'LR' }, 30 | ); 31 | 32 | expect(result).toMatchInlineSnapshot(` 33 | "flowchart LR 34 | %% Legend: 35 | %% () = variable node 36 | %% [] = function node 37 | %% * suffix = unused in template/style 38 | %% A --> B means A depends on B 39 | count(count) 40 | increment[increment*] 41 | increment --> count 42 | " 43 | `); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-vue-hook-optimizer", 3 | "type": "module", 4 | "version": "0.0.80", 5 | "description": "vue-hook-optimizer eslint plugin", 6 | "author": "zcf0508 ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/antfu", 9 | "homepage": "https://github.com/zcf0508/vue-hook-optimizer/tree/master/packages/eslint", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/zcf0508/vue-hook-optimizer.git", 13 | "directory": "eslint" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/zcf0508/vue-hook-optimizer/issues" 17 | }, 18 | "sponsor": { 19 | "url": "https://github.com/sponsors/antfu" 20 | }, 21 | "keywords": [], 22 | "sideEffects": false, 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.ts", 26 | "import": "./dist/index.js", 27 | "require": "./dist/index.cjs" 28 | } 29 | }, 30 | "main": "./dist/index.js", 31 | "module": "./dist/index.js", 32 | "types": "./dist/index.d.ts", 33 | "files": [ 34 | "dist" 35 | ], 36 | "scripts": { 37 | "build": "tsdown", 38 | "prepublishOnly": "npm run build", 39 | "typecheck": "tsc --noEmit" 40 | }, 41 | "peerDependencies": { 42 | "eslint": "*" 43 | }, 44 | "devDependencies": { 45 | "@types/eslint": "catalog:", 46 | "@typescript-eslint/utils": "catalog:", 47 | "eslint": "catalog:", 48 | "eslint-define-config": "catalog:", 49 | "tsdown": "catalog:" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/src/mermaid.ts: -------------------------------------------------------------------------------- 1 | import type { RelationType, TypedNode } from './analyze/utils'; 2 | 3 | interface MermaidOptions { 4 | direction?: 'TB' | 'BT' | 'LR' | 'RL' 5 | } 6 | 7 | export function getMermaidText( 8 | graph: { 9 | nodes: Set 10 | edges: Map> 11 | }, 12 | nodesUsedInTemplate: Set, 13 | nodesUsedInStyle: Set = new Set(), 14 | options: MermaidOptions = {}, 15 | ): string { 16 | const direction: 'TB' | 'BT' | 'LR' | 'RL' = options.direction || 'TB'; 17 | const usedNodes: Set = new Set([...nodesUsedInTemplate, ...nodesUsedInStyle]); 18 | 19 | let mermaidText: string = `flowchart ${direction} 20 | %% Legend: 21 | %% () = variable node 22 | %% [] = function node 23 | %% * suffix = unused in template/style 24 | %% A --> B means A depends on B\n`; 25 | 26 | graph.nodes.forEach((node: TypedNode) => { 27 | const shape: string = node.type === 'var' 28 | ? '(' 29 | : '['; 30 | const closeShape: string = node.type === 'var' 31 | ? ')' 32 | : ']'; 33 | const unusedSuffix: string = !(usedNodes.has(node.label) || node.info?.used?.size) 34 | ? '*' 35 | : ''; 36 | mermaidText += ` ${node.label}${shape}${node.label}${unusedSuffix}${closeShape}\n`; 37 | }); 38 | 39 | graph.edges.forEach((edge, key) => { 40 | edge.forEach((to) => { 41 | if (!to || !to.node.label) { 42 | return; 43 | } 44 | mermaidText += ` ${key.label} --> ${to.node.label}\n`; 45 | }); 46 | }); 47 | 48 | return mermaidText; 49 | } 50 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://img.shields.io/npm/v/vue-hook-optimizer?color=a1b858&label=)](https://www.npmjs.com/package/vue-hook-optimizer) 2 | Visual Studio Marketplace Version 3 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/zcf0508/vue-hook-optimizer) 4 | 5 | 这是一个用来分析组件代码的工具。它支持 `Vue` 和 `React`。访问 [playground](vue-hook-optimizer.vercel.app/) 或者试试 vscode 扩展 [vue-hook-optimizer-ext](https://marketplace.visualstudio.com/items?itemName=zcf0508.vue-hook-optimizer-ext)。 6 | 7 | ## 安装和运行 8 | 9 | ```bash 10 | # 克隆仓库并安装依赖 11 | pnpm install 12 | # 运行 playground 13 | pnpm run play 14 | ``` 15 | 16 | 打开浏览器并访问 `http://localhost:3000/`. 17 | 18 | ## 如何使用 19 | 20 | 1. 把你的组件代码粘贴到编辑器中 21 | 22 | 2. 点击 `Analyze` 按钮 23 | 24 | 这个工具会分析代码,并展示变量和方法之间的关联关系。这是一个简单的示例。 25 | 26 | ![playground](./images/playground1.png) 27 | 28 | ## 动机 29 | 30 | 有时我们不得不重构代码,可能一个文件里有成千上万行代码。太复杂难以理解。 31 | 32 | 所以我想开发一个工具来帮助我们分析代码,并找出变量和方法之间的关联关系。我们可以发现一些变量是孤立的,一些方法是过度关联的,然后我们可以重构它们。 33 | 34 | ## 开发计划 35 | 36 | - [x] 增加更多细信息, 包括变量类型、注释、是否在模板或者 hook 方法中被使用 37 | - [x] 提供优化建议 38 | - [x] 支持 `options api` 39 | - [x] [vscode 扩展](./packages/vscode) 40 | - [x] 支持 `React` 41 | - [x] eslint 规则 42 | - [x] mcp server 43 | 44 | ## 贡献 45 | 46 | 欢迎贡献代码,提交 PR。 47 | 48 | ## 支持我 49 | 50 | 如果你喜欢这个工具,请考虑支持我。我将继续努力开发这个工具,并添加更多功能。 51 | 52 | ![sponsor](./images/sponsor.png) 53 | 54 | ## License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | merge_group: {} 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 22.x 23 | 24 | - name: Setup 25 | run: npm i -g pnpm@9 26 | 27 | - name: Install 28 | run: pnpm i --frozen-lockfile 29 | 30 | - name: Build 31 | run: pnpm -C packages/core run build 32 | 33 | - name: Build eslint 34 | run: pnpm -C packages/eslint run build 35 | 36 | - name: Lint 37 | run: pnpm run lint 38 | 39 | test: 40 | runs-on: ${{ matrix.os }} 41 | 42 | strategy: 43 | matrix: 44 | os: [ubuntu-latest] 45 | node_version: [20, 22, 24] 46 | include: 47 | - os: macos-latest 48 | node_version: 22 49 | - os: windows-latest 50 | node_version: 22 51 | fail-fast: false 52 | 53 | steps: 54 | - name: Set git to use LF 55 | run: | 56 | git config --global core.autocrlf false 57 | git config --global core.eol lf 58 | 59 | - uses: actions/checkout@v3 60 | - name: Set node ${{ matrix.node }} 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node }} 64 | 65 | - name: Setup 66 | run: npm i -g pnpm@9 67 | 68 | - name: Install 69 | run: pnpm i --frozen-lockfile 70 | 71 | - name: Build 72 | run: pnpm run build 73 | 74 | - name: Coverage 75 | run: pnpm run coverage 76 | 77 | - name: Typecheck 78 | run: pnpm run typecheck 79 | -------------------------------------------------------------------------------- /packages/core/src/suggest/utils.ts: -------------------------------------------------------------------------------- 1 | import type { RelationType, TypedNode } from '../analyze/utils'; 2 | 3 | export function hasCycle( 4 | graph: Map>, 5 | ): { hasCycle: boolean, cycleNodes: TypedNode[] } { 6 | const visited: Set = new Set(); 7 | const onStack: Set = new Set(); 8 | const stack: TypedNode[] = []; 9 | let cycleNodes: TypedNode[] = []; 10 | 11 | function dfs(node: TypedNode): boolean { 12 | if (visited.has(node)) { 13 | if (onStack.has(node)) { 14 | // 只有当环中所有边都是写(set) 或全是 调用(call) 才算循环 15 | const idx = stack.indexOf(node); 16 | const cycle = stack.slice(idx); 17 | const allNotGet = cycle.every((curr, i) => { 18 | const next = cycle[(i + 1) % cycle.length]; 19 | return Array.from(graph.get(curr) || []).some( 20 | edge => edge.node === next && edge.type !== 'get', 21 | ); 22 | }); 23 | 24 | if (allNotGet) { 25 | cycleNodes = cycle; 26 | return true; 27 | } 28 | return false; 29 | } 30 | return false; 31 | } 32 | 33 | visited.add(node); 34 | onStack.add(node); 35 | stack.push(node); 36 | 37 | for (const neighbor of (graph.get(node) || new Set())) { 38 | // 检查自环:自己依赖自己且 type 为 'set' 也算环 39 | if (neighbor.node === node && neighbor.type !== 'get') { 40 | cycleNodes = [node]; 41 | return true; 42 | } 43 | if (dfs(neighbor.node)) { 44 | return true; 45 | } 46 | } 47 | 48 | onStack.delete(node); 49 | stack.pop(); 50 | return false; 51 | } 52 | 53 | for (const [node, targets] of graph) { 54 | if (dfs(node)) { 55 | return { hasCycle: true, cycleNodes }; 56 | } 57 | } 58 | 59 | return { hasCycle: false, cycleNodes: [] }; 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/analyze/template.ts: -------------------------------------------------------------------------------- 1 | import _traverse from '@babel/traverse'; 2 | import { babelParse, compileTemplate } from '@vue/compiler-sfc'; 3 | 4 | const traverse: typeof _traverse 5 | // @ts-expect-error unwarp default 6 | = _traverse.default?.default || _traverse.default || _traverse; 7 | 8 | export function analyze( 9 | content: string, 10 | ) { 11 | const id = 'template'; 12 | const { code } = compileTemplate({ 13 | id, 14 | source: content, 15 | filename: `${id}.js`, 16 | }); 17 | 18 | // console.log(code); 19 | const ast = babelParse(code, { sourceType: 'module', plugins: [ 20 | 'typescript', 21 | ] }); 22 | 23 | // ---- 24 | 25 | const nodes = new Set(); 26 | 27 | traverse(ast, { 28 | MemberExpression(path) { 29 | if (path.type === 'MemberExpression') { 30 | if (path.node.object && path.node.object.type === 'Identifier' && path.node.object.name === '_ctx') { 31 | if (path.node.property && path.node.property.type === 'Identifier') { 32 | nodes.add(path.node.property.name); 33 | } 34 | } 35 | } 36 | }, 37 | ObjectProperty(path) { 38 | if (path.node.key.type === 'Identifier' && path.node.key.name === 'ref') { 39 | if (path.node.value.type === 'StringLiteral') { 40 | const name = path.node.value.value; 41 | if (name) { 42 | nodes.add(name); 43 | } 44 | } 45 | } 46 | }, 47 | // component 48 | CallExpression(path) { 49 | if (path.node.callee.type === 'Identifier' && path.node.callee.name === '_resolveComponent') { 50 | if (path.node.arguments[0].type === 'StringLiteral') { 51 | const name = path.node.arguments[0].value; 52 | if (name) { 53 | nodes.add(name); 54 | } 55 | } 56 | } 57 | }, 58 | }); 59 | 60 | return nodes; 61 | } 62 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.useFlatConfig": true, 4 | // Disable the default formatter, use eslint instead 5 | "prettier.enable": false, 6 | "editor.formatOnSave": false, 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { 15 | "rule": "style/*", 16 | "severity": "off" 17 | }, 18 | { 19 | "rule": "format/*", 20 | "severity": "off" 21 | }, 22 | { 23 | "rule": "*-indent", 24 | "severity": "off" 25 | }, 26 | { 27 | "rule": "*-spacing", 28 | "severity": "off" 29 | }, 30 | { 31 | "rule": "*-spaces", 32 | "severity": "off" 33 | }, 34 | { 35 | "rule": "*-order", 36 | "severity": "off" 37 | }, 38 | { 39 | "rule": "*-dangle", 40 | "severity": "off" 41 | }, 42 | { 43 | "rule": "*-newline", 44 | "severity": "off" 45 | }, 46 | { 47 | "rule": "*quotes", 48 | "severity": "off" 49 | }, 50 | { 51 | "rule": "*semi", 52 | "severity": "off" 53 | } 54 | ], 55 | // Enable eslint for all supported languages 56 | "eslint.validate": [ 57 | "javascript", 58 | "javascriptreact", 59 | "typescript", 60 | "typescriptreact", 61 | "vue", 62 | "html", 63 | "markdown", 64 | "json", 65 | "jsonc", 66 | "yaml", 67 | "toml" 68 | ], 69 | "editor.tabSize": 2, 70 | "editor.detectIndentation": false, 71 | "editor.insertSpaces": true, 72 | "emeraldwalk.runonsave": { 73 | "commands": [ 74 | { 75 | "match": "packages/vscode/package.json", 76 | "isAsync": true, 77 | "cmd": "npm -C packages/vscode run update & npx eslint packages/vscode/src/generated-meta.ts --fix" 78 | } 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | id-token: write 12 | contents: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 22 23 | registry-url: 'https://registry.npmjs.org' 24 | 25 | - run: npx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | 29 | - name: Setup 30 | run: npm i -g pnpm@9 31 | 32 | - name: Install Dependencies 33 | run: pnpm i --frozen-lockfile 34 | 35 | - name: Publish to NPM 36 | run: pnpm publish --access public --no-git-checks 37 | working-directory: ./packages/core 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 40 | NPM_CONFIG_PROVENANCE: true 41 | 42 | - name: Publish eslint-plugin to NPM 43 | run: pnpm publish --access public --no-git-checks 44 | working-directory: ./packages/eslint 45 | env: 46 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 47 | NPM_CONFIG_PROVENANCE: true 48 | 49 | - name: Publish mcp to NPM 50 | run: pnpm publish --access public --no-git-checks 51 | working-directory: ./packages/mcp 52 | env: 53 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 54 | NPM_CONFIG_PROVENANCE: true 55 | 56 | - name: Publish to VSCE 57 | run: | 58 | VERSION=$(node -p "require('./package.json').version") 59 | if [[ "$VERSION" == *"-beta"* ]]; then 60 | pnpm vsce publish --no-dependencies --pre-release -p ${{secrets.VSCE_TOKEN}} 61 | else 62 | pnpm vsce publish --no-dependencies -p ${{secrets.VSCE_TOKEN}} 63 | fi 64 | working-directory: ./packages/vscode 65 | env: 66 | VSCE_TOKEN: ${{secrets.VSCE_TOKEN}} 67 | -------------------------------------------------------------------------------- /packages/core/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/traverse'; 2 | import type * as t from '@babel/types'; 3 | import { parse } from '@babel/parser'; 4 | import traverse from '@babel/traverse'; 5 | import { isCallingNode, isWritingNode } from '../src/analyze/utils'; 6 | 7 | function findIdentifierPath(code: string, name: string) { 8 | const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] }); 9 | const found: NodePath[] = []; 10 | traverse(ast, { 11 | Identifier(path) { 12 | if (path.node.name === name) { 13 | found.push(path); 14 | // path.stop(); 15 | } 16 | }, 17 | }); 18 | return found; 19 | } 20 | 21 | describe('isWritingNode', () => { 22 | it('should return true for identifier in left side of assignment', () => { 23 | const path = findIdentifierPath('obj.a.b.c = 3;', 'obj'); 24 | expect(isWritingNode(path[0])).toBe(true); 25 | }); 26 | 27 | it('should return false for identifier in right side of assignment', () => { 28 | const path = findIdentifierPath('a = b;', 'b'); 29 | expect(isWritingNode(path[0])).toBe(false); 30 | }); 31 | 32 | it('should return true for identifier in update expression argument', () => { 33 | const path = findIdentifierPath('++count;', 'count'); 34 | expect(isWritingNode(path[0])).toBe(true); 35 | }); 36 | 37 | it('should return true for identifier in update expression argument 2', () => { 38 | const path = findIdentifierPath('count++', 'count'); 39 | expect(isWritingNode(path[0])).toBe(true); 40 | }); 41 | 42 | it('should return false for identifier not in writing context', () => { 43 | const path = findIdentifierPath('const y = z;', 'z'); 44 | expect(isWritingNode(path[0])).toBe(false); 45 | }); 46 | }); 47 | 48 | describe('isCallingNode', () => { 49 | it('should return true for identifier in call expression argument', () => { 50 | const path = findIdentifierPath(`const test = () => { 51 | console.log(test()) 52 | }`, 'test'); 53 | expect(isCallingNode(path[1])).toBe(true); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hook-optimizer", 3 | "type": "module", 4 | "version": "0.0.80", 5 | "description": "a tool that helps refactor and optimize hook abstractions in Vue components", 6 | "author": "zcf0508 ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/zcf0508/vue-hook-optimizer", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/zcf0508/vue-hook-optimizer.git" 12 | }, 13 | "keywords": [ 14 | "vue", 15 | "hooks" 16 | ], 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.js", 22 | "require": "./dist/index.cjs" 23 | } 24 | }, 25 | "main": "./dist/index.cjs", 26 | "module": "./dist/index.js", 27 | "types": "./dist/index.d.ts", 28 | "typesVersions": { 29 | "*": { 30 | "*": [ 31 | "./dist/*", 32 | "./dist/index.d.ts" 33 | ] 34 | } 35 | }, 36 | "files": [ 37 | "dist" 38 | ], 39 | "scripts": { 40 | "dev": "nodemon", 41 | "build": "tsdown", 42 | "test": "vitest", 43 | "coverage": "vitest run --coverage", 44 | "typecheck": "tsc --noEmit", 45 | "prepublishOnly": "npm run build" 46 | }, 47 | "peerDependencies": { 48 | "@babel/core": "^7.22.5", 49 | "@babel/parser": "^7.22.5", 50 | "@babel/traverse": "^7.22.5", 51 | "@babel/types": "^7.22.5", 52 | "@vue/compiler-sfc": "^3.4.1" 53 | }, 54 | "devDependencies": { 55 | "@babel/preset-typescript": "catalog:", 56 | "@types/babel__core": "catalog:", 57 | "@types/babel__traverse": "catalog:", 58 | "@types/lodash-es": "catalog:", 59 | "@types/node": "catalog:", 60 | "@types/uuid": "catalog:", 61 | "@vitest/coverage-istanbul": "catalog:", 62 | "cross-env": "catalog:", 63 | "fast-glob": "catalog:", 64 | "lodash-es": "catalog:", 65 | "nodemon": "catalog:", 66 | "ts-node": "catalog:", 67 | "tsdown": "catalog:", 68 | "typescript": "catalog:", 69 | "vis-network": "catalog:", 70 | "vitest": "catalog:" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/loop-call.ts: -------------------------------------------------------------------------------- 1 | import type { TypedNode } from 'vue-hook-optimizer'; 2 | import type { PluginOptions } from '../types'; 3 | import { analyze, createEslintRule } from '../utils'; 4 | 5 | export const RULE_NAME = 'loop-call'; 6 | export type MessageIds = 'maybeCanRefactor'; 7 | 8 | export default createEslintRule({ 9 | name: RULE_NAME, 10 | meta: { 11 | type: 'problem', 12 | docs: { 13 | description: 'There is a loop call, perhaps you can refactor it.', 14 | }, 15 | schema: [{ 16 | type: 'object', 17 | properties: { 18 | framework: { 19 | type: 'string', 20 | enum: [ 21 | 'vue', 22 | 'react', 23 | ], 24 | }, 25 | }, 26 | }], 27 | messages: { 28 | maybeCanRefactor: 'There is a loop call in nodes [{{ name }}], perhaps you can refactor it.', 29 | }, 30 | }, 31 | defaultOptions: [ 32 | { framework: 'vue' }, 33 | ], 34 | create: (context) => { 35 | const analysisResult = analyze(context); 36 | 37 | return { 38 | Identifier(node) { 39 | if (analysisResult) { 40 | analysisResult.forEach((s) => { 41 | if (s.message.includes('There is a loop call')) { 42 | if (Array.isArray(s.nodeInfo)) { 43 | s.nodeInfo.forEach((nodeInfo) => { 44 | if ( 45 | node.loc.start.line === nodeInfo?.info?.line 46 | && node.loc.start.column === nodeInfo?.info?.column 47 | ) { 48 | context.report({ 49 | node, 50 | messageId: 'maybeCanRefactor', 51 | loc: { 52 | start: node.loc.end, 53 | end: node.loc.start, 54 | }, 55 | data: { 56 | name: (s.nodeInfo as TypedNode[] || []).map(n => n.label).join(',') || '', 57 | }, 58 | }); 59 | } 60 | }); 61 | } 62 | } 63 | }); 64 | } 65 | }, 66 | }; 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /packages/playground/components/codemirror/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 78 | 79 | 93 | -------------------------------------------------------------------------------- /packages/eslint/src/rules/linear-path.ts: -------------------------------------------------------------------------------- 1 | import type { TypedNode } from 'vue-hook-optimizer'; 2 | import type { PluginOptions } from '../types'; 3 | import { analyze, createEslintRule } from '../utils'; 4 | 5 | export const RULE_NAME = 'linear-path'; 6 | export type MessageIds = 'maybeCanRefactor'; 7 | 8 | export default createEslintRule({ 9 | name: RULE_NAME, 10 | meta: { 11 | type: 'problem', 12 | docs: { 13 | description: 'Nodes are have function chain calls, perhaps you can refactor it.', 14 | }, 15 | schema: [{ 16 | type: 'object', 17 | properties: { 18 | framework: { 19 | type: 'string', 20 | enum: [ 21 | 'vue', 22 | 'react', 23 | ], 24 | }, 25 | }, 26 | }], 27 | messages: { 28 | maybeCanRefactor: 'Nodes [{{ name }}] are have function chain calls, perhaps you can refactor it.', 29 | }, 30 | }, 31 | defaultOptions: [ 32 | { framework: 'vue' }, 33 | ], 34 | create: (context) => { 35 | const analysisResult = analyze(context); 36 | 37 | return { 38 | Identifier(node) { 39 | if (analysisResult) { 40 | analysisResult.forEach((s) => { 41 | if (s.message.includes('are have function chain calls')) { 42 | if (Array.isArray(s.nodeInfo)) { 43 | s.nodeInfo.forEach((nodeInfo) => { 44 | if ( 45 | node.loc.start.line === nodeInfo?.info?.line 46 | && node.loc.start.column === nodeInfo?.info?.column 47 | ) { 48 | context.report({ 49 | node, 50 | messageId: 'maybeCanRefactor', 51 | loc: { 52 | start: node.loc.end, 53 | end: node.loc.start, 54 | }, 55 | data: { 56 | name: (s.nodeInfo as TypedNode[] || []).map(n => n.label).join(',') || '', 57 | }, 58 | }); 59 | } 60 | }); 61 | } 62 | } 63 | }); 64 | } 65 | }, 66 | }; 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config'; 3 | import pluginSecurity from 'eslint-plugin-security'; 4 | import pluginVueHookOptimizer from 'eslint-plugin-vue-hook-optimizer'; 5 | 6 | export default antfu({ 7 | typescript: true, 8 | pnpm: true, 9 | }, [ 10 | { 11 | ignores: [ 12 | 'fixtures/**/*', 13 | 'packages/core/test/output/**/*', 14 | 'packages/playground/default-codes/**/*', 15 | ], 16 | }, 17 | pluginSecurity.configs.recommended, 18 | { 19 | plugins: { 20 | 'vue-hook-optimizer': pluginVueHookOptimizer, 21 | }, 22 | rules: { 23 | 'vue-hook-optimizer/not-used': ['warn', { 24 | framework: 'vue', 25 | }], 26 | 'vue-hook-optimizer/loop-call': ['warn', { 27 | framework: 'vue', 28 | }], 29 | 'vue-hook-optimizer/linear-path': ['warn', { 30 | framework: 'vue', 31 | }], 32 | }, 33 | }, 34 | { 35 | rules: { 36 | 'curly': ['error', 'all'], 37 | 'style/brace-style': 'error', 38 | 'style/multiline-ternary': ['error', 'always'], 39 | 'unused-imports/no-unused-imports': 'off', 40 | 'unused-imports/no-unused-vars': [ 41 | 'warn', 42 | { args: 'after-used', argsIgnorePattern: '^_', vars: 'all', varsIgnorePattern: '^_' }, 43 | ], 44 | 'no-console': ['warn'], 45 | 'style/semi': ['error', 'always'], 46 | 'style/indent': ['error', 2, { SwitchCase: 1 }], 47 | 'style/max-len': [ 48 | 'error', 49 | { 50 | code: 120, 51 | tabWidth: 2, 52 | ignoreRegExpLiterals: true, 53 | ignoreStrings: true, 54 | ignoreUrls: true, 55 | ignoreTemplateLiterals: true, 56 | ignoreComments: true, 57 | }, 58 | ], 59 | 'comma-dangle': ['error', 'always-multiline'], 60 | 'style/quotes': ['error', 'single'], 61 | 'pnpm/json-prefer-workspace-settings': 'off', 62 | }, 63 | }, 64 | { 65 | files: [ 66 | 'packages/vscode/package.json', 67 | ], 68 | rules: { 69 | 'pnpm/json-enforce-catalog': 'off', 70 | }, 71 | }, 72 | { 73 | files: ['**/*.md'], 74 | rules: { 75 | 'style/max-len': 'off', 76 | }, 77 | }, 78 | ]); 79 | -------------------------------------------------------------------------------- /packages/mcp/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { readFile } from 'node:fs/promises'; 4 | import process from 'node:process'; 5 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 6 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 7 | import { z } from 'zod'; 8 | import { version } from '../package.json'; 9 | import { analyze } from './analyze'; 10 | 11 | const server = new McpServer( 12 | { 13 | name: 'vue-hook-optimizer', 14 | version, 15 | }, 16 | { 17 | capabilities: { 18 | tools: {}, 19 | }, 20 | }, 21 | ); 22 | 23 | server.registerTool( 24 | 'analyze', 25 | { 26 | title: 'Analyze', 27 | description: 'Analyze your component to assist in refactoring and optimizing hook abstractions. Requires 2 parameters: `absolutePath` (the file\'s absolute path) and `framework` (the project\'s framework, with optional values vue / react default vue).', 28 | inputSchema: { 29 | absolutePath: z.string(), 30 | framework: z.enum(['vue', 'react']).optional().default('vue'), 31 | }, 32 | }, 33 | async ({ absolutePath, framework }) => { 34 | try { 35 | const code = await readFile(absolutePath, 'utf-8'); 36 | const res = await analyze(code, framework); 37 | 38 | return { 39 | content: [{ 40 | type: 'text', 41 | text: [ 42 | '```mermaid', 43 | res.mermaid, 44 | '```', 45 | ...res.suggests.map(s => s.message), 46 | ].join('\n'), 47 | }], 48 | }; 49 | } 50 | catch (error: unknown) { 51 | const errorMessage = (error as Error)?.message ?? 'Get error message failed.'; 52 | console.error('Something went wrong:', errorMessage); 53 | return { 54 | content: [{ 55 | type: 'text', 56 | text: `Error analyzing file: ${errorMessage}`, 57 | }], 58 | }; 59 | } 60 | }, 61 | ); 62 | 63 | async function runServer() { 64 | const transport = new StdioServerTransport(); 65 | await server.connect(transport); 66 | console.error('MCP Server running on stdio'); 67 | } 68 | 69 | runServer().catch((error) => { 70 | console.error('Fatal error in main():', error); 71 | process.exit(1); 72 | }); 73 | -------------------------------------------------------------------------------- /fixtures/vue/tsx1.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | -------------------------------------------------------------------------------- /packages/core/src/suggest/split.ts: -------------------------------------------------------------------------------- 1 | import type { RelationType, TypedNode } from '../analyze/utils'; 2 | 3 | function dfs( 4 | graph: Map>, 5 | node: TypedNode, 6 | targets: Set<{ node: TypedNode, type: RelationType }>, 7 | visited: Set, 8 | component: Set, 9 | ) { 10 | component.add(node); 11 | visited.add(node); 12 | targets.forEach((target) => { 13 | if (!visited.has(target.node)) { 14 | dfs(graph, target.node, graph.get(target.node) || new Set(), visited, component); 15 | } 16 | }); 17 | } 18 | 19 | function haveIntersection(setA: Set, setB: Set): boolean { 20 | for (const item of setA) { 21 | if (setB.has(item)) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | } 27 | 28 | function mergeSets(arr: Set[]): Set[] { 29 | let result: Set[] = [...arr]; 30 | for (let i = 0; i < result.length; i++) { 31 | for (let j = i + 1; j < result.length; j++) { 32 | if (haveIntersection(result[i], result[j])) { 33 | const newSet = new Set([...result[i], ...result[j]]); 34 | result.splice(j, 1); 35 | result.splice(i, 1); 36 | result = [...result, newSet]; 37 | return mergeSets(result); 38 | } 39 | } 40 | } 41 | return result; 42 | } 43 | 44 | export function splitGraph( 45 | graph: Map>, 46 | ) { 47 | const components = [] as Set[]; 48 | 49 | const sorted = Array.from(graph).sort((a, b) => b[1].size - a[1].size); 50 | 51 | (new Map(sorted)).forEach((targets, node) => { 52 | const visited = new Set(); 53 | if (!visited.has(node)) { 54 | const component = new Set(); 55 | dfs(graph, node, targets, visited, component); 56 | components.push(component); 57 | } 58 | }); 59 | 60 | return mergeSets(components).map((component) => { 61 | const subGraph = new Map>(); 62 | component.forEach((node) => { 63 | const targets = graph.get(node); 64 | if (targets) { 65 | subGraph.set(node, targets); 66 | } 67 | }); 68 | return subGraph; 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /packages/playground/default-codes/optionsBase.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 63 | 64 | 110 | -------------------------------------------------------------------------------- /packages/core/test/output/react/tsx4.jsx.graph.txt: -------------------------------------------------------------------------------- 1 | { 2 | "edges": Map { 3 | { 4 | "info": { 5 | "column": 9, 6 | "line": 1, 7 | }, 8 | "label": "open", 9 | "type": "var", 10 | } => Set {}, 11 | { 12 | "info": { 13 | "column": 15, 14 | "line": 1, 15 | }, 16 | "label": "setOpen", 17 | "type": "var", 18 | } => Set {}, 19 | { 20 | "info": { 21 | "column": 9, 22 | "line": 3, 23 | }, 24 | "label": "xx", 25 | "type": "var", 26 | } => Set {}, 27 | { 28 | "info": { 29 | "column": 2, 30 | "line": 5, 31 | }, 32 | "label": "processInfo", 33 | "type": "fun", 34 | } => Set { 35 | { 36 | "node": { 37 | "info": { 38 | "column": 9, 39 | "line": 1, 40 | }, 41 | "label": "open", 42 | "type": "var", 43 | }, 44 | "type": "get", 45 | }, 46 | }, 47 | { 48 | "info": { 49 | "column": 8, 50 | "line": 9, 51 | }, 52 | "label": "writeBaseInfo", 53 | "type": "fun", 54 | } => Set { 55 | { 56 | "node": { 57 | "info": { 58 | "column": 9, 59 | "line": 3, 60 | }, 61 | "label": "xx", 62 | "type": "var", 63 | }, 64 | "type": "get", 65 | }, 66 | }, 67 | }, 68 | "nodes": Set { 69 | { 70 | "info": { 71 | "column": 9, 72 | "line": 1, 73 | }, 74 | "label": "open", 75 | "type": "var", 76 | }, 77 | { 78 | "info": { 79 | "column": 15, 80 | "line": 1, 81 | }, 82 | "label": "setOpen", 83 | "type": "var", 84 | }, 85 | { 86 | "info": { 87 | "column": 9, 88 | "line": 3, 89 | }, 90 | "label": "xx", 91 | "type": "var", 92 | }, 93 | { 94 | "info": { 95 | "column": 2, 96 | "line": 5, 97 | }, 98 | "label": "processInfo", 99 | "type": "fun", 100 | }, 101 | { 102 | "info": { 103 | "column": 8, 104 | "line": 9, 105 | }, 106 | "label": "writeBaseInfo", 107 | "type": "fun", 108 | }, 109 | }, 110 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://img.shields.io/npm/v/vue-hook-optimizer?color=a1b858&label=)](https://www.npmjs.com/package/vue-hook-optimizer) 2 | Visual Studio Marketplace Version 3 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/zcf0508/vue-hook-optimizer) 4 | 5 | [中文文档](./README_cn.md) 6 | 7 | This is a tool to analyze your components code. It supports `Vue` and `React`. Viste [playground](vue-hook-optimizer.vercel.app/) or try the vscode extension [vue-hook-optimizer-ext](https://marketplace.visualstudio.com/items?itemName=zcf0508.vue-hook-optimizer-ext). 8 | 9 | ## Install And Run Playground 10 | 11 | ```bash 12 | # clone the repo then install the dependencies 13 | pnpm install 14 | # run the playground 15 | pnpm run play 16 | ``` 17 | 18 | Open the browser and visit `http://localhost:3000/`. 19 | 20 | ## How To Use 21 | 22 | 1. paste your component code into the editor 23 | 24 | 2. click `Analyze` button 25 | 26 | The tool will analyze the code, and show the relations between the variables and the methods. This is a simple demo. 27 | 28 | ![playground](./images/playground1.png) 29 | 30 | ## Motive 31 | 32 | Sometime we have to refactor the code, maybe there are thousands of lines of code in one file. 33 | And it is too complex and hard to understand. 34 | 35 | So I want to build a tool to help us analyze the code, and find the relations between the variables and the methods. 36 | We can find out some variables are isolated, and some methods are over-association, and then we can refactor them. 37 | 38 | ## Development Plan 39 | 40 | - [x] add more info, including the variable type, comment, whether has been used in template or hook methods 41 | - [x] provide some suggestions for optimization 42 | - [x] support `options api` 43 | - [x] [vscode extension](./packages/vscode) 44 | - [x] support `React` 45 | - [x] eslint rules 46 | - [x] mcp server 47 | 48 | ## Contribution 49 | 50 | Any contributions are welcome. 51 | 52 | ## Sponsor Me 53 | 54 | If you like this tool, please consider to sponsor me. I will keep working on this tool and add more features. 55 | 56 | ![sponsor](./images/sponsor.png) 57 | 58 | ## License 59 | 60 | MIT 61 | -------------------------------------------------------------------------------- /packages/core/src/analyze/style.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/style/cssVars.ts 2 | 3 | import type { SFCStyleBlock } from '@vue/compiler-sfc'; 4 | 5 | enum LexerState { 6 | inParens, 7 | inSingleQuoteString, 8 | inDoubleQuoteString, 9 | } 10 | 11 | function lexBinding(content: string, start: number): number | null { 12 | let state: LexerState = LexerState.inParens; 13 | let parenDepth = 0; 14 | 15 | for (let i = start; i < content.length; i++) { 16 | const char = content.charAt(i); 17 | switch (state) { 18 | case LexerState.inParens: 19 | if (char === '\'') { 20 | state = LexerState.inSingleQuoteString; 21 | } 22 | else if (char === '"') { 23 | state = LexerState.inDoubleQuoteString; 24 | } 25 | else if (char === '(') { 26 | parenDepth++; 27 | } 28 | else if (char === ')') { 29 | if (parenDepth > 0) { 30 | parenDepth--; 31 | } 32 | else { 33 | return i; 34 | } 35 | } 36 | break; 37 | case LexerState.inSingleQuoteString: 38 | if (char === '\'') { 39 | state = LexerState.inParens; 40 | } 41 | break; 42 | case LexerState.inDoubleQuoteString: 43 | if (char === '"') { 44 | state = LexerState.inParens; 45 | } 46 | break; 47 | } 48 | } 49 | return null; 50 | } 51 | 52 | function normalizeExpression(exp: string) { 53 | exp = exp.trim(); 54 | if ( 55 | (exp[0] === '\'' && exp[exp.length - 1] === '\'') 56 | || (exp[0] === '"' && exp[exp.length - 1] === '"') 57 | ) { 58 | return exp.slice(1, -1); 59 | } 60 | return exp; 61 | } 62 | 63 | const vBindRE = /v-bind\s*\(/g; 64 | 65 | export function analyze( 66 | styles: SFCStyleBlock[], 67 | ) { 68 | const nodes = new Set(); 69 | 70 | styles.forEach((style) => { 71 | let match; 72 | const content = style.content.replace(/\/\*([\s\S]*?)\*\/|\/\/.*/g, ''); 73 | // eslint-disable-next-line no-cond-assign 74 | while ((match = vBindRE.exec(content))) { 75 | const start = match.index + match[0].length; 76 | const end = lexBinding(content, start); 77 | if (end !== null) { 78 | const variable = normalizeExpression(content.slice(start, end)); 79 | nodes.add(variable); 80 | } 81 | } 82 | }); 83 | 84 | return nodes; 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-jsx.vue.graph.txt: -------------------------------------------------------------------------------- 1 | { 2 | "edges": Map { 3 | { 4 | "info": { 5 | "column": 8, 6 | "line": 11, 7 | }, 8 | "label": "number", 9 | "type": "var", 10 | } => Set {}, 11 | { 12 | "info": { 13 | "column": 6, 14 | "line": 15, 15 | "used": Set { 16 | "provide", 17 | }, 18 | }, 19 | "label": "count", 20 | "type": "var", 21 | } => Set {}, 22 | { 23 | "info": { 24 | "column": 6, 25 | "line": 20, 26 | }, 27 | "label": "plus", 28 | "type": "fun", 29 | } => Set { 30 | { 31 | "node": { 32 | "info": { 33 | "column": 6, 34 | "line": 20, 35 | }, 36 | "label": "plus", 37 | "type": "fun", 38 | }, 39 | "type": "get", 40 | }, 41 | }, 42 | { 43 | "info": { 44 | "column": 6, 45 | "line": 24, 46 | }, 47 | "label": "add", 48 | "type": "fun", 49 | } => Set { 50 | { 51 | "node": { 52 | "info": { 53 | "column": 8, 54 | "line": 11, 55 | }, 56 | "label": "number", 57 | "type": "var", 58 | }, 59 | "type": "get", 60 | }, 61 | { 62 | "node": { 63 | "info": { 64 | "column": 6, 65 | "line": 20, 66 | }, 67 | "label": "plus", 68 | "type": "fun", 69 | }, 70 | "type": "get", 71 | }, 72 | }, 73 | }, 74 | "nodes": Set { 75 | { 76 | "info": { 77 | "column": 8, 78 | "line": 11, 79 | }, 80 | "label": "number", 81 | "type": "var", 82 | }, 83 | { 84 | "info": { 85 | "column": 6, 86 | "line": 15, 87 | "used": Set { 88 | "provide", 89 | }, 90 | }, 91 | "label": "count", 92 | "type": "var", 93 | }, 94 | { 95 | "info": { 96 | "column": 6, 97 | "line": 20, 98 | }, 99 | "label": "plus", 100 | "type": "fun", 101 | }, 102 | { 103 | "info": { 104 | "column": 6, 105 | "line": 24, 106 | }, 107 | "label": "add", 108 | "type": "fun", 109 | }, 110 | }, 111 | } -------------------------------------------------------------------------------- /packages/core/test/output/vue/options-base-jsx2.vue.graph.txt: -------------------------------------------------------------------------------- 1 | { 2 | "edges": Map { 3 | { 4 | "info": { 5 | "column": 8, 6 | "line": 11, 7 | }, 8 | "label": "number", 9 | "type": "var", 10 | } => Set {}, 11 | { 12 | "info": { 13 | "column": 6, 14 | "line": 15, 15 | "used": Set { 16 | "provide", 17 | }, 18 | }, 19 | "label": "count", 20 | "type": "var", 21 | } => Set {}, 22 | { 23 | "info": { 24 | "column": 6, 25 | "line": 20, 26 | }, 27 | "label": "plus", 28 | "type": "fun", 29 | } => Set { 30 | { 31 | "node": { 32 | "info": { 33 | "column": 6, 34 | "line": 20, 35 | }, 36 | "label": "plus", 37 | "type": "fun", 38 | }, 39 | "type": "get", 40 | }, 41 | }, 42 | { 43 | "info": { 44 | "column": 6, 45 | "line": 24, 46 | }, 47 | "label": "add", 48 | "type": "fun", 49 | } => Set { 50 | { 51 | "node": { 52 | "info": { 53 | "column": 8, 54 | "line": 11, 55 | }, 56 | "label": "number", 57 | "type": "var", 58 | }, 59 | "type": "get", 60 | }, 61 | { 62 | "node": { 63 | "info": { 64 | "column": 6, 65 | "line": 24, 66 | }, 67 | "label": "add", 68 | "type": "fun", 69 | }, 70 | "type": "get", 71 | }, 72 | }, 73 | }, 74 | "nodes": Set { 75 | { 76 | "info": { 77 | "column": 8, 78 | "line": 11, 79 | }, 80 | "label": "number", 81 | "type": "var", 82 | }, 83 | { 84 | "info": { 85 | "column": 6, 86 | "line": 15, 87 | "used": Set { 88 | "provide", 89 | }, 90 | }, 91 | "label": "count", 92 | "type": "var", 93 | }, 94 | { 95 | "info": { 96 | "column": 6, 97 | "line": 20, 98 | }, 99 | "label": "plus", 100 | "type": "fun", 101 | }, 102 | { 103 | "info": { 104 | "column": 6, 105 | "line": 24, 106 | }, 107 | "label": "add", 108 | "type": "fun", 109 | }, 110 | }, 111 | } -------------------------------------------------------------------------------- /packages/vscode/src/generated-meta.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by `vscode-ext-gen`. Do not modify manually. 2 | // @see https://github.com/antfu/vscode-ext-gen 3 | 4 | // Meta info 5 | export const publisher = 'zcf0508'; 6 | export const name = 'vue-hook-optimizer-ext'; 7 | export const version = '0.0.73'; 8 | export const displayName = 'vue-hook-optimizer'; 9 | export const description = undefined; 10 | export const extensionId = `${publisher}.${name}`; 11 | 12 | /** 13 | * Type union of all commands 14 | */ 15 | export type CommandKey 16 | = | 'vho.action.analyze'; 17 | 18 | /** 19 | * Commands map registed by `zcf0508.vue-hook-optimizer-ext` 20 | */ 21 | export const commands = { 22 | /** 23 | * Analyze your `vue` file 24 | * @value `vho.action.analyze` 25 | */ 26 | vhoActionAnalyze: 'vho.action.analyze', 27 | } satisfies Record; 28 | 29 | /** 30 | * Type union of all configs 31 | */ 32 | export type ConfigKey 33 | = | 'vho.theme' 34 | | 'vho.language' 35 | | 'vho.highlight'; 36 | 37 | export interface ConfigKeyTypeMap { 38 | 'vho.theme': ('auto' | 'light' | 'dark') 39 | 'vho.language': ('vue' | 'react') 40 | 'vho.highlight': boolean 41 | } 42 | 43 | export interface ConfigShorthandMap { 44 | vhoTheme: 'vho.theme' 45 | vhoLanguage: 'vho.language' 46 | vhoHighlight: 'vho.highlight' 47 | } 48 | 49 | export interface ConfigItem { 50 | key: T 51 | default: ConfigKeyTypeMap[T] 52 | } 53 | 54 | /** 55 | * Configs map registed by `zcf0508.vue-hook-optimizer-ext` 56 | */ 57 | export const configs = { 58 | /** 59 | * Choose settings that are suitable for the current theme. 60 | * @key `vho.theme` 61 | * @default `"auto"` 62 | * @type `string` 63 | */ 64 | vhoTheme: { 65 | key: 'vho.theme', 66 | default: 'auto', 67 | } as ConfigItem<'vho.theme'>, 68 | /** 69 | * Choose the language used by components. It is recommended that differentiated settings be made according to the workspace. 70 | * @key `vho.language` 71 | * @default `"vue"` 72 | * @type `string` 73 | */ 74 | vhoLanguage: { 75 | key: 'vho.language', 76 | default: 'vue', 77 | } as ConfigItem<'vho.language'>, 78 | /** 79 | * Enable dependence highlight. 80 | * @key `vho.highlight` 81 | * @default `true` 82 | * @type `boolean` 83 | */ 84 | vhoHighlight: { 85 | key: 'vho.highlight', 86 | default: true, 87 | } as ConfigItem<'vho.highlight'>, 88 | }; 89 | -------------------------------------------------------------------------------- /packages/mcp/src/analyze.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RelationType, 3 | TypedNode, 4 | } from 'vue-hook-optimizer'; 5 | import { 6 | analyzeOptions, 7 | analyzeSetupScript, 8 | analyzeStyle, 9 | analyzeTemplate, 10 | analyzeTsx, 11 | gen, 12 | getMermaidText, 13 | parse, 14 | } from 'vue-hook-optimizer'; 15 | 16 | export async function analyze(code: string, language: 'vue' | 'react') { 17 | let graph = { 18 | nodes: new Set(), 19 | edges: new Map>(), 20 | }; 21 | let nodesUsedInTemplate = new Set(); 22 | let nodesUsedInStyle = new Set(); 23 | 24 | if (language === 'vue') { 25 | const sfc = parse(code); 26 | 27 | if (sfc.descriptor.scriptSetup?.content) { 28 | graph = analyzeSetupScript( 29 | sfc.descriptor.scriptSetup?.content || '', 30 | (sfc.descriptor.scriptSetup.loc.start.line || 1) - 1, 31 | (sfc.descriptor.scriptSetup.lang === 'tsx' || sfc.descriptor.scriptSetup.lang === 'jsx'), 32 | ); 33 | } 34 | else if (sfc.descriptor.script?.content) { 35 | const res = analyzeOptions( 36 | sfc.descriptor.script?.content || '', 37 | (sfc.descriptor.script.loc.start.line || 1) - 1, 38 | (sfc.descriptor.script.lang === 'tsx' || sfc.descriptor.script.lang === 'jsx'), 39 | ); 40 | graph = res.graph; 41 | nodesUsedInTemplate = res.nodesUsedInTemplate; 42 | } 43 | else { 44 | try { 45 | const res = analyzeOptions( 46 | code, 47 | 0, 48 | true, 49 | ); 50 | graph = res.graph; 51 | nodesUsedInTemplate = res.nodesUsedInTemplate; 52 | } 53 | catch (e) { 54 | console.log(e); 55 | } 56 | } 57 | 58 | try { 59 | if (sfc.descriptor.template?.content) { 60 | nodesUsedInTemplate = analyzeTemplate(sfc.descriptor.template!.content); 61 | } 62 | } 63 | catch (e) { 64 | console.log(e); 65 | } 66 | 67 | try { 68 | nodesUsedInStyle = analyzeStyle(sfc.descriptor.styles); 69 | } 70 | catch (e) { 71 | // console.log(e); 72 | } 73 | } 74 | 75 | if (language === 'react') { 76 | const res = await analyzeTsx( 77 | code, 78 | 'react', 79 | 0, 80 | ); 81 | graph = res.graph; 82 | nodesUsedInTemplate = res.nodesUsedInTemplate; 83 | } 84 | 85 | return { 86 | mermaid: getMermaidText(graph, nodesUsedInTemplate, nodesUsedInStyle), 87 | suggests: gen(graph, nodesUsedInTemplate, nodesUsedInStyle, { 88 | ellipsis: false, 89 | }), 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /packages/vscode/syntaxes/vho.output.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scopeName 6 | vho.output 7 | name 8 | vho-output 9 | patterns 10 | 11 | 12 | captures 13 | 14 | 1 15 | 16 | name 17 | token.info-token 18 | 19 | 2 20 | 21 | name 22 | token.info-token 23 | 24 | 25 | match 26 | (\[Info\])(.*) 27 | name 28 | vho-output.info 29 | 30 | 31 | captures 32 | 33 | 1 34 | 35 | name 36 | token.info-token 37 | 38 | 2 39 | 40 | name 41 | token.warning-token 42 | 43 | 44 | match 45 | (\[Warning\])(.+) 46 | name 47 | vho-output.warning 48 | 49 | 50 | captures 51 | 52 | 1 53 | 54 | name 55 | token.info-token 56 | 57 | 2 58 | 59 | name 60 | token.error-token 61 | 62 | 63 | match 64 | (\[Error\])(.+) 65 | name 66 | vho-output.error 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/vscode/src/analyze.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RelationType, 3 | TypedNode, 4 | } from '../../../packages/core/src'; 5 | import { 6 | analyzeOptions, 7 | analyzeSetupScript, 8 | analyzeStyle, 9 | analyzeTemplate, 10 | analyzeTsx, 11 | gen, 12 | getMermaidText, 13 | getVisData, 14 | parse, 15 | } from '../../../packages/core/src'; 16 | 17 | export async function analyze(code: string, language: 'vue' | 'react') { 18 | let graph = { 19 | nodes: new Set(), 20 | edges: new Map>(), 21 | }; 22 | let nodesUsedInTemplate = new Set(); 23 | let nodesUsedInStyle = new Set(); 24 | 25 | if (language === 'vue') { 26 | const sfc = parse(code); 27 | 28 | if (sfc.descriptor.scriptSetup?.content) { 29 | graph = analyzeSetupScript( 30 | sfc.descriptor.scriptSetup?.content || '', 31 | (sfc.descriptor.scriptSetup.loc.start.line || 1) - 1, 32 | (sfc.descriptor.scriptSetup.lang === 'tsx' || sfc.descriptor.scriptSetup.lang === 'jsx'), 33 | ); 34 | } 35 | else if (sfc.descriptor.script?.content) { 36 | const res = analyzeOptions( 37 | sfc.descriptor.script?.content || '', 38 | (sfc.descriptor.script.loc.start.line || 1) - 1, 39 | (sfc.descriptor.script.lang === 'tsx' || sfc.descriptor.script.lang === 'jsx'), 40 | ); 41 | graph = res.graph; 42 | nodesUsedInTemplate = res.nodesUsedInTemplate; 43 | } 44 | else { 45 | try { 46 | const res = analyzeOptions( 47 | code, 48 | 0, 49 | true, 50 | ); 51 | graph = res.graph; 52 | nodesUsedInTemplate = res.nodesUsedInTemplate; 53 | } 54 | catch (e) { 55 | console.log(e); 56 | } 57 | } 58 | 59 | try { 60 | if (sfc.descriptor.template?.content) { 61 | nodesUsedInTemplate = analyzeTemplate(sfc.descriptor.template!.content); 62 | } 63 | } 64 | catch (e) { 65 | console.log(e); 66 | } 67 | 68 | try { 69 | nodesUsedInStyle = analyzeStyle(sfc.descriptor.styles); 70 | } 71 | catch (e) { 72 | // console.log(e); 73 | } 74 | } 75 | 76 | if (language === 'react') { 77 | const res = await analyzeTsx( 78 | code, 79 | 'react', 80 | 0, 81 | ); 82 | graph = res.graph; 83 | nodesUsedInTemplate = res.nodesUsedInTemplate; 84 | } 85 | 86 | return { code: 0, data: { 87 | vis: getVisData(graph, nodesUsedInTemplate, nodesUsedInStyle), 88 | suggests: gen(graph, nodesUsedInTemplate, nodesUsedInStyle), 89 | mermaid: getMermaidText(graph, nodesUsedInTemplate, nodesUsedInStyle), 90 | }, msg: 'ok' }; 91 | } 92 | -------------------------------------------------------------------------------- /fixtures/vue/options-base-defineComponent.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 94 | 95 | 112 | -------------------------------------------------------------------------------- /packages/playground/default-codes/reactFunction.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | export default function Counter() { 4 | const [count, setCount] = useState(0); 5 | const [isEven, setIsEven] = useState(true); 6 | const [status, setStatus] = useState('normal'); 7 | 8 | useEffect(() => { 9 | setIsEven(count % 2 === 0); 10 | setStatus(count > 10 11 | ? '很大' 12 | : count < 0 13 | ? '负数' 14 | : '正常'); 15 | }, [count]); 16 | 17 | const increment = () => setCount(count + 1); 18 | const decrement = () => setCount(count - 1); 19 | const reset = () => setCount(0); 20 | 21 | return ( 22 |
23 |

React 函数组件计数器

24 |

25 | 计数: 26 | {count} 27 |

28 |
29 |

30 | 状态: 31 | {isEven 32 | ? '偶数' 33 | : '奇数'} 34 |

35 |

36 | 数值: 37 | {status} 38 |

39 |
40 |
41 | 44 | 47 | 50 |
51 |

React Hooks 示例

52 | 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /fixtures/vue/options-base.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 103 | 104 | -------------------------------------------------------------------------------- /packages/eslint/README.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://img.shields.io/npm/v/eslint-plugin-vue-hook-optimizer?color=a1b858&label=)](https://www.npmjs.com/package/eslint-plugin-vue-hook-optimizer) 2 | 3 | For more information, please visit [vue-hook-optimizer](https://github.com/zcf0508/vue-hook-optimizer). 4 | 5 | ## Install 6 | 7 | ```bash 8 | pnpm add eslint vue-eslint-parser @typescript-eslint/parser eslint-plugin-vue-hook-optimizer --save-dev 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### legacy config 14 | 15 | ```js 16 | // .eslintrc.js 17 | 18 | /** @type {import('eslint').Linter.Config} */ 19 | module.exports = { 20 | parser: 'vue-eslint-parser', 21 | parserOptions: { 22 | parser: { 23 | 'js': '@typescript-eslint/parser', 24 | '