/** * 尝试批量处理 unplugin 代码,将它恢复为显式 import 写法 * * 目前暂只支持解析 components unplugin * * 用法: * ``` * npx tsx scripts/re-plugin packages/sdk-chart/src/ * ``` * * 会将目录下的所有 .vue 文件都进行一次处理 * * 为什么不用 unplugin? * - 隐式声明使得代码不够明确 * - 当项目变大时,unplugin 冲突问题明显,造成更大的隐患 * * 包含的内容 * - vue 常用方法,例如 ref、computed 等 * - antdv 组件,例如 a-input * - 业务组件,例如 color-picker */ import { parse } from '@vue/compiler-sfc'; import { readdir, readFile, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { readUnpluginComponents, Source } from './read-unplugin'; import { pascalCase } from './utils'; import assert from 'node:assert'; import { extractComponents } from './extract-components'; import { extractGlobals } from './extract-globals'; const UNPLUGIN_SRC_DIR = join(__dirname, '../../src'); interface Import extends Source { name: string; } async function main() { const components = await readUnpluginComponents(UNPLUGIN_SRC_DIR); const dir = process.argv[2]; if (!dir) { throw new Error( `请指定要解析的 .vue 文件所在目录,例如 npx tsx scripts/re-plugin packages/sdk-chart/src`, ); } const walk = async (dir: string) => { const list = await readdir(dir); for (const file of list) { const filePath = join(dir, file); const s = await stat(filePath); if (s.isDirectory()) { await walk(filePath); } else if (file.endsWith('.vue')) { rePluginVueFile(filePath, components); } } }; await walk(join(__dirname, '../../', dir)); } async function rePluginVueFile( filePath: string, components: Awaited>, ) { const content = await readFile(filePath, 'utf-8'); const result = parse(content); // console.dir(result.descriptor.template.ast) const { template, script, scriptSetup } = result.descriptor; const list = extractComponents(result.descriptor.template, (n) => components.has(n), ); const globals = new Set([ ...extractGlobals(script), ...extractGlobals(scriptSetup), ]); const imports = generateImportStatements( list .map((n) => { const source = components.get(n.tag)!; return { ...source, name: pascalCase(n.tag), }; }) .filter((o) => !globals.has(o.name)), ); const importStatements = imports.join(';\n'); // 回写文件 if (!importStatements) { console.log(`无需修改 - ${filePath}`); return; } const { source } = result.descriptor; const { loc } = scriptSetup ?? script ?? {}; await writeFile( filePath, source.slice(0, loc.start.offset) + '\n' + importStatements + source.slice(loc.start.offset), 'utf8', ); console.log( `已更新 - ${filePath}\n${imports.map((l) => ` ${l}`).join('\n')}`, ); } // 生成 import 语句 function generateImportStatements(list: Import[]) { // TODO 未来可以解析已经导入过了的 const imports = new Map>(); for (const { module, path, name } of list) { let members = imports.get(module); if (!members) { members = new Map(); imports.set(module, members); } let member = members.get(name); if (member) { assert(member === path); continue; } members.set(name, path); } const lines: string[] = []; for (const [m, members] of imports) { const DEFAULT = 'default'; let defaultStatement = ''; const others: string[] = []; for (const [name, path] of members) { if (path === DEFAULT) { defaultStatement = `${name}`; continue; } others.push(name === path ? name : `${path} as ${name}`); } const otherStatement = others.length ? `{ ${others.join(', ')} }` : ''; lines.push( `import ${[defaultStatement, otherStatement] .filter(Boolean) .join(', ')} from '${m}'`, ); } return lines; } main().catch(console.error);