@epxing/cli 0.5.0-beta.0 → 0.5.0-beta.10
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.
- package/API.md +5 -5
- package/README.md +119 -96
- package/dist/cli.mjs +20 -12
- package/dist/lazy-lib.mjs +22 -22
- package/dist/lib-vite-config.mjs +1 -0
- package/dist/platform-version.mjs +48 -0
- package/dist/skills/epxing-frontend-vue3-migrate/SKILL.md +211 -144
- package/dist/skills/epxing-frontend-vue3-migrate/pitfalls.md +743 -498
- package/dist/skills/epxing-frontend-vue3-migrate/scripts/fix-eval-paths.cjs +255 -0
- package/dist/tools/chunks/7NAADJFP.mjs +45 -0
- package/dist/tools/chunks/{AMQWUBTZ.mjs → 7YE2UHC6.mjs} +2 -2
- package/dist/tools/chunks/J7B2EWKH.mjs +8 -0
- package/dist/tools/chunks/{PP5RD4RH.mjs → U27ZPQAM.mjs} +3 -3
- package/dist/tools/chunks/VASFHXYG.mjs +251 -0
- package/dist/tools/epx-create/scaffold.mjs +73 -57
- package/dist/tools/epx-migrate/detect-vue3-risks.mjs +1 -1
- package/dist/tools/epx-migrate/fix-date-formats.mjs +1 -1
- package/dist/tools/epx-migrate/fix-theme-styles.mjs +1 -1
- package/dist/tools/epx-migrate/migrate.mjs +67 -267
- package/dist/tools/epx-migrate/verify-components.mjs +6 -6
- package/dist/vite.mjs +2 -2
- package/package.json +2 -2
- package/dist/tools/chunks/H4POBCHI.mjs +0 -1
- package/dist/tools/chunks/SH6LMMHZ.mjs +0 -32
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 将参数占位符中的动态属性读取迁移为公开路径读取;业务表达式只报告。
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { createRequire } = require('node:module')
|
|
6
|
+
|
|
7
|
+
function dependencies (project) {
|
|
8
|
+
const requireProject = createRequire(path.join(project, 'package.json'))
|
|
9
|
+
try {
|
|
10
|
+
return {
|
|
11
|
+
parser: requireProject('@babel/parser'),
|
|
12
|
+
traverse: requireProject('@babel/traverse').default,
|
|
13
|
+
sfc: requireProject('@vue/compiler-sfc')
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error('需要目标工程已安装的 @babel/parser、@babel/traverse 和 @vue/compiler-sfc;未自动安装依赖。' + error.message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nameOf (node) {
|
|
21
|
+
return node?.name || node?.value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function componentObject (nodePath) {
|
|
25
|
+
if (!nodePath?.isObjectExpression()) return false
|
|
26
|
+
const parent = nodePath.parentPath
|
|
27
|
+
return parent?.isExportDefaultDeclaration() ||
|
|
28
|
+
(parent?.isCallExpression() && nameOf(parent.node.callee) === 'defineComponent' &&
|
|
29
|
+
parent.node.arguments[0] === nodePath.node)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function thisOwner (nodePath) {
|
|
33
|
+
let fn = nodePath.getFunctionParent()
|
|
34
|
+
while (fn?.isArrowFunctionExpression()) fn = fn.getFunctionParent()
|
|
35
|
+
if (!fn || (!fn.isObjectMethod() && !fn.isFunctionExpression())) return null
|
|
36
|
+
let member = fn.isObjectMethod() ? fn : fn.parentPath
|
|
37
|
+
if (!member?.isObjectMethod() && !member?.isObjectProperty()) return null
|
|
38
|
+
// 普通嵌套函数会改变 this,不能仅因位于组件文件中就自动处理。
|
|
39
|
+
let object = member.parentPath
|
|
40
|
+
for (let level = 0; level < 4 && object?.isObjectExpression(); level++) {
|
|
41
|
+
if (componentObject(object)) {
|
|
42
|
+
return ['methods', 'computed', 'watch', 'created', 'mounted', 'render',
|
|
43
|
+
'beforeCreate', 'beforeMount', 'updated', 'beforeUpdate',
|
|
44
|
+
'unmounted', 'beforeUnmount', 'data', 'setup'].includes(nameOf(member.node.key))
|
|
45
|
+
? (nameOf(member.node.key) === 'setup' ? null : fn)
|
|
46
|
+
: null
|
|
47
|
+
}
|
|
48
|
+
member = object.parentPath
|
|
49
|
+
if (!member?.isObjectProperty()) return null
|
|
50
|
+
object = member.parentPath
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function placeholderRegex (node) {
|
|
56
|
+
return node?.type === 'RegExpLiteral' &&
|
|
57
|
+
/(?:\\\$\\\[|#\\\[|#\\\{)/.test(node.pattern)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function replaceCall (node) {
|
|
61
|
+
return node?.type === 'CallExpression' && node.callee.type === 'MemberExpression' &&
|
|
62
|
+
!node.callee.computed && node.callee.property.name === 'replace' &&
|
|
63
|
+
placeholderRegex(node.arguments[0])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function placeholderKey (argPath) {
|
|
67
|
+
if (!argPath.isIdentifier()) return false
|
|
68
|
+
const binding = argPath.scope.getBinding(argPath.node.name)
|
|
69
|
+
if (!binding || !binding.constant) return false
|
|
70
|
+
const fn = binding.scope.path
|
|
71
|
+
if (binding.kind === 'param' && fn.isFunction() &&
|
|
72
|
+
fn.node.params[1]?.type === 'Identifier' && fn.node.params[1].name === argPath.node.name &&
|
|
73
|
+
replaceCall(fn.parentPath.node) && fn.parentPath.node.arguments[1] === fn.node) return true
|
|
74
|
+
if (!binding.path.isVariableDeclarator() || !replaceCall(binding.path.node.init)) return false
|
|
75
|
+
const callback = binding.path.node.init.arguments[1]
|
|
76
|
+
if (callback?.type === 'StringLiteral') return callback.value === '$1'
|
|
77
|
+
if (!callback || !['ArrowFunctionExpression', 'FunctionExpression'].includes(callback.type)) return false
|
|
78
|
+
const capture = callback.params[1]
|
|
79
|
+
if (capture?.type !== 'Identifier') return false
|
|
80
|
+
const body = callback.body.type === 'BlockStatement'
|
|
81
|
+
? (callback.body.body.length === 1 && callback.body.body[0].type === 'ReturnStatement'
|
|
82
|
+
? callback.body.body[0].argument : null)
|
|
83
|
+
: callback.body
|
|
84
|
+
return body?.type === 'Identifier' && body.name === capture.name
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function contextFor (callPath, root) {
|
|
88
|
+
if (root === 'this') return thisOwner(callPath) ? { root } : null
|
|
89
|
+
const binding = callPath.scope.getBinding(root)
|
|
90
|
+
if (binding) {
|
|
91
|
+
if (!binding.constant || !binding.path.isVariableDeclarator() ||
|
|
92
|
+
binding.path.node.init?.type !== 'ThisExpression' || !thisOwner(binding.path)) return null
|
|
93
|
+
return { root }
|
|
94
|
+
}
|
|
95
|
+
// 只在已证实属于组件的词法 this 中修复旧代码缺少的常见别名。
|
|
96
|
+
return ['that', '_this'].includes(root) && thisOwner(callPath) ? { root: 'this' } : null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function analyzeCode (code, { filename, lang = '', offset = 0, source = code }, deps) {
|
|
100
|
+
const plugins = []
|
|
101
|
+
if (/tsx?$/.test(lang) || /\.[cm]?tsx?$/.test(filename)) plugins.push('typescript')
|
|
102
|
+
if (lang !== 'ts' && !/\.[cm]?ts$/.test(filename)) plugins.push('jsx')
|
|
103
|
+
const ast = deps.parser.parse(code, { sourceType: 'unambiguous', plugins })
|
|
104
|
+
const edits = []
|
|
105
|
+
const findings = []
|
|
106
|
+
deps.traverse(ast, {
|
|
107
|
+
CallExpression (callPath) {
|
|
108
|
+
const node = callPath.node
|
|
109
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== 'eval') return
|
|
110
|
+
// 同名业务函数不是全局 eval。
|
|
111
|
+
if (callPath.scope.getBinding('eval')) return
|
|
112
|
+
const finding = {
|
|
113
|
+
file: filename,
|
|
114
|
+
line: source.slice(0, offset + node.start).split('\n').length,
|
|
115
|
+
kind: 'review',
|
|
116
|
+
reason: '表达式、配置解析或未知路径来源,保留原代码并人工核对上下文。',
|
|
117
|
+
before: code.slice(node.start, node.end)
|
|
118
|
+
}
|
|
119
|
+
const value = node.arguments[0]
|
|
120
|
+
if (node.arguments.length === 1 && value?.type === 'TemplateLiteral' &&
|
|
121
|
+
value.expressions.length === 1 && value.quasis[1].value.cooked === '') {
|
|
122
|
+
const prefix = value.quasis[0].value.cooked
|
|
123
|
+
const match = /^(this|[A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*\.$/.exec(prefix)
|
|
124
|
+
const exprPath = callPath.get('arguments.0.expressions.0')
|
|
125
|
+
if (match && placeholderKey(exprPath)) {
|
|
126
|
+
const context = contextFor(callPath, match[1])
|
|
127
|
+
if (context) {
|
|
128
|
+
const object = context.root + prefix.slice(match[1].length, -1)
|
|
129
|
+
const key = code.slice(value.expressions[0].start, value.expressions[0].end)
|
|
130
|
+
const after = context.root + '.$_.get(' + object + ', ' + key + ')'
|
|
131
|
+
edits.push({ start: offset + node.start, end: offset + node.end, text: after })
|
|
132
|
+
Object.assign(finding, { kind: 'automatic', after, reason: '参数占位符路径读取,上下文已确认。' })
|
|
133
|
+
} else {
|
|
134
|
+
finding.reason = '参数占位符命中,但别名或 this 上下文无法确认。'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
findings.push(finding)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
return { edits, findings }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function analyzeFile (filename, source, deps) {
|
|
145
|
+
if (!filename.endsWith('.vue')) return analyzeCode(source, { filename }, deps)
|
|
146
|
+
const parsed = deps.sfc.parse(source, { filename })
|
|
147
|
+
if (parsed.errors.length) throw new Error(parsed.errors.map(e => e.message || e).join('; '))
|
|
148
|
+
const result = { edits: [], findings: [] }
|
|
149
|
+
for (const block of [parsed.descriptor.script, parsed.descriptor.scriptSetup]) {
|
|
150
|
+
if (!block) continue
|
|
151
|
+
if (block.src) {
|
|
152
|
+
result.findings.push({ file: filename, line: block.loc.start.line, kind: 'review', reason: '外部 script src 需核对是否在本次 src 扫描范围内。' })
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
const part = analyzeCode(block.content, {
|
|
156
|
+
filename, lang: block.lang || '', offset: block.loc.start.offset, source
|
|
157
|
+
}, deps)
|
|
158
|
+
result.edits.push(...part.edits)
|
|
159
|
+
result.findings.push(...part.findings)
|
|
160
|
+
}
|
|
161
|
+
const template = parsed.descriptor.template
|
|
162
|
+
for (const match of (template?.content || '').matchAll(/\beval\s*\(/g)) {
|
|
163
|
+
result.findings.push({ file: filename, line: template.loc.start.line + template.content.slice(0, match.index).split('\n').length - 1, kind: 'review', reason: '模板内 eval 需人工核对模板上下文。' })
|
|
164
|
+
}
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sourceFiles (directory) {
|
|
169
|
+
const result = []
|
|
170
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
171
|
+
if (entry.isSymbolicLink()) continue
|
|
172
|
+
const file = path.join(directory, entry.name)
|
|
173
|
+
if (entry.isDirectory() && !['node_modules', '.git', '.cache', 'dist', 'lib'].includes(entry.name)) result.push(...sourceFiles(file))
|
|
174
|
+
else if (entry.isFile() && /\.(vue|[cm]?js|jsx|[cm]?ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) result.push(file)
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function migrate ({ project, apply = false, reportFile }, deps = dependencies(project)) {
|
|
180
|
+
project = path.resolve(project)
|
|
181
|
+
const src = path.join(project, 'src')
|
|
182
|
+
if (!fs.existsSync(src)) throw new Error('目标必须是包含 src 的前端工程:' + project)
|
|
183
|
+
const files = sourceFiles(src)
|
|
184
|
+
const pending = []
|
|
185
|
+
const findings = []
|
|
186
|
+
const errors = []
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
189
|
+
try {
|
|
190
|
+
const result = analyzeFile(file, source, deps)
|
|
191
|
+
findings.push(...result.findings)
|
|
192
|
+
if (result.edits.length) {
|
|
193
|
+
let changed = source
|
|
194
|
+
for (const edit of result.edits.sort((a, b) => b.start - a.start)) changed = changed.slice(0, edit.start) + edit.text + changed.slice(edit.end)
|
|
195
|
+
pending.push({ file, source, changed })
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
errors.push({ file, message: error.message })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const report = {
|
|
202
|
+
version: 1, project, mode: apply ? 'apply' : 'preview', filesScanned: files.length,
|
|
203
|
+
automatic: findings.filter(f => f.kind === 'automatic').length,
|
|
204
|
+
review: findings.filter(f => f.kind === 'review').length,
|
|
205
|
+
filesChanged: [], findings, errors
|
|
206
|
+
}
|
|
207
|
+
// 先完成全部解析;任一文件失败时不修改源码。
|
|
208
|
+
if (apply && !errors.length && pending.length) {
|
|
209
|
+
const backupRoot = path.join(project, '.cache', 'eval-path-backups')
|
|
210
|
+
fs.mkdirSync(backupRoot, { recursive: true })
|
|
211
|
+
const backup = fs.mkdtempSync(path.join(backupRoot, 'run-'))
|
|
212
|
+
for (const entry of pending) {
|
|
213
|
+
const target = path.join(backup, path.relative(project, entry.file))
|
|
214
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
215
|
+
fs.writeFileSync(target, entry.source)
|
|
216
|
+
}
|
|
217
|
+
// 防止扫描到写入之间覆盖其它人的新改动。
|
|
218
|
+
for (const entry of pending) if (fs.readFileSync(entry.file, 'utf8') !== entry.source) throw new Error('扫描后文件已变化,未写入源码:' + entry.file)
|
|
219
|
+
for (const entry of pending) fs.writeFileSync(entry.file, entry.changed)
|
|
220
|
+
report.backup = backup
|
|
221
|
+
report.filesChanged = pending.map(entry => entry.file)
|
|
222
|
+
}
|
|
223
|
+
report.exitCode = errors.length ? 1 : ((report.review || (!apply && report.automatic)) ? 2 : 0)
|
|
224
|
+
if (reportFile) {
|
|
225
|
+
fs.mkdirSync(path.dirname(path.resolve(reportFile)), { recursive: true })
|
|
226
|
+
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2) + '\n')
|
|
227
|
+
}
|
|
228
|
+
return report
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (require.main === module) {
|
|
232
|
+
try {
|
|
233
|
+
const args = process.argv.slice(2)
|
|
234
|
+
if (args.includes('--help')) {
|
|
235
|
+
console.log('用法:node fix-eval-paths.cjs <前端工程> [--apply] [--report <JSON文件>]\n默认只读预览;--apply 先备份后改写。退出码:0 已处理,2 有待修复/待复核项,1 解析或依赖失败。')
|
|
236
|
+
} else {
|
|
237
|
+
const options = { project: null, apply: false }
|
|
238
|
+
for (let i = 0; i < args.length; i++) {
|
|
239
|
+
if (args[i] === '--apply') options.apply = true
|
|
240
|
+
else if (args[i] === '--report' && args[i + 1] && !args[i + 1].startsWith('--')) options.reportFile = args[++i]
|
|
241
|
+
else if (!args[i].startsWith('--') && !options.project) options.project = path.resolve(args[i])
|
|
242
|
+
else throw new Error('未知或不完整参数:' + args[i])
|
|
243
|
+
}
|
|
244
|
+
if (!options.project) throw new Error('请指定前端工程;使用 --help 查看用法。')
|
|
245
|
+
const report = migrate(options)
|
|
246
|
+
console.log(JSON.stringify(report, null, 2))
|
|
247
|
+
process.exitCode = report.exitCode
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(error.message)
|
|
251
|
+
process.exitCode = 1
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { dependencies, analyzeCode, analyzeFile, migrate }
|