这是json

下面给你一份可直接替换现有 parser.js 的增强版实现(仍保持"最小可运行"的思路),重点做了你要求的三件事:

  1. 组件优先导出 __SEARCH_META 时优先解析 (并保留权限字段:privilege / funcPermission 等原始表达式字符串)

  2. 完善递归解析能力 :仍然通过 import 建立子组件映射,并递归进入子组件;为避免死循环加了 visited

  3. 保留并增强 v-tabs 解析能力

    • 平铺 v-tab-pane(含 v-if/v-show 渲染条件提取)

    • tabArr 在同文件静态数组写法(提取 t('...') / $t('...') 的多语言 key,并把渲染条件如 supportPlayback || supportLiveView 放到 support 链里)

    • :tabs="xxx" 的静态数组/computed(() => [...]) 场景继续支持

    • 重要 :对 tabArrcomponent: 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 }

你接下来在工程里要注意的两个点(不改也能跑,但建议尽快补)

  1. funcPermission / privilege 需要"原始表达式字符串"

    我这里为了"最小可运行",对复杂表达式只保留了一个类型占位(比如 [SequenceExpression])。

    如果你们后续运行时需要"按表达式去 store 里取值",建议你在构建阶段把 AST 节点用 @babel/generator 输出成完整代码字符串,例如:

  • MENU_MODULES.adminLog

  • PERMISSION_MODULE.device

  • MODULE_FUNC_LIST[PERMISSION_MODULE.device].addDevice

  • 或逗号表达式/数组表达式等

  1. 避免重复入库

    现在的策略是:

  • 当前组件有 __SEARCH_METAtab/card 不做启发式重复提取

  • 但仍会递归子组件,以便子组件 meta 被收集

    如果你们页面/子组件都写了 meta,这个策略最稳。


如果你愿意把你们项目里一个真实的 v-tabs 页面组件 (包含 tabArr + v-tab-pane + __SEARCH_META 的混合情况)贴一段,我可以按你们实际模板结构把 tabArr / v-tab-pane 的 key 映射规则再压得更准(尤其是 v-tabs 的 props 命名差异:name/key/tab 等)。

相关推荐
Dxy12393102161 小时前
Python 将 JSON 字符串转换为字典
前端·python·json
叫我一声阿雷吧2 小时前
【JS实战案例】实现图片懒加载(基础版)原生JS+性能优化,新手可直接复现
开发语言·javascript·性能优化·js图片懒加载
colicode2 小时前
语音提醒接口开发方案:日程安排与待办事项自动电话提醒的集成思路
前端·前端框架·语音识别
爱上妖精的尾巴2 小时前
8-8 WPS JS宏 正则表达式 字符组与任选
java·服务器·前端
山岚的运维笔记2 小时前
SQL Server笔记 -- 第34章:cross apply
服务器·前端·数据库·笔记·sql·microsoft·sqlserver
前端程序猿i3 小时前
第 8 篇:Markdown 渲染引擎 —— 从流式解析到安全输出
开发语言·前端·javascript·vue.js·安全
coding随想3 小时前
告别构建焦虑!用 Shoelace 打造零配置的现代 Web 应用
前端
css趣多多3 小时前
resize.js
前端·javascript·vue.js
_codemonster3 小时前
java web修改了文件和新建了文件需要注意的问题
java·开发语言·前端