项目概况

Vit 依赖收集插件

2026-01-293 min read前端技术
vue
text
1// plugins/vite-plugin-collect-render.ts 2import { createFilter } from '@rollup/pluginutils' 3import { parse as parseAst } from '@typescript-eslint/parser' 4import { TSESTree } from '@typescript-eslint/types' 5import { writeFileSync, mkdirSync, readFileSync } from 'fs' 6import { resolve, dirname } from 'path' 7 8const PLUGIN_NAME = 'vite:collect-render' 9 10export default function collectRenderPlugin(options: { 11 include?: string | string[] 12 exclude?: string | string[] 13 output?: string 14} = {}) { 15 16 const filter = createFilter( 17 options.include || 'src/**/*.{vue,ts,tsx}', 18 options.exclude || 'node_modules' 19 ) 20 21 const output = resolve(options.output || 'src/auto-collect-renders.ts') 22 23 // 存储收集到的 { key, renderCode } 24 const renders: Array<{ key: string; render: string; file: string }> = [] 25 26 /** 遍历 AST 收集 collectRender */ 27 function walkAst(node: TSESTree.Node, code: string, file: string) { 28 if ( 29 node.type === 'CallExpression' && 30 node.callee.type === 'Identifier' && 31 node.callee.name === 'collectRender' 32 ) { 33 console.log('[collect-render] 收集文件 collectRender:', file) 34 const args = node.arguments 35 if (args.length === 2) { 36 const keyArg = args[0] 37 const renderArg = args[1] 38 39 if ( 40 keyArg.type === 'Literal' && 41 typeof keyArg.value === 'string' && 42 (renderArg.type === 'ArrowFunctionExpression' || 43 renderArg.type === 'FunctionExpression') 44 ) { 45 const key = keyArg.value 46 const [start, end] = renderArg.range! 47 const renderCode = code.slice(start, end) 48 49 // 去重:后面覆盖前面 50 const index = renders.findIndex(r => r.key === key) 51 if (index >= 0) renders[index] = { key, render: renderCode, file } 52 else renders.push({ key, render: renderCode, file }) 53 } 54 } 55 } 56 57 // 递归 children 58 for (const k in node) { 59 const value = (node as any)[k] 60 if (Array.isArray(value)) value.forEach(v => v && typeof v.type === 'string' && walkAst(v, code, file)) 61 else if (value && typeof value.type === 'string') walkAst(value, code, file) 62 } 63 } 64 65 /** 收集单个文件的 collectRender */ 66 function collectFile(file: string, code?: string) { 67 if (!filter(file)) return 68 if (!code) code = readFileSync(file, 'utf-8') 69 70 let scriptCode = code 71 let isVue = false 72 if (file.endsWith('.vue')) { 73 isVue = true 74 const scriptSetupMatch = code.match(/<script setup.*?>([sS]*?)</script>/i) 75 const scriptMatch = code.match(/<script(?! setup).*?>([sS]*?)</script>/i) 76 if (scriptSetupMatch) scriptCode = scriptSetupMatch[1] 77 else if (scriptMatch) scriptCode = scriptMatch[1] 78 else return 79 } 80 try { 81 const ast = parseAst(scriptCode, { 82 range: true, 83 sourceType: 'module', 84 ecmaVersion: 'latest', 85 ecmaFeatures: { jsx: true } 86 }) 87 walkAst(ast as any, scriptCode, file) 88 } catch (e) { 89 console.warn(`[collect-render] AST 解析失败: ${file}`, e) 90 } 91 } 92 93 /** 生成 auto-collect-renders.ts 文件 */ 94 function generateFile() { 95 if (renders.length === 0) return 96 97 const exports = renders.map(r => ` '${r.key}': ${r.render}`).join(', 98') 99 const content = ` 100// 🚨 This file is auto-generated by vite-plugin-collect-render 101// 🔄 Do not edit manually 102 103export const collectedRenders = { 104${exports} 105} as const 106 107export type CollectedRenderKeys = keyof typeof collectedRenders 108 `.trim() 109 110 mkdirSync(dirname(output), { recursive: true }) 111 writeFileSync(output, content, 'utf-8') 112 } 113 114 return { 115 name: PLUGIN_NAME, 116 enforce: 'pre' as const, 117 118 transform(code: string, id: string) { 119 collectFile(id, code) 120 return null 121 }, 122 123 buildEnd() { 124 generateFile() 125 this.info(`${PLUGIN_NAME}: Collected ${renders.length} render functions → ${output}`) 126 }, 127 128 // ⚡ HMR: 文件修改时重新收集并触发更新 129 handleHotUpdate({ file, server }) { 130 if (!filter(file)) return 131 collectFile(file) 132 generateFile() 133 134 const module = server.moduleGraph.getModuleById(output) 135 if (module) { 136 server.moduleGraph.invalidateModule(module) 137 server.ws.send({ 138 type: 'update', 139 updates: [ 140 { 141 type: 'js-update', 142 path: '/' + output, 143 acceptedPath: '/' + output, 144 timestamp: Date.now(), 145 }, 146 ], 147 }) 148 } 149 return [] 150 } 151 } 152} 153