@epxing/cli 0.5.0-beta.11 → 0.5.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ # 跨产品升级复核:从症状到验收
2
+
3
+ 本清单归纳 Vue2 → Vue3、Element UI → Element Plus、webpack → Vite 和远程产品库升级中的共性遗漏。迁移前用于确定覆盖范围,转换后用于审阅 diff,交付前用于检查真实入口。具体修法见 pitfalls.md;产品名称、页面 ID、截图位置和一次修复的命中数量只保留在该产品报告,不作为其他产品的配置默认值。
4
+
5
+ ## 高频问题与判据
6
+
7
+ | 症状 | 优先追踪的契约 | 验收证据 | 详细规则 |
8
+ | --- | --- | --- | --- |
9
+ | 列表显示对象 ID、代码,操作列空白 | 重写方法的 `{ scopedSlots: { default } }` 与调用点属性展开是两个独立断点;核对父类和辅助渲染链 | 名称存在时显示名称;原 ID 用于查看、编辑和提交;操作列有可点击内容;分组子列保留 | [F23](../pitfalls.md#f23-列表已有字段名称却显示-id代码旧单写插槽返回协议) |
10
+ | 有总数和表头,表体空白 | 第一条运行异常;表格样式回调返回类型;旧 `{ props: tableProps }` 包装;rowKey 和展开配置 | 有颜色/无颜色、父行/子行均显示;不出现数字 CSS 属性;字段参数真正到达表格 | [F24](../pitfalls.md#f24-控件初值不回显弹窗打不开模型属性和-jsx-事件仍是-vue2-协议) |
11
+ | 更多、设置按钮点了没有菜单 | dropdown/reference/footer 具名插槽是否进入接收端,触发节点是否符合当前组件契约 | 真实点击后菜单可见且选项可操作;不能只看 default 插槽存在 | [F31](../pitfalls.md#f31-触发按钮存在但菜单为空设置状态改变却不显示) |
12
+ | flag 已变 true,弹窗/抽屉不出现 | modelValue/update:modelValue、旧命名模型、ref 方法异常、内容渲染异常 | 从入口点击到可见内容、关闭、取消、再次打开;必要时检查 Teleport 裁剪 | F24、F31 |
13
+ | 图标还在但不能点,或编辑了错误字段 | 转换是否丢失事件、修饰符、循环变量、ID、附加类名、显示条件;相同图标是否错误复用了参数 | 点击入口验证对应单元格或页面;保留 stop/prevent;测试不同记录/不同等级/不同方案 | F31 |
14
+ | 拖拽列表不渲染、报 item slot 错误 | 替代依赖的组件 API、模板和 JSX 的 item 插槽、稳定键、可排序数据与固定操作入口 | 空/单项/多项渲染和排序回写;添加入口不参与排序 | [F29](../pitfalls.md#f29-拖拽组件升级后不渲染与步骤选中状态混淆) |
15
+ | 历史列表翻页异常、分页弃用警告 | 模板事件与 JSX 事件命名不同;当前页/每页数更新;sizes 所需监听 | 翻页及切换页大小改变绑定值与请求参数;目标版本无对应警告 | [F30](../pitfalls.md#f30-布局分页及可选配置的静默回退) |
16
+ | 页面贴边、卡片布局或弹窗对齐丢失 | render 根与 CSS 选择器不符、插槽后代缺 scopeId、选择框默认占满一行、父布局最小尺寸 | 实际继承/插槽链下计算样式正确;普通和窄容器可用;外部同名元素及其他页面不受影响 | F15、F19、F20、F30 |
17
+ | 初次渲染异常,异步筛选或回显不更新 | data/props/computed 声明、普通 filters 字段误当旧过滤器、Vue2 模型、对象/数组监听 | 从空初值加载到结果;保留 0、false;回显、清空、再次读取和请求序列化一致 | F16、F24 |
18
+ | 卡片标题错误、关闭图标后白屏、步骤选中混乱 | propsData 等旧结构、可选配置 null、当前项与完成数不同、页面参数类型 | 无图标/无标题分支仍可用;当前步骤可辨;回退和全部完成状态正确 | F22、F29、F30 |
19
+ | 本地修了,目标站仍旧或空白 | 实际加载的宿主/产品版本与 JS、CSS、清单、分片;服务 500 与渲染异常分别取证 | 同批制品、正确 MIME/加载方式、真实页面操作;旧缓存或临时注入不能当发布生效 | D3、F23、G |
20
+ | 库包夹带 index.html/制品不全 | 独立应用与远程库输出混淆、publicDir 复制、并发构建清理、懒加载清单遗漏 | 库入口与 CSS/依赖/分片匹配;应用构建按设计保留 HTML;上传形态完整 | B、C、下文制品检查 |
21
+
22
+ ## 模型与样式的逐组件必检项
23
+
24
+ 新增重点:按 [F32](../pitfalls.md#f32-同层插槽条件访问尚未声明的局部参数) 检查插槽参数与条件的词法作用域;按 F33 验证树刷新后的选中引用、业务响应式状态及依赖请求的不同返回顺序;按 F34 对账历史报告与实际证据,并检查移动滚动和底栏。以上是共性检查,不复制具体产品的参数、状态默认值或布局尺寸。
25
+
26
+ 以下项目并入现有 E2 台账,不另建一份互相脱节的清单。没有发现问题也须留下检查依据;确实没有模型或样式的组件注明原因,不靠组件名称或继承关系免检。
27
+
28
+ | 复核项 | 必须追踪的链路 | 关闭条件 |
29
+ | --- | --- | --- |
30
+ | 模型输入与回写 | template、JSX/TSX、h/render、辅助函数和包装组件两端;核对目标版本真实 props/emits、旧 model 选项、value/input、visible/update:visible、v-model 命名参数与 .sync | 初值、异步回显、编辑、清空、取消、再次打开及提交参数一致;0/false 不丢失。Element Plus 与平台命名模型分别核实,不批量将所有 value/visible 改成 modelValue |
31
+ | 弹层与受控开关 | 从真实按钮的事件、ref 方法、草稿初始化、模型更新到可见内容;区分确认后才提交的状态和即时双向绑定 | Drawer/Dialog 实际可见、关闭再开正确;取消确认不会提前改业务值。仅 flag=true 或回调被调用不足以通过 |
32
+ | 样式作用域与布局 | 实际渲染根、extends、具名插槽后代、Teleport;核对最终选择器和 scopeId,区分类名遗漏、规则未命中和父级布局问题 | 最终按需制品的计算样式、尺寸和滚动正确;组件外同名元素及切页不受影响。保留 scoped,按 F15 局部修复,不批量删 scoped 或全量加 deep |
33
+ | 样式值与主题 | 静态 CSS、模板 :style、JSX style、计算属性和表格回调;核对 UI 库回调返回值类型、主题变量、明暗和打印用途 | 表格 CSSProperties 对象符合消费契约,普通 DOM 字符串不误改;主题切换与组件使用场景正确。只转换 style 块不能覆盖动态样式 |
34
+ | 样式加载 | 实际使用的 JS/CSS/清单、组件依赖和加载时机,检查普通页面与设计/预览入口 | 更新本地构建插件并重建后,组件加载时样式齐备;遵循平台当前 CSS 按需协议,不以聚合 index.css 非空作为通用判据 |
35
+
36
+ 每个适用项记录“组件/入口 → 消费方契约 → 旧写法候选 → 修复或保留依据 → 源码/组件回归/制品/目标宿主证据 → 未验条件”。模板已转换不能替代脚本、JSX、辅助函数和包装链检查;静态工具没有覆盖的项保持待复核,不能填零后当作通过。按 F24、F15、E2 查具体修法,本表只规定复核和关闭条件。
37
+
38
+ ## 审阅顺序
39
+
40
+ 1. **保留升级前基线**:记录 Git 引用、工作区已有修改、入口、公开组件、依赖及宿主版本;不覆盖客户自定义内容。版本选择按当前平台兼容契约核实,不复制别的产品的锁文件、页面 ID 或版本号。依赖安装失败先恢复工具链,不用全局工具绕过项目校验。
41
+ 2. **沿实际调用链查两端**:渲染方法返回与消费位置、子控件模型与包装组件事件、ref 公开方法与调用顺序、字段展示与提交值分别核对。模板转换成功后仍检查 script、JS/TS、JSX/TSX、辅助函数及继承分支。
42
+ 3. **按同类结构扩展**:一处命中后检查同基类覆盖、复制组件、同样的插槽/样式/事件写法,记录候选和逐项结论。不要只修截图位置;也不要对所有控件全局扁平化、改模型或删除 scoped。
43
+ 4. **再验证真实行为**:先复用项目现有回归,关键交互需要从真实入口触发。VNodes、隔离夹具、最终制品和目标宿主各自能证明的范围不同。
44
+
45
+ ## 四层证据分别记录
46
+
47
+ | 层次 | 能证明什么 | 不能替代什么 |
48
+ | --- | --- | --- |
49
+ | 源码、AST、ESLint | 已覆盖的旧语法候选、类型/调用结构和源文件合法性 | 动态别名、元数据、权限、实际 DOM |
50
+ | 组件回归/本地浏览器 | 真实模板或 JSX 的事件参数、状态切换、插槽内容、局部布局 | 目标宿主所用 UI 版本、真实数据与权限、正式写入 |
51
+ | 最终库/应用产物 | 入口注册、资源、样式隔离和打包形态 | 远端是否已加载本批制品 |
52
+ | 目标宿主入口 | 当前角色、菜单、懒加载子页、实际制品、业务数据下的可见行为 | 未进入的分支或未执行的写操作 |
53
+
54
+ 在 E2 台账记录:组件/入口、适用分支、问题类型、证据层次、实际结果、制品版本、待办。状态用“通过”“已改待验”“待发布”“缺条件待验”“有依据的不适用”等,不能将缺环境记成通过,也不能把未确认的按钮名称或接口配置猜成通用修复。
55
+
56
+ ## 最小交互场景
57
+
58
+ - **列表**:有名称的对象/代码项、操作列、父子/分组列、带颜色与无颜色的行、0 值和空结果;查看和提交仍携带原 ID。
59
+ - **弹层**:点击入口 → 当前记录/草稿 → 可见内容 → 关闭/取消 → 再开另一记录。验证旧状态未残留,打开本身不会意外保存。增加 append-to-body 只能解决已确认的容器裁剪,不能代替模型迁移。
60
+ - **编辑**:点击哪个等级/字段,就只切换那一格;模型更新、失焦/change、保存参数和错误恢复对应同一记录。不能只用一个通用 spy 验证方法被调用。
61
+ - **结构与布局**:真实树的层级和展开/收起;拖拽顺序;步骤当前项;空卡片;工具栏及导出字段行在普通与窄容器下的排列。SVG 图标变化后检查 hover/隐藏/焦点选择器。
62
+ - **上下文与数据**:项目/模板、角色、页签、返回及缓存复用按组件已有能力覆盖。区分接口无名称、无数据、服务失败与前端不渲染;不为凑齐截图而写入默认业务数据。
63
+
64
+ 有保存/删除/清空/计算/同步等动作时,接口替身或拦截只证明待发送参数;实际写入沿当前任务授权进行,未写入就如实记为模拟回归。不要通过真实批量操作来证明按钮已接线。
65
+
66
+ ## 随技能提供的只读扫描
67
+
68
+ 从目标工程解析本地依赖;将 `<技能目录>` 和 `<前端工程根目录>` 换为实际路径。先确认工程已有 Vue 编译器和 Babel 依赖,缺失时报工具链问题,不临时拉全局依赖来制造通过。
69
+
70
+ ```text
71
+ node <技能目录>/scripts/check-list-slots.cjs <前端工程根目录>
72
+ node <技能目录>/scripts/check-draggable-slots.cjs <前端工程根目录>
73
+ node <技能目录>/scripts/check-icon-migration.cjs <前端工程根目录> <升级前Git引用>
74
+ ```
75
+
76
+ | 脚本 | 覆盖 | 退出码与边界 |
77
+ | --- | --- | --- |
78
+ | check-list-slots | 同层 v-slot 与 if/else-if 对插槽参数的引用(含解构和别名);render 旧 scopedSlots、列渲染函数展开、表格旧 props 包装及静态绑定的表格样式回调字符串返回 | 0:无此类候选;1:有候选或插槽表达式需复核。不自动改条件链;对象属性函数、动态别名、h 第三参及运行时元数据继续审阅 |
79
+ | check-draggable-slots | 模板 vuedraggable 导入别名、直接 item 插槽、item-key;纯 JSX 标为复核 | 0:已扫描项无候选;1:明确缺项;2:需人工复核。JSX 和动态插槽继续验证 |
80
+ | check-icon-migration | 对比升级前模板的静态图标顺序、事件/参数/修饰符和条件,接受事件移动到祖先按钮 | 0:未发现保留差异;1:候选或无法对齐。新增图标、结构调整需要人工判断;普通属性、CSS、JSX 另查 |
81
+
82
+ 脚本不自动改业务代码;0 不等于产品验收通过。解析/依赖失败同样会非零退出,须读报告区分,不能忽略 stderr。项目回归命令以目标产品真实文件为准,不要求其他产品照搬本产品测试文件或造同名测试。
83
+
84
+ ## 制品检查
85
+
86
+ - 核对 package.json 与构建入口真正实现的命令,不凭脚本名称判断懒加载/完整库;同一输出目录的构建串行执行。
87
+ - 远程库不需要独立应用 HTML 时,检查 Vite 的公共目录复制和 copyPublicDir,按用途处理。独立应用的 index.html 是入口,不能一律删除。
88
+ - 验最终入口、CSS、依赖清单及实际分片来自同批;本地替换远程库时也提供原字节和正确加载方式,经典 UMD 入口不能误经 Vite 模块转换注入 import。
89
+ - 宿主和产品版本分别记。没有目标环境时可交付本地修改、回归及产物,但远端页面、权限和主题验收继续保留待办。
@@ -0,0 +1,36 @@
1
+ // 只读检查模板中的 Vue3 拖拽插槽;JSX 和动态插槽需继续做渲染验证。
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const { createRequire } = require('node:module')
5
+ const root = path.resolve(process.argv[2] || '.')
6
+ const local = createRequire(path.join(root, 'package.json'))
7
+ const { parse } = local('vue/compiler-sfc')
8
+ const vue = createRequire(local.resolve('vue/package.json'))
9
+ const { parse: parseTemplate } = vue('@vue/compiler-dom')
10
+ const findings = []
11
+ let checked = 0
12
+ function files (dir) {
13
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? files(path.join(dir, e.name)) : [path.join(dir, e.name)])
14
+ }
15
+ for (const file of files(path.join(root, 'src')).filter(f => f.endsWith('.vue'))) {
16
+ const source = fs.readFileSync(file, 'utf8')
17
+ const aliases = [...source.matchAll(/import\s+(\w+)\s+from\s+['"]vuedraggable(?:\/[^'"]*)?['"]/g)].map(m => m[1])
18
+ if (!aliases.length) continue
19
+ const descriptor = parse(source).descriptor
20
+ if (!descriptor.template) {
21
+ findings.push({ file: path.relative(root, file), status: 'review', reason: 'JSX/render 拖拽:核对 item 插槽函数、稳定 itemKey 和排序回写,需真实渲染' })
22
+ continue
23
+ }
24
+ function visit (node) {
25
+ if (node.type === 1 && aliases.some(alias => alias === node.tag || alias.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() === node.tag)) {
26
+ checked++
27
+ const item = node.children.find(n => n.type === 1 && n.tag === 'template' && n.props.some(p => p.type === 7 && p.name === 'slot' && p.arg?.content === 'item'))
28
+ const key = node.props.some(p => p.name === 'item-key' || (p.type === 7 && p.arg?.content === 'item-key'))
29
+ if (!item || !key) findings.push({ file: path.relative(root, file), line: node.loc.start.line, status: 'error', reason: !item ? '缺少直接子项 item 插槽' : '缺少 item-key' })
30
+ }
31
+ for (const child of node.children || []) visit(child)
32
+ }
33
+ visit(parseTemplate(descriptor.template.content))
34
+ }
35
+ console.log(JSON.stringify({ checked, findings }, null, 2))
36
+ process.exitCode = findings.some(f => f.status === 'error') ? 1 : findings.length ? 2 : 0
@@ -0,0 +1,56 @@
1
+ // 只读比较升级前 Git 基线中的图标交互。
2
+ // node check-icon-migration.cjs <frontend-root> <pre-migration-ref>
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+ const { execFileSync } = require('node:child_process')
6
+ const { createRequire } = require('node:module')
7
+ const root = path.resolve(process.argv[2] || '.')
8
+ const ref = process.argv[3]
9
+ if (!ref || ref.startsWith('-')) throw new Error('Supply the pre-migration Git ref as the second argument')
10
+ const req = createRequire(path.join(root, 'package.json'))
11
+ const { parse: sfc } = req('vue/compiler-sfc')
12
+ const { parse } = createRequire(req.resolve('vue/package.json'))('@vue/compiler-dom')
13
+ const prefix = execFileSync('git', ['rev-parse', '--show-prefix'], { cwd: root, encoding: 'utf8' }).trim()
14
+ function files(dir) {
15
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? files(path.join(dir, e.name)) : [path.join(dir, e.name)])
16
+ }
17
+ function nodes(node, result = [], ancestors = []) {
18
+ if (node.type === 1) result.push({ node, ancestors })
19
+ for (const child of node.children || []) nodes(child, result, [...ancestors, node])
20
+ return result
21
+ }
22
+ function iconKey(node) {
23
+ if (node.tag === 'i') return node.props.find(p => p.name === 'class')?.value?.content.match(/\bel-icon-[\w-]+/)?.[0]
24
+ if (node.tag === 'el-icon') return node.children.find(c => c.type === 1 && /^el-icon-/.test(c.tag))?.tag
25
+ }
26
+ const signature = p => JSON.stringify([p.name, p.arg?.content || '', p.exp?.content.replace(/\s+/g, '') || '', [...(p.modifiers || [])].map(m => m.content || m).sort()])
27
+ let compared = 0, icons = 0
28
+ const findings = []
29
+ for (const file of files(path.join(root, 'src')).filter(f => f.endsWith('.vue'))) {
30
+ const relative = path.relative(root, file).replace(/\\/g, '/')
31
+ let before
32
+ try { before = execFileSync('git', ['show', `${ref}:${prefix}${relative}`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) } catch { continue }
33
+ const oldTemplate = sfc(before).descriptor.template
34
+ const newTemplate = sfc(fs.readFileSync(file, 'utf8')).descriptor.template
35
+ if (!oldTemplate || !newTemplate) continue
36
+ const oldNodes = nodes(parse(oldTemplate.content))
37
+ const newNodes = nodes(parse(newTemplate.content))
38
+ const oldIcons = oldNodes.filter(e => iconKey(e.node))
39
+ const newIcons = newNodes.filter(e => iconKey(e.node))
40
+ compared++
41
+ if (oldIcons.length !== newIcons.length || oldIcons.some((e,i) => iconKey(e.node) !== iconKey(newIcons[i].node))) {
42
+ findings.push({ file: relative, reason: 'Icon sequence changed; manual alignment required' })
43
+ continue
44
+ }
45
+ for (const [index, entry] of oldIcons.entries()) {
46
+ const node = entry.node, current = newIcons[index]
47
+ // 允许事件移至真实按钮祖先;其他图标上的同名表达式不能掩盖当前图标的错误。
48
+ const currentDirectives = new Set([current.node, ...current.ancestors].flatMap(n => (n.props || []).filter(p => p.type === 7).map(signature)))
49
+ icons++
50
+ for (const prop of node.props.filter(p => p.type === 7 && ['on', 'if', 'show', 'bind'].includes(p.name))) {
51
+ if (!currentDirectives.has(signature(prop))) findings.push({ file: relative, line: prop.loc.start.line + oldTemplate.loc.start.line - 1, directive: prop.loc.source })
52
+ }
53
+ }
54
+ }
55
+ console.log(JSON.stringify({ compared, icons, findings, scope: 'template icon order and directive preservation on the icon or its ancestors; plain attributes, CSS, JSX and runtime behavior still require review' }, null, 2))
56
+ process.exitCode = findings.length ? 1 : 0
@@ -0,0 +1,95 @@
1
+ // 只读扫描列表插槽与表格样式回调,参数为前端工程根目录。
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const { createRequire } = require('node:module')
5
+ const root = path.resolve(process.argv[2] || '.')
6
+ const req = createRequire(path.join(root, 'package.json'))
7
+ const babel = req('@babel/core')
8
+ const t = babel.types
9
+ const { parse } = req('vue/compiler-sfc')
10
+ const findings = []
11
+ let files = 0, columns = 0
12
+ const tableStyles = new Set(['cell-style', 'row-style', 'header-cell-style', 'header-row-style', 'cellStyle', 'rowStyle', 'headerCellStyle', 'headerRowStyle'])
13
+ function walk(dir) {
14
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e =>
15
+ e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])
16
+ }
17
+ function hasRenderer(node) {
18
+ if (!node || typeof node !== 'object') return false
19
+ if (t.isCallExpression(node) && t.isMemberExpression(node.callee) &&
20
+ /^render/.test(node.callee.property.name || '')) return true
21
+ return Object.values(node).some(v => Array.isArray(v) ? v.some(hasRenderer) : hasRenderer(v))
22
+ }
23
+ for (const file of walk(path.join(root, 'src')).filter(f => /\.(vue|[cm]?[jt]sx?)$/.test(f))) {
24
+ const source = fs.readFileSync(file, 'utf8')
25
+ const parsed = file.endsWith('.vue') ? parse(source) : null
26
+ if (parsed?.errors.length) throw new Error(file + ':Vue 文件解析失败:' + parsed.errors.map(error => error.message || error).join('; '))
27
+ const descriptor = parsed?.descriptor || null
28
+ const styleCallbacks = new Set()
29
+ function templateBindings(node) {
30
+ if (node.type === 1) {
31
+ const slot = node.props.find(p => p.type === 7 && p.name === 'slot' && p.exp)
32
+ const condition = node.props.find(p => p.type === 7 && ['if', 'else-if'].includes(p.name) && p.exp)
33
+ if (slot && condition) {
34
+ const location = { file: path.relative(root, file), line: condition.loc.start.line }
35
+ try {
36
+ const options = { configFile: false, babelrc: false, parserOpts: { plugins: ['typescript'] } }
37
+ const parameters = babel.parseSync(`(${slot.exp.content}) => 0`, options).program.body[0].expression.params
38
+ const names = new Set(parameters.flatMap(param => Object.keys(t.getBindingIdentifiers(param))))
39
+ const expression = babel.parseSync(`(${condition.exp.content})`, options)
40
+ const referenced = new Set()
41
+ babel.traverse(expression, { ReferencedIdentifier(p) {
42
+ if (names.has(p.node.name) && !p.scope.hasBinding(p.node.name)) referenced.add(p.node.name)
43
+ } })
44
+ if (referenced.size) findings.push({ ...location, reason: '同层插槽条件与局部参数同名引用:' + [...referenced].join(', ') + ';核对外层同名状态,若依赖插槽参数则将条件移入内部并保留完整 else 链' })
45
+ } catch (error) {
46
+ findings.push({ ...location, reason: '插槽参数或条件无法静态解析,需复核作用域:' + error.message })
47
+ }
48
+ }
49
+ }
50
+ if (node.type === 1 && node.tag === 'el-table') for (const p of node.props) {
51
+ if (p.type === 7 && p.name === 'bind' && tableStyles.has(p.arg?.content) && /^[\w$]+$/.test(p.exp?.content || '')) styleCallbacks.add(p.exp.content)
52
+ }
53
+ for (const child of node.children || []) templateBindings(child)
54
+ }
55
+ if (descriptor?.template?.ast) templateBindings(descriptor.template.ast)
56
+ const blocks = file.endsWith('.vue')
57
+ ? [descriptor.script, descriptor.scriptSetup].filter(Boolean).map(b => ({ code: b.content, lang: b.lang, offset: b.loc.start.line - 1 }))
58
+ : [{ code: source, lang: /tsx?$/.test(file) ? 'ts' : 'js', offset: 0 }]
59
+ files++
60
+ for (const block of blocks) {
61
+ const ast = babel.parseSync(block.code, { configFile: false, babelrc: false,
62
+ parserOpts: { plugins: ['jsx', ...(['ts', 'tsx'].includes(block.lang) ? ['typescript'] : [])] } })
63
+ const report = (node, reason) => findings.push({ file: path.relative(root, file), line: node.loc.start.line + block.offset, reason })
64
+ babel.traverse(ast, { JSXElement(p) {
65
+ if (p.node.openingElement.name.name !== 'el-table') return
66
+ for (const attr of p.node.openingElement.attributes) {
67
+ const expr = attr.value?.expression
68
+ if (tableStyles.has(attr.name?.name) && t.isMemberExpression(expr) && t.isThisExpression(expr.object)) styleCallbacks.add(expr.property.name)
69
+ }
70
+ } })
71
+ babel.traverse(ast, {
72
+ ReturnStatement(p) {
73
+ const method = p.findParent(a => a.isObjectMethod() || a.isClassMethod())
74
+ if (styleCallbacks.has(method?.node.key.name) && (t.isStringLiteral(p.node.argument) || t.isTemplateLiteral(p.node.argument))) report(p.node, 'table style callback returns a string; inspect CSSProperties object contract')
75
+ },
76
+ ObjectProperty(p) {
77
+ if ((p.node.key.name || p.node.key.value) !== 'scopedSlots') return
78
+ const method = p.findParent(a => a.isObjectMethod() || a.isClassMethod())
79
+ if (method && /^render/.test(method.node.key.name || '')) report(p.node, 'legacy scopedSlots inside renderer; inspect return contract')
80
+ },
81
+ JSXElement(p) {
82
+ if (p.node.openingElement.name.name === 'el-table') for (const attr of p.node.openingElement.attributes) {
83
+ if (t.isJSXSpreadAttribute(attr) && t.isObjectExpression(attr.argument) && attr.argument.properties.some(a => t.isObjectProperty(a) && (a.key.name || a.key.value) === 'props')) report(attr, 'legacy nested props on table; pass Vue3 props directly')
84
+ }
85
+ if (p.node.openingElement.name.name !== 'el-table-column') return
86
+ columns++
87
+ for (const attr of p.node.openingElement.attributes) {
88
+ if (t.isJSXSpreadAttribute(attr) && hasRenderer(attr.argument)) report(attr, 'renderer spread into props, including conditional branches; inspect v-slots contract')
89
+ }
90
+ }
91
+ })
92
+ }
93
+ }
94
+ console.log(JSON.stringify({ files, columns, findings, scope: '同层插槽参数条件、script 插槽/props、静态绑定的表格样式回调;别名、h() 消费方与运行时元数据仍需复核' }, null, 2))
95
+ process.exitCode = findings.length ? 1 : 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epxing/cli",
3
- "version": "0.5.0-beta.11",
3
+ "version": "0.5.0-beta.13",
4
4
  "description": "EPX 交互式工程开发、服务启动、构建、Vue3 迁移及技能安装工具",
5
5
  "type": "module",
6
6
  "bin": {