下面给你一份可直接替换现有 parser.js 的增强版实现(仍保持"最小可运行"的思路),重点做了你要求的三件事:
-
组件优先导出
__SEARCH_META时优先解析 (并保留权限字段:privilege / funcPermission等原始表达式字符串) -
完善递归解析能力 :仍然通过 import 建立子组件映射,并递归进入子组件;为避免死循环加了
visited -
保留并增强 v-tabs 解析能力:
-
平铺
v-tab-pane(含v-if/v-show渲染条件提取) -
tabArr在同文件静态数组写法(提取t('...')/$t('...')的多语言 key,并把渲染条件如supportPlayback || supportLiveView放到support链里) -
:tabs="xxx"的静态数组/computed(() => [...])场景继续支持 -
重要 :对
tabArr的component: AccountInfo会用 import 映射递归解析子组件(更贴合你们现状)
-
约定:
权限相关字段:都以字符串形式原样落盘(后续 runtime 从 store 取值判断)
条件渲染(
v-if/v-show)作为support的一项字符串表达式进入索引(你后续过滤时可统一处理)
// parser.js
// Vue3 SFC 解析器(ESM)- 增强版
// 目标:静态构建全项目搜索索引(tab/card)
//
// 新增能力:
// 1) 优先解析 export const __SEARCH_META = [...](tab/card),并保留权限字段原始表达式字符串
// 2) 递归解析更可靠:import -> 子组件映射;支持 tabArr.component 递归;visited 防止循环
// 3) 保留 v-tabs 解析:
// - :tabs="xxx"(静态数组/ computed(()=>[...]))
// - 平铺 v-tab-pane(含 v-if/v-show 条件提取)
// - 同文件 const tabArr = [...] 静态数组写法(title: t('...')/ $t('...'))
// 4) 当 __SEARCH_META 存在时:
// - 对 tabs/cards 的"启发式解析"不再重复提取(避免重复)
// - 仍然会递归解析模板中出现的子组件,以便子组件自己有 __SEARCH_META 时被收集到
import { parse, compileTemplate, compileScript } from '@vue/compiler-sfc'
import { isHTMLTag } from '@vue/shared'
import { transform } from '@vue/compiler-dom'
import * as fs from 'fs'
import * as path from 'path'
import { PROJECT_SRC } from './constant.js'
import { normalize } from './utils/naming.js'
// ---- 简单日志包装 ----
function dbg(...args) { console.debug('[debug][parser]', ...args) }
function info(...args) { console.info('[info][parser]', ...args) }
function warn(...args) { console.warn('[warn][parser]', ...args) }
function err(...args) { console.error('[error][parser]', ...args) }
// ---- 小工具 ----
function tryResolveFile(candidate) {
if (!candidate) return null
const candidates = [
candidate,
`${candidate}.vue`,
path.resolve(candidate, 'index.vue'),
]
for (const c of candidates) {
try {
if (c && fs.existsSync(c) && fs.statSync(c).isFile()) return c
} catch (e) {}
}
return null
}
function getScriptBody(scriptAst) {
if (!scriptAst) return []
if (Array.isArray(scriptAst)) return scriptAst
if (scriptAst.type === 'Program' && Array.isArray(scriptAst.body)) return scriptAst.body
return scriptAst.body || []
}
// 判断是否自定义组件(非标准 HTML 标签)
function isCustomComponent(tag) {
if (!tag || typeof tag !== 'string') return false
if (isHTMLTag(tag)) return false
return true
}
// 是否显式标记为非搜索组件
function isNonSearchable(node) {
if (!node || !node.props) return false
return node.props.some((p) => {
try {
// searchable="false"
if ((p.name === 'searchable' || p.arg?.content === 'searchable') && p.value && p.value.content === 'false') return true
// :searchable="false"
if (p.name === 'bind' && p.arg?.content === 'searchable' && p.exp?.content === 'false') return true
} catch (e) {}
return false
})
}
// 从模板 AST 的 node.props 获取绑定值(支持 :xxx / xxx)
function getBindValue(node, name) {
if (!node || !node.props) return undefined
for (const p of node.props) {
try {
// :xxx="expr"
if (p.name === 'bind' && p.arg && p.arg.content === name) {
if (p.exp && p.exp.content) return p.exp.content
}
// xxx="literal"
if (p.name === name) {
if (p.value && typeof p.value.content === 'string') return p.value.content
if (p.exp && p.exp.content) return p.exp.content
}
// 兜底
if (p.arg && p.arg.content === name && p.exp && p.exp.content) return p.exp.content
} catch (e) {}
}
return undefined
}
// 获取 v-if/v-show 条件表达式(字符串)
function getConditionExpr(node) {
if (!node || !node.props) return null
for (const p of node.props) {
try {
// @vue/compiler-dom 中 v-if 指令通常是 name === 'if'
if (p.name === 'if' || p.name === 'show') {
if (p.exp?.content) return p.exp.content
}
// 某些版本可能 rawName 是 'v-if'/'v-show'
if (p.rawName === 'v-if' || p.rawName === 'v-show') {
if (p.exp?.content) return p.exp.content
}
} catch (e) {}
}
return null
}
function isConditionalComponent(node) {
return !!getConditionExpr(node)
}
// 简单提取 card 标题(保留原实现,但你们现在 card 更推荐 __SEARCH_META)
function getTitle(node) {
const t = getBindValue(node, 'title')
if (t) return t
if (node.children && node.children.length) {
for (const c of node.children) {
try {
if (c.type === 3 && typeof c.content === 'string' && c.content.trim()) return c.content.trim()
} catch (e) {}
}
}
return null
}
function clonePermissions(p) {
return {
isConditional: !!p.isConditional,
permission: Array.isArray(p.permission) ? [...p.permission] : p.permission ? [p.permission] : [],
support: Array.isArray(p.support) ? [...p.support] : p.support ? [p.support] : [],
}
}
// ---------- Script AST 解析:通用提取工具 ----------
// 从 StringLiteral / Literal / TemplateLiteral / CallExpression(t/$t('x')) 中提取 i18n key
function extractI18nKeyFromExpr(expr) {
if (!expr) return null
// 字符串字面量
if (expr.type === 'StringLiteral' && typeof expr.value === 'string') return expr.value
if (expr.type === 'Literal' && typeof expr.value === 'string') return expr.value
// t('X') / $t('X')
if (expr.type === 'CallExpression') {
const calleeName =
expr.callee?.name ||
(expr.callee?.type === 'Identifier' ? expr.callee.name : null) ||
(expr.callee?.type === 'MemberExpression' ? expr.callee.property?.name : null)
if (calleeName === 't' || calleeName === '$t') {
const a0 = expr.arguments?.[0]
if (a0 && (a0.type === 'StringLiteral' || a0.type === 'Literal') && typeof a0.value === 'string') return a0.value
}
}
return null
}
// 从 ObjectExpression 中按 key 提取 value expr
function getObjectPropValueExpr(objExpr, keyName) {
if (!objExpr || objExpr.type !== 'ObjectExpression') return null
for (const p of objExpr.properties || []) {
if (!p) continue
if (p.type === 'ObjectProperty') {
const k = p.key?.name || p.key?.value
if (k === keyName) return p.value || null
}
}
return null
}
function getObjectPropLiteralOrExprString(objExpr, keyName) {
const v = getObjectPropValueExpr(objExpr, keyName)
if (!v) return null
// 字符串字面量
if (v.type === 'StringLiteral' && typeof v.value === 'string') return v.value
if (v.type === 'Literal' && typeof v.value === 'string') return v.value
// 其他表达式:原样字符串化(最小实现:用 loc.source 兜底不稳定;这里只返回一个标记)
// 注意:compiler-sfc 的 AST 节点不一定带 source,这里返回一个保守字符串
return (v.name && String(v.name)) || (v.type ? `[${v.type}]` : null)
}
// 在 scriptBody 中找 export const XXX = ...
function findExportedConstInit(scriptBody, constName) {
for (const node of scriptBody || []) {
try {
if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
for (const decl of node.declaration.declarations || []) {
if (decl.id?.name === constName) return decl.init || null
}
}
if (node.type === 'VariableDeclaration') {
for (const decl of node.declarations || []) {
if (decl.id?.name === constName) return decl.init || null
}
}
} catch (e) {}
}
return null
}
// 解析 __SEARCH_META:export const __SEARCH_META = [ { ... } ]
function parseSearchMeta(scriptBody) {
const init = findExportedConstInit(scriptBody, '__SEARCH_META')
if (!init || init.type !== 'ArrayExpression') return []
const out = []
for (const el of init.elements || []) {
if (!el || el.type !== 'ObjectExpression') continue
const type = getObjectPropLiteralOrExprString(el, 'type')
const lanKeyExpr = getObjectPropValueExpr(el, 'lanKey')
const lanKey = extractI18nKeyFromExpr(lanKeyExpr) || getObjectPropLiteralOrExprString(el, 'lanKey')
// 注意:key/privilege/funcPermission 等可能是表达式,这里尽量保留原始"可用字符串"
const keyExpr = getObjectPropValueExpr(el, 'key')
const key =
(keyExpr?.type === 'StringLiteral' ? keyExpr.value : null) ||
(keyExpr?.type === 'Literal' ? keyExpr.value : null) ||
(keyExpr?.name ? keyExpr.name : null) ||
(keyExpr?.type ? `[${keyExpr.type}]` : null)
const privilegeExpr = getObjectPropValueExpr(el, 'privilege')
const funcPermissionExpr = getObjectPropValueExpr(el, 'funcPermission')
const privilege =
(privilegeExpr?.type === 'StringLiteral' ? privilegeExpr.value : null) ||
(privilegeExpr?.type === 'Literal' ? privilegeExpr.value : null) ||
(privilegeExpr?.name ? privilegeExpr.name : null) ||
(privilegeExpr?.type ? `[${privilegeExpr.type}]` : null)
// funcPermission 你例子里是一个逗号表达式/复杂表达式,这里返回占位字符串并保留字段
// 你如果需要更精确的表达式字符串,建议后续在构建阶段使用 @babel/generator 输出代码字符串。
const funcPermission =
(funcPermissionExpr?.type === 'StringLiteral' ? funcPermissionExpr.value : null) ||
(funcPermissionExpr?.type === 'Literal' ? funcPermissionExpr.value : null) ||
(funcPermissionExpr?.name ? funcPermissionExpr.name : null) ||
(funcPermissionExpr?.type ? `[${funcPermissionExpr.type}]` : null)
out.push({
type,
key,
lanKey,
privilege,
funcPermission,
// 允许 meta 里未来扩展 support/permission 字段(不破坏)
support: getObjectPropLiteralOrExprString(el, 'support') || undefined,
permission: getObjectPropLiteralOrExprString(el, 'permission') || undefined,
})
}
return out.filter((x) => x && (x.type === 'tab' || x.type === 'card'))
}
// 解析 tabArr:const tabArr = [ { name, title: t('...'), component: X } ...]
function parseTabArr(scriptBody, varName) {
const init = findExportedConstInit(scriptBody, varName)
if (!init) return []
// 允许 tabArr = computed(()=>[...])
let arrExpr = init
if (arrExpr.type === 'CallExpression' && arrExpr.callee?.name === 'computed') {
const fn = arrExpr.arguments?.[0]
if (fn?.type === 'ArrowFunctionExpression') {
if (fn.body?.type === 'ArrayExpression') arrExpr = fn.body
if (fn.body?.type === 'BlockStatement') {
const ret = fn.body.body?.find((x) => x.type === 'ReturnStatement')
if (ret?.argument?.type === 'ArrayExpression') arrExpr = ret.argument
}
}
}
if (arrExpr.type !== 'ArrayExpression') return []
const out = []
for (const el of arrExpr.elements || []) {
if (!el || el.type !== 'ObjectExpression') continue
const nameExpr = getObjectPropValueExpr(el, 'name')
const titleExpr = getObjectPropValueExpr(el, 'title')
const compExpr = getObjectPropValueExpr(el, 'component')
const key =
(nameExpr?.type === 'StringLiteral' ? nameExpr.value : null) ||
(nameExpr?.type === 'Literal' ? nameExpr.value : null) ||
(nameExpr?.name ? nameExpr.name : null)
const lanKey = extractI18nKeyFromExpr(titleExpr) || getObjectPropLiteralOrExprString(el, 'title')
const componentName =
(compExpr?.type === 'Identifier' ? compExpr.name : null) ||
(compExpr?.type === 'StringLiteral' ? compExpr.value : null) ||
(compExpr?.type === 'Literal' ? compExpr.value : null)
if (!key && !lanKey) continue
out.push({ key, lanKey, componentName })
}
return out
}
// 从模板 node.props 找到 :tabs="xxx" 绑定变量名
function getTabsBindingVar(node) {
if (!node || !node.props) return null
const tabsProp = (node.props || []).find((p) => {
try {
if (p.name === 'bind' && p.arg?.content === 'tabs') return true
if (p.name === 'tabs') return true
if (p.arg?.content === 'tabs') return true
} catch (e) {}
return false
})
if (tabsProp?.exp?.content) return tabsProp.exp.content.split('.').pop()
return null
}
// 将 tabsElements(ObjectExpression[])转成 {key, lanKey, componentName?}[]
function normalizeTabsElementsToTabInfos(tabsElements) {
const out = []
for (const el of tabsElements || []) {
if (!el) continue
const obj = el.type === 'ObjectExpression' ? el : null
if (!obj) continue
const keyExpr = getObjectPropValueExpr(obj, 'key') || getObjectPropValueExpr(obj, 'name')
const titleExpr = getObjectPropValueExpr(obj, 'title') || getObjectPropValueExpr(obj, 'label')
const compExpr = getObjectPropValueExpr(obj, 'component')
const key =
(keyExpr?.type === 'StringLiteral' ? keyExpr.value : null) ||
(keyExpr?.type === 'Literal' ? keyExpr.value : null) ||
(keyExpr?.name ? keyExpr.name : null)
const lanKey = extractI18nKeyFromExpr(titleExpr) || getObjectPropLiteralOrExprString(obj, 'title')
const componentName =
(compExpr?.type === 'Identifier' ? compExpr.name : null) ||
(compExpr?.type === 'StringLiteral' ? compExpr.value : null) ||
(compExpr?.type === 'Literal' ? compExpr.value : null)
if (!key && !lanKey) continue
out.push({ key, lanKey, componentName })
}
return out
}
// ---------- walk:递归解析单个文件 ----------
function walk(params) {
const {
filePath,
parentName,
subComponentMap,
results,
parentTab,
parentPermissions = { isConditional: false, permission: [], support: [] },
visited,
} = params
if (!filePath) return
const resolved = tryResolveFile(filePath)
if (!resolved) {
err(`[error][parseSfc]: 读取文件失败: ${filePath} (ENOENT)`)
return
}
// 防止递归死循环
if (visited.has(resolved)) {
dbg(`已访问过 ${resolved},跳过递归`)
return
}
visited.add(resolved)
info(`解析文件: ${resolved} (父: ${parentName})`)
let fileText = ''
try {
fileText = fs.readFileSync(resolved, 'utf-8')
} catch (e) {
err(`[error][parseSfc]: 读取文件失败: ${resolved}`, e.message)
return
}
let sfc
try {
sfc = parse(fileText)
} catch (e) {
err('[error][parseSfc]: parse SFC 失败', resolved, e.message)
return
}
const templateContent = sfc.descriptor.template?.content || ''
let template
try {
template = compileTemplate({ source: templateContent })
} catch (e) {
warn(`[warn][parseSfc]: compileTemplate 失败 (${resolved}): ${e.message}`)
}
const ast = template?.ast
let script = null
try {
script = compileScript(sfc.descriptor, { id: '1' })
} catch (e) {
info(`[info][parseSfc]: Missing or unparsable <script> in ${resolved}`)
}
const scriptAst = script ? (script.scriptAst || script.scriptSetupAst) : null
const scriptBody = getScriptBody(scriptAst)
// import -> 本地子组件映射
try {
for (const node of scriptBody || []) {
if (node.type === 'ImportDeclaration' && typeof node.source?.value === 'string') {
const rawPath = node.source.value
const spec = node.specifiers?.find((s) => s.local?.type === 'Identifier')
const name = spec?.local?.name
if (!name) continue
const looksLocal =
rawPath.startsWith('.') ||
rawPath.startsWith('@') ||
rawPath.endsWith('.vue') ||
rawPath.startsWith(PROJECT_SRC)
if (!looksLocal) {
dbg(`跳过外部 import: ${rawPath} (标识: ${name})`)
continue
}
let fullPath = rawPath
if (rawPath.startsWith('@')) fullPath = rawPath.replace('@', PROJECT_SRC)
else if (!path.isAbsolute(rawPath)) fullPath = path.resolve(path.dirname(resolved), rawPath)
const real = tryResolveFile(fullPath)
if (!real) {
dbg(`无法解析 import 到文件: ${rawPath} -> ${fullPath},跳过`)
continue
}
subComponentMap.set(normalize(name), real)
dbg(`发现 import 组件 ${name} -> ${real}`)
}
}
} catch (e) {
warn('[warn][parseSfc]: import 解析异常', e.message)
}
// 1) 优先解析 __SEARCH_META
const metaItems = parseSearchMeta(scriptBody)
const hasSearchMeta = metaItems.length > 0
if (hasSearchMeta) {
dbg(`发现 __SEARCH_META,条目数=${metaItems.length},将优先使用 meta 提取 tab/card`)
for (const it of metaItems) {
const currentPerm = clonePermissions(parentPermissions)
const condExpr = null // meta 中通常不带 v-if;你需要也可扩展
// merge permission/support from meta(如果 meta 里也写了)
if (it.permission) currentPerm.permission.push(it.permission)
if (it.support) currentPerm.support.push(it.support)
if (condExpr) currentPerm.support.push(condExpr)
if (it.type === 'tab') {
const tabInfo = {
type: 'tab',
key: it.key,
lanKey: it.lanKey,
privilege: it.privilege,
funcPermission: it.funcPermission,
permission: currentPerm.permission,
support: currentPerm.support,
isConditional: currentPerm.isConditional,
}
results.push(tabInfo)
}
if (it.type === 'card') {
const { key: parentTabKey, lanKey: parentTabLanKey } = parentTab || {}
const cardInfo = {
type: 'card',
lanKey: it.lanKey,
parentTabKey,
parentTabLanKey,
privilege: it.privilege,
funcPermission: it.funcPermission,
permission: currentPerm.permission,
support: currentPerm.support,
isConditional: currentPerm.isConditional,
selfConditional: false,
parentConditional: currentPerm.isConditional,
}
results.push(cardInfo)
}
}
}
// 如果没有 template AST,就只能靠 __SEARCH_META 了
if (!ast) return
// 2) 解析 template:当 hasSearchMeta=true 时,不再重复提取 tabs/cards(避免重复),但仍递归子组件
transform(ast, {
nodeTransforms: [
(node) => {
if (!node.tag || typeof node.tag !== 'string') return
const tag = node.tag
// 只关心:v-tabs/v-tab-pane/v-card + 自定义组件递归
const shouldConsider =
tag === 'v-tabs' ||
tag === 'v-card' ||
tag === 'common-card' ||
isCustomComponent(tag)
if (!shouldConsider) return
if (isNonSearchable(node)) return
// ---- v-tabs ----
if (!hasSearchMeta && tag === 'v-tabs') {
dbg(`发现 v-tabs (文件: ${resolved})`)
// support/permission 从 v-tabs 上拿(可选)
const basePerm = clonePermissions(parentPermissions)
const tabsSupport = getBindValue(node, 'support')
const tabsPermission = getBindValue(node, 'permission')
if (tabsSupport) basePerm.support.push(tabsSupport)
if (tabsPermission) basePerm.permission.push(tabsPermission)
const tabsCond = getConditionExpr(node)
if (tabsCond) basePerm.support.push(tabsCond)
// A) :tabs="xxx"
let tabInfos = []
try {
const varName = getTabsBindingVar(node)
if (varName) {
// 尝试从 script 找到静态数组(或 computed 返回数组)
const init = findExportedConstInit(scriptBody, varName)
let arrExpr = init
if (arrExpr?.type === 'CallExpression' && arrExpr.callee?.name === 'computed') {
const fn = arrExpr.arguments?.[0]
if (fn?.type === 'ArrowFunctionExpression') {
if (fn.body?.type === 'ArrayExpression') arrExpr = fn.body
if (fn.body?.type === 'BlockStatement') {
const ret = fn.body.body?.find((x) => x.type === 'ReturnStatement')
if (ret?.argument?.type === 'ArrayExpression') arrExpr = ret.argument
}
}
}
if (arrExpr?.type === 'ArrayExpression') {
tabInfos = normalizeTabsElementsToTabInfos(arrExpr.elements || [])
}
}
} catch (e) {
warn('解析 :tabs 失败', e.message)
}
// B) tabArr 静态数组写法(你给的例子)
if (!tabInfos.length) {
// 常见命名 tabArr / tabs / tabList,你也可以扩展更多候选
const candidates = ['tabArr', 'tabs', 'tabList']
for (const c of candidates) {
const parsed = parseTabArr(scriptBody, c)
if (parsed.length) {
tabInfos = parsed
dbg(`通过 ${c} 提取到 tabInfos 长度=${tabInfos.length}`)
break
}
}
}
// C) 平铺 v-tab-pane(含 v-if/v-show)
if (!tabInfos.length) {
const tabPanes = (node.children || []).filter((c) => c.type === 1 && c.tag === 'v-tab-pane')
if (tabPanes.length) {
tabInfos = tabPanes.map((pane) => {
const key = getBindValue(pane, 'key') || getBindValue(pane, 'name') || undefined
// title/label/tab
const tabProp = getBindValue(pane, 'tab') || getBindValue(pane, 'title') || getBindValue(pane, 'label')
// 从 slot 里 t('xx') 兜底
let lanKey = null
if (typeof tabProp === 'string') {
// 如果是表达式字符串,比如 t('VIDEO.PLAYBACK'),尝试正则提取
const m = tabProp.match(/\$t\(\s*['"]([^'"]+)['"]\s*\)/) || tabProp.match(/t\(\s*['"]([^'"]+)['"]\s*\)/)
lanKey = m ? m[1] : tabProp
}
const cond = getConditionExpr(pane)
return { key, lanKey, componentName: null, cond }
})
}
}
if (!tabInfos.length) {
err(`[error][parser]: v-tabs 在 ${parentName} 中无法标准化解析,跳过。Path: ${resolved}`)
return
}
// 落盘 + 递归(componentName 或 key 作为候选)
tabInfos.forEach((tinfo, idx) => {
const currentPerm = clonePermissions(basePerm)
if (tinfo.cond) currentPerm.support.push(tinfo.cond)
const lanKey = tinfo.lanKey || `${parentName}.tab.idx${idx}`
const tabInfo = {
type: 'tab',
key: tinfo.key || `idx${idx}`,
lanKey,
permission: currentPerm.permission,
support: currentPerm.support,
isConditional: currentPerm.isConditional,
}
results.push(tabInfo)
// 递归解析:优先 componentName(tabArr/component),否则退化为 key 映射 import(你们老逻辑)
const tryKeys = []
if (tinfo.componentName) tryKeys.push(tinfo.componentName)
if (tinfo.key && !/^\d+$/.test(String(tinfo.key))) tryKeys.push(String(tinfo.key))
for (const k of tryKeys) {
const subPath = subComponentMap.get(normalize(k))
if (subPath) {
walk({
filePath: subPath,
parentName,
subComponentMap,
results,
parentTab: tabInfo,
parentPermissions: currentPerm,
visited,
})
break
}
}
})
}
// ---- v-card / common-card(仅当没有 meta 时启发式提取;你们更推荐 __SEARCH_META)----
if (!hasSearchMeta && (tag === 'v-card' || tag === 'common-card')) {
const titleLanKey = getTitle(node)
if (!titleLanKey) {
// 不强制报错(避免噪声),因为你们 card 可能用 div 写
return
}
const { key: parentTabKey, lanKey: parentTabLanKey } = parentTab || {}
const currentPerm = clonePermissions(parentPermissions)
const cardSupport = getBindValue(node, 'support')
const cardPermission = getBindValue(node, 'permission')
if (cardSupport) currentPerm.support.push(cardSupport)
if (cardPermission) currentPerm.permission.push(cardPermission)
const cond = getConditionExpr(node)
if (cond) currentPerm.support.push(cond)
const cardInfo = {
type: 'card',
lanKey: titleLanKey,
parentTabKey,
parentTabLanKey,
support: currentPerm.support,
permission: currentPerm.permission,
isConditional: currentPerm.isConditional,
selfConditional: !!cond,
parentConditional: currentPerm.isConditional,
}
results.push(cardInfo)
}
// ---- 递归自定义组件:无论是否有 meta,都保留(用来发现子组件的 __SEARCH_META)----
if (isCustomComponent(tag) && subComponentMap.has(normalize(tag))) {
const currentPerm = clonePermissions(parentPermissions)
const compSupport = getBindValue(node, 'support')
const compPermission = getBindValue(node, 'permission')
if (compSupport) currentPerm.support.push(compSupport)
if (compPermission) currentPerm.permission.push(compPermission)
const cond = getConditionExpr(node)
if (cond) currentPerm.support.push(cond)
// 如果组件本身有明确 support/permission,通常不应认为"纯条件隐藏"------按你的旧逻辑保留
currentPerm.isConditional = currentPerm.isConditional || (!!cond)
walk({
filePath: subComponentMap.get(normalize(tag)),
parentName,
subComponentMap,
results,
parentTab,
parentPermissions: currentPerm,
visited,
})
}
},
],
})
}
// ---- 导出 parseSfc 作为入口(同步返回 results 数组) ----
function parseSfcEntry(filePath, parentName = 'UNKNOWN') {
const subComponentMap = new Map()
const results = []
const visited = new Set()
try {
walk({
filePath,
parentName,
subComponentMap,
results,
parentTab: null,
parentPermissions: { isConditional: false, permission: [], support: [] },
visited,
})
} catch (e) {
err('[error][parseSfcEntry]: walk 失败', e && e.message)
}
return results
}
export { parseSfcEntry as parseSfc }
你接下来在工程里要注意的两个点(不改也能跑,但建议尽快补)
-
funcPermission/privilege需要"原始表达式字符串"我这里为了"最小可运行",对复杂表达式只保留了一个类型占位(比如
[SequenceExpression])。如果你们后续运行时需要"按表达式去 store 里取值",建议你在构建阶段把 AST 节点用
@babel/generator输出成完整代码字符串,例如:
-
MENU_MODULES.adminLog -
PERMISSION_MODULE.device -
MODULE_FUNC_LIST[PERMISSION_MODULE.device].addDevice -
或逗号表达式/数组表达式等
-
避免重复入库
现在的策略是:
-
当前组件有
__SEARCH_META→ tab/card 不做启发式重复提取 -
但仍会递归子组件,以便子组件 meta 被收集
如果你们页面/子组件都写了 meta,这个策略最稳。
如果你愿意把你们项目里一个真实的 v-tabs 页面组件 (包含 tabArr + v-tab-pane + __SEARCH_META 的混合情况)贴一段,我可以按你们实际模板结构把 tabArr / v-tab-pane 的 key 映射规则再压得更准(尤其是 v-tabs 的 props 命名差异:name/key/tab 等)。