UNPKG

vite-vue-tagger

Version:

138 lines (118 loc) 3.51 kB
// src/index.ts import { parse } from '@vue/compiler-sfc'; import { parse as domParse, ElementNode } from '@vue/compiler-dom'; import MagicString from 'magic-string'; import { createHash } from 'crypto'; export interface TaggerOptions { /** 开发/生产模式 */ mode?: 'development' | 'production'; /** 哈希盐值 */ hashSalt?: string; /** 最小化属性 */ minify?: boolean; /** 包含文件的正则 */ include: RegExp; /** 排除文件的正则 */ exclude: RegExp; } const DEFAULT_OPTIONS: TaggerOptions = { mode: 'development', hashSalt: 'vite-vue-tagger', minify: false, include: /\.(vue|tsx|jsx)$/, exclude: /node_modules/, }; export default function ComponentTagger(options?: TaggerOptions) { const opts = { ...DEFAULT_OPTIONS, ...options }; return { name: 'vite-vue-tagger', enforce: 'pre', async transform(code: string, id: string) { if (opts.mode === 'production') return; if (opts.exclude.test(id)) return; if (!opts.include.test(id)) return; try { const s = await processCode(code, id, opts); return { code: opts.minify ? minifyAttributes(s.toString()) : s.toString(), map: s.generateMap({ source: id }), }; } catch (e: unknown) { const errorMessage = e instanceof Error ? e.message : String(e); console.error(`[vue-tagger] Error processing ${id}:\n${errorMessage}`); } }, }; } async function processCode(code: string, id: string, opts: TaggerOptions) { const s = new MagicString(code); const { descriptor } = parse(code, { sourceMap: true }); if (!descriptor.template) return s; const template = descriptor.template; const ast = domParse(template.content, { parseMode: 'html', isNativeTag: () => true, // 假设全部为原生标签 }); ast.children.forEach((node: any) => { if (node.type === 1) { // ElementNode processElement(node as ElementNode, template.loc.start.line, s, id, opts); } }); return s; } function processElement(node: ElementNode, lineOffset: number, s: MagicString, id: string, opts: TaggerOptions) { const source = { line: node.loc.start.line + lineOffset, column: node.loc.start.column, tag: node.tag, }; const attrs = [ `data-lov-id="${id}:${source.line}:${source.column}"`, `data-lov-name="${node.tag}"`, `data-component-path="${id}"`, `data-component-file="${id.split('/').pop()}"`, `data-component-line="${source.line}"`, `data-component-name="${getComponentName(id)}"`, `data-component-content="${encodeContent(node, opts)}"`, ].join(' '); s.appendLeft( node.loc.start.offset + 1, // 在标签名后插入 ` ${attrs} ` ); } function getComponentName(filePath: string) { return ( filePath .split('/') .pop() ?.replace(/\.(vue|tsx|jsx)$/, '') ?.replace(/([a-z])([A-Z])/g, '$1 $2') // 驼峰转空格 ?.replace(/[-_]/g, ' ') || 'Anonymous' ); } function encodeContent(node: ElementNode, opts: TaggerOptions) { const props = extractProps(node); const contentHash = createHash('sha256') .update(opts.hashSalt + node.loc.source) .digest('hex') .substr(0, 8); return encodeURIComponent( JSON.stringify({ props, hash: contentHash, }) ); } function extractProps(node: ElementNode) { return node.props.reduce((acc: any, prop) => { if (prop.type === 6) { // 属性节点 acc[prop.name] = prop.value?.content || ''; } return acc; }, {}); } function minifyAttributes(code: string) { return code.replace(/\s+data-[\w-]+="[^"]*"/g, (match) => match.replace(/\s+/g, ' ').trim()); }