niucloud/admin/scripts/scan-exports.cjs
2026-07-14 17:02:06 +08:00

104 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 通用导出冲突扫描器
* 扫描目录下 .ts/.js 文件的 export检测同名冲突返回冲突映射。
*/
const fs = require('fs')
const path = require('path')
const DEFAULT_EXTENSIONS = ['.ts', '.js', '.tsx', '.jsx']
/** 从源码中提取所有命名导出名称 */
function scanExports(src) {
const names = []
// export const|let|var|function|class|type|interface|enum NAME
const declRe = /export\s+(?:const|let|var|function|class|type|interface|enum)\s+(\w+)/g
for (const m of src.matchAll(declRe)) names.push(m[1])
// export { a, b, c as d }
const braceRe = /export\s*(?:type\s*)?\{([^}]+)\}/g
for (const m of src.matchAll(braceRe)) {
for (const item of m[1].split(',').map(s => s.trim()).filter(Boolean)) {
const parts = item.split(/\s+(?:as|:)\s+/)
names.push(parts.length > 1 ? parts[parts.length - 1].trim() : parts[0].trim())
}
}
return [...new Set(names)]
}
/** 扫描目录,返回文件列表 */
function scanDirExports(dir, opts = {}) {
const exclude = new Set(opts.exclude || [])
const exts = new Set(opts.extensions || DEFAULT_EXTENSIONS)
if (!fs.existsSync(dir)) return []
const out = []
for (const f of fs.readdirSync(dir)) {
const full = path.join(dir, f)
if (!fs.statSync(full).isFile()) continue
if (![...exts].some(e => f.endsWith(e))) continue
if (exclude.has(f) || exclude.has(full)) continue
out.push({ moduleKey: f.replace(/\.[^.]+$/, ''), path: full })
}
return out
}
/** 递归扫描目录 */
function scanRecursive(dir, opts = {}) {
const exclude = new Set(opts.exclude || [])
const exts = new Set(opts.extensions || DEFAULT_EXTENSIONS)
const out = []
if (!fs.existsSync(dir)) return out
function walk(base, rel) {
for (const name of fs.readdirSync(base)) {
const relPath = rel ? `${rel}/${name}` : name
if (exclude.has(relPath) || exclude.has(name)) continue
const full = path.join(base, name)
const stat = fs.statSync(full)
if (stat.isDirectory()) walk(full, relPath)
else if (stat.isFile() && [...exts].some(e => name.endsWith(e)))
out.push({ moduleKey: relPath.replace(/\.[^.]+$/, '').replace(/[\\/]/g, '_'), path: full })
}
}
walk(dir, '')
return out
}
/**
* 检测并解决导出冲突。按 moduleKey 字母序处理,
* 先导出的保留原名,后出现的加 moduleKey 前缀。
*/
function resolveConflicts(files, opts = {}) {
const sorted = [...files].sort((a, b) => a.moduleKey.localeCompare(b.moduleKey))
const exportedNames = new Set()
const conflicts = {}
const resolved = []
for (const file of sorted) {
const src = fs.readFileSync(file.path, 'utf-8')
const names = scanExports(src)
const normal = []; const renamed = []
for (const name of names) {
if (!exportedNames.has(name)) { exportedNames.add(name); normal.push(name) }
else {
const newName = opts.prefixer ? opts.prefixer(file, name) : file.moduleKey + '_' + name
exportedNames.add(newName); renamed.push(name)
conflicts[file.moduleKey + ':' + name] = newName
}
}
resolved.push({ module: file.moduleKey, normal, renamed, src: file.path })
}
return { resolved, conflicts }
}
/** 生成 re-export 语句 */
function buildReExportStatements(resolved, modulePathPrefix) {
const lines = []
for (const r of resolved) {
if (r.normal.length) lines.push(`export { ${r.normal.join(', ')} } from '${modulePathPrefix}/${r.module}'`)
if (r.renamed.length) {
const renamedStr = r.renamed.map(n => `${n} as ${r.module}_${n}`).join(', ')
lines.push(`export { ${renamedStr} } from '${modulePathPrefix}/${r.module}'`)
}
}
return lines
}
module.exports = { scanExports, scanDirExports, scanRecursive, resolveConflicts, buildReExportStatements }