Vit 依赖收集插件
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