@epxing/cli 0.0.0-stage → 0.2.0
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/README.md +71 -2
- package/dist/THIRD_PARTY_NOTICES.txt +21 -0
- package/dist/cli.mjs +230 -0
- package/dist/project.mjs +178 -0
- package/dist/prompts.mjs +86 -0
- package/dist/skills/epxing-frontend-vue3-migrate/SKILL.md +129 -0
- package/dist/skills/epxing-frontend-vue3-migrate/pitfalls.md +381 -0
- package/dist/tools/epx-create/scaffold.mjs +248 -0
- package/dist/tools/epx-migrate/detect-vue3-risks.mjs +3 -0
- package/dist/tools/epx-migrate/migrate.mjs +80 -0
- package/dist/tools/epx-migrate/verify-components.mjs +10 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @epxing/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
独立的工程开发工具,包含交互菜单、开发服务、工程与产品库构建、创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖。启动、构建调用当前工程的脚本及本地依赖;迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
npm install -g @epxing/cli@0.2.0 --registry https://registry.npmjs.org/
|
|
7
|
+
epx # ↑↓ + Enter 选择;Esc / Ctrl+C 取消
|
|
8
|
+
epx dev --port 3000
|
|
9
|
+
epx build --mode prd
|
|
10
|
+
epx lib # 产品库整体构建;平台目录构建聚合组件库
|
|
11
|
+
epx lib --lazy # 产品按需库(工程需支持此构建形态)
|
|
12
|
+
epx create my-product --epx-frame <目标平台版本>
|
|
13
|
+
epx migrate E:/products/old-product --epx-frame <目标平台版本>
|
|
14
|
+
epx migrate E:/products/old-product --epx-frame <目标平台版本> --apply
|
|
15
|
+
epx skill install --dir E:/products/my-product
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
如已全局安装旧的 `epx-frame` CLI,先用 `npm uninstall -g epx-frame` 释放 `epx` 命令。产品工程里的 `epx-frame` 依赖保留。工具不在安装阶段运行迁移、下载平台或改产品文件;产品仍需安装目标框架依赖才能开发和构建。
|
|
19
|
+
|
|
20
|
+
使用 fnm/nvm 时,不同 Node 版本的全局包分别安装。切换到日常使用的 Node 版本后更新 CLI,再用 `epx help` 确认开头为 `@epxing/cli`、`epx --version` 确认版本。旧仓库的 `pnpm epx` 仍是该仓库自己的脚本。
|
|
21
|
+
|
|
22
|
+
## 命令
|
|
23
|
+
|
|
24
|
+
| 命令 | 行为 |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `epx` / `epx menu` | 交互选择操作、工程目录、环境、端口和构建方式;管道支持数字输入 |
|
|
27
|
+
| `epx dev` / `epx serve` | 使用工程 `dev`、`serve` 或 `start` 脚本启动服务 |
|
|
28
|
+
| `epx build` | 使用工程的构建脚本;只有 `build:lib` 的产品自动构建产品库 |
|
|
29
|
+
| `epx lib` / `epx build --lib` | 优先产品 `build:lib` / `lib`,平台使用完整 `build:component`;识别旧 `--target lib` 构建脚本 |
|
|
30
|
+
| `epx lib --lazy` | 优先 `build:lib:lazy`,否则为产品库脚本设置 `LAZY_LIB=true`;工程需支持按需构建 |
|
|
31
|
+
| `epx preview` | 使用 `preview` / `vite:preview`;没有脚本时使用工程本地 Vite |
|
|
32
|
+
| `epx create` | 无参数进入创建引导,也可用参数直接创建 |
|
|
33
|
+
| `epx migrate` / `epx upgrade` | 无参数引导:目标版本、迁移范围、预览、确认应用 |
|
|
34
|
+
| `epx skill install` | 安装技能,支持自定义目录和明确覆盖 |
|
|
35
|
+
| `epx scan` / `epx verify` | 迁移风险扫描 / 本地与线上组件清单校验 |
|
|
36
|
+
| `epx lint [--fix]` / `epx circular` | 执行工程已有的代码检查、循环依赖检查脚本 |
|
|
37
|
+
| `epx remote` / `epx pkg <包名\|all>` | 使用平台已有 `build:remote` / `build:pkg` / `build:pkgs` 脚本 |
|
|
38
|
+
| `epx help` / `epx --version` | 帮助 / 工具版本 |
|
|
39
|
+
|
|
40
|
+
工程命令支持 `--dir <目录>`,默认从当前目录向上找到最近的 `package.json`;不会改为 CLI 全局安装目录。没有项目脚本时,dev/build/preview 可使用项目已安装的 Vite,不会自动下载构建工具。工程仍需先安装自身依赖。
|
|
41
|
+
|
|
42
|
+
`--mode/-m`、`--port/-p` 支持旧的 `-mprd`、`-p3000` 写法。build 优先匹配 `build:<mode>`;`localize` 也匹配 `build:local`,其余情况向构建脚本传入 `--mode`。平台 `build:component` 包含多步构建,不追加 `--mode`,环境由工程脚本统一配置。
|
|
43
|
+
|
|
44
|
+
平台 `epx build` 保留 `--thin`、`--no-designer`、`--no-dashboard`;产品工程不接受平台裁剪选项。需向工程脚本透传其他参数时使用 `--`,如 `epx dev -- --host 0.0.0.0`。脚本原有的 pre/post 生命周期、自定义环境和多步构建会保留,失败退出码向上传递。
|
|
45
|
+
|
|
46
|
+
`create` 和 `migrate --apply` 自动写入 `runtime: "vue3"`。带参数直接调用时 `--epx-frame` 必填,交互引导也会要求输入目标平台。迁移默认预览;apply 要求目标 Git 工作树干净。实际执行 `.vue` 转换时才按需安装并缓存 gogocode。标识表示迁移目标,仍需完成转换、构建和业务复测。
|
|
47
|
+
|
|
48
|
+
`epx scan <目录>` 扫描静态风险,`epx verify <目录> --online <地址|文件>` 对照组件清单。技能安装到 `.claude/skills/epxing-frontend-vue3-migrate/`,可用 `--skills-dir` 指定其他客户端要求的位置。已有技能默认不覆盖;确认替换使用 `--force`。平台基类源码分析仍需准备对应版本源码或文档。
|
|
49
|
+
|
|
50
|
+
在框架源码仓库发布工具,不需要构建平台 `lib/`:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
cd frontend/frame/packages/cli
|
|
54
|
+
npm login --registry https://registry.npmjs.org/
|
|
55
|
+
# 发布前调整为未发布过的新版本
|
|
56
|
+
# npm version <新的CLI版本> --no-git-tag-version
|
|
57
|
+
$previousPublicPublish = $env:EPX_PUBLISH_PUBLIC
|
|
58
|
+
try {
|
|
59
|
+
$env:EPX_PUBLISH_PUBLIC = '1'
|
|
60
|
+
npm publish --access public --registry https://registry.npmjs.org/
|
|
61
|
+
} finally {
|
|
62
|
+
$env:EPX_PUBLISH_PUBLIC = $previousPublicPublish
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
发布账号需要拥有 `@epxing` 名称空间的发布权限,并完成 npm 要求的发布身份验证。`publishConfig` 固定公开发布到 npm 公网;上面的命令仅为这次操作启用共享守卫的显式公开发布开关,并在结束后恢复原值。`prepack` 自动构建工具并带入当前技能文件。可在框架根执行 `pnpm build:cli`,或在这里运行 `npm pack` 生成可独立安装的 tgz。技能与 CLI 同版本交付,平台升级不要求工具同步发版;工具规则、模板或技能更新时才发布新 CLI。新平台若改变接口或构建约定,仍需验证并更新相应工具规则。
|
|
67
|
+
|
|
68
|
+
在框架根运行 `npm --prefix packages/cli test`,验证真实 tgz 的离线安装、无平台依赖运行、创建、迁移、扫描和技能安装。
|
|
69
|
+
|
|
70
|
+
首次发布前,需创建或加入 npm 的 `epxing` 组织。可用 `npm org ls epxing --json --registry https://registry.npmjs.org/` 核对组织角色。若发布返回 `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`,表示当前发布凭据未满足 npm 要求;仅 `npm whoami` 成功不代表可以发布。可在 npm 网站创建具有目标 scope 发布权限、启用 `Bypass 2FA` 的细粒度令牌,并配置到本机用户 npm 设置;令牌不写入项目或发布包。参见 [npm 发布身份验证要求](https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification/)。
|
|
71
|
+
|
|
72
|
+
令牌的 `Packages and scopes` 权限应选择 `Read and write (publish and stage)`,范围覆盖 `@epxing`;`Read and write (stage only)` 无法直接发布。`Organizations` 里的读写权限只管理组织、团队和成员,不授予组织包的发布权限。首次创建包时需覆盖相应 scope。参见 [npm 令牌权限说明](https://docs.npmjs.com/creating-and-viewing-access-tokens/)。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@babel/parser 7.29.3
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI 输出 */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { findProject, runProject } from './project.mjs'
|
|
8
|
+
import { Prompts } from './prompts.mjs'
|
|
9
|
+
|
|
10
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const payload = path.join(packageRoot, 'dist')
|
|
12
|
+
const skillName = 'epxing-frontend-vue3-migrate'
|
|
13
|
+
|
|
14
|
+
function takeOption (args, name) {
|
|
15
|
+
const index = args.findIndex(arg => arg === name || arg.startsWith(name + '='))
|
|
16
|
+
if (index < 0) return null
|
|
17
|
+
const arg = args[index]
|
|
18
|
+
if (arg.startsWith(name + '=')) {
|
|
19
|
+
args.splice(index, 1)
|
|
20
|
+
const value = arg.slice(name.length + 1)
|
|
21
|
+
if (!value) throw new Error(`${name} 缺少值`)
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
const value = args[index + 1]
|
|
25
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} 缺少值`)
|
|
26
|
+
args.splice(index, 2)
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireTarget (args) {
|
|
31
|
+
const target = takeOption(args, '--epx-frame')
|
|
32
|
+
if (!target) throw new Error('请用 --epx-frame 指定目标平台版本或包 spec;CLI 版本不作为平台版本。')
|
|
33
|
+
return target
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runTool (script, args, cwd) {
|
|
37
|
+
const result = spawnSync(process.execPath, [path.join(payload, 'tools', script), ...args], { cwd, stdio: 'inherit' })
|
|
38
|
+
if (result.error) throw result.error
|
|
39
|
+
return result.status ?? 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function installSkill (args, cwd) {
|
|
43
|
+
const dir = path.resolve(cwd, takeOption(args, '--dir') || '.')
|
|
44
|
+
const custom = takeOption(args, '--skills-dir')
|
|
45
|
+
const force = args.includes('--force')
|
|
46
|
+
const unknown = args.filter(arg => arg !== '--force')
|
|
47
|
+
if (unknown.length) throw new Error(`不支持的参数:${unknown.join(' ')}`)
|
|
48
|
+
const dest = path.join(custom ? path.resolve(dir, custom) : path.join(dir, '.claude/skills'), skillName)
|
|
49
|
+
const files = ['SKILL.md', 'pitfalls.md']
|
|
50
|
+
if (!force && files.some(file => fs.existsSync(path.join(dest, file)))) {
|
|
51
|
+
throw new Error(`技能已存在:${dest}。如需替换,核对自定义内容后使用 --force。`)
|
|
52
|
+
}
|
|
53
|
+
// 先读齐资源,再写入,避免缺少发布资源时只安装一半。
|
|
54
|
+
const contents = files.map(file => fs.readFileSync(path.join(payload, 'skills', skillName, file)))
|
|
55
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
56
|
+
files.forEach((file, index) => fs.writeFileSync(path.join(dest, file), contents[index]))
|
|
57
|
+
console.log(`✓ 已安装技能:${dest}`)
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function interactive (cwd, action) {
|
|
62
|
+
const prompts = new Prompts()
|
|
63
|
+
const dispatch = argv => { prompts.close(); return main(argv, cwd) }
|
|
64
|
+
const choices = pairs => pairs.map(([label, value]) => ({ label, value }))
|
|
65
|
+
const mode = async () => {
|
|
66
|
+
const value = await prompts.select('环境模式', choices([
|
|
67
|
+
['使用工程默认环境', ''], ['dev', 'dev'], ['pre', 'pre'], ['ppe', 'ppe'], ['prd', 'prd'],
|
|
68
|
+
['localize', 'localize'], ['development', 'development'], ['production', 'production'], ['输入其他模式', '*']
|
|
69
|
+
]))
|
|
70
|
+
return value === '*' ? prompts.required('模式名称') : value
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
console.log('@epxing/cli · 当前目录:' + cwd)
|
|
74
|
+
if (!action) {
|
|
75
|
+
action = await prompts.select('选择操作', choices([
|
|
76
|
+
['dev 启动开发服务', 'dev'], ['build 工程打包', 'build'], ['lib 产品库 / 平台组件库打包', 'lib'],
|
|
77
|
+
['preview 预览构建结果', 'preview'], ['create 创建产品 / 项目', 'create'], ['migrate 升级到 Vue 3', 'migrate'],
|
|
78
|
+
['skill 安装升级技能', 'skill'], ['check 风险扫描 / 组件校验 / 代码检查', 'check'],
|
|
79
|
+
['advanced 平台独立包 / 远程模块构建', 'advanced'], ['help 查看命令帮助', 'help'], ['exit 退出', 'exit']
|
|
80
|
+
]))
|
|
81
|
+
}
|
|
82
|
+
if (action === 'exit') return 0
|
|
83
|
+
if (action === 'help') return dispatch(['help'])
|
|
84
|
+
if (action === 'create') {
|
|
85
|
+
const name = await prompts.required('产品名或目标路径')
|
|
86
|
+
const target = await prompts.required('目标 epx-frame 版本或包 spec')
|
|
87
|
+
const dir = await prompts.text('生成目录', name)
|
|
88
|
+
return dispatch(['create', name, '--dir', dir, '--epx-frame', target])
|
|
89
|
+
}
|
|
90
|
+
if (action === 'migrate') {
|
|
91
|
+
const dir = await prompts.required('待升级工程目录', '.')
|
|
92
|
+
const target = await prompts.required('目标 epx-frame 版本或包 spec')
|
|
93
|
+
const flags = await prompts.select('迁移范围', choices([
|
|
94
|
+
['依赖与入口迁移', []], ['依赖、入口与 Vue 组件转换', ['--transform-vue']],
|
|
95
|
+
['依赖、Vue 组件及 Vite 构建迁移', ['--transform-vue', '--to-vite']]
|
|
96
|
+
]))
|
|
97
|
+
const args = ['migrate', dir, '--epx-frame', target, ...flags]
|
|
98
|
+
const preview = await main(args, cwd)
|
|
99
|
+
if (preview !== 0 || !(await prompts.confirm('应用以上迁移?要求 Git 工作树干净;默认不修改'))) return preview
|
|
100
|
+
return dispatch([...args, '--apply'])
|
|
101
|
+
}
|
|
102
|
+
if (action === 'skill') {
|
|
103
|
+
const dir = await prompts.required('安装到哪个项目', '.')
|
|
104
|
+
const location = await prompts.required('项目中的技能目录', '.claude/skills')
|
|
105
|
+
const dest = path.resolve(cwd, dir, location, skillName)
|
|
106
|
+
const args = ['skill', 'install', '--dir', dir, '--skills-dir', location]
|
|
107
|
+
if (['SKILL.md', 'pitfalls.md'].some(file => fs.existsSync(path.join(dest, file)))) {
|
|
108
|
+
if (!(await prompts.confirm(`技能已存在,替换 ${dest} 中的两个技能文件?`))) return 0
|
|
109
|
+
args.push('--force')
|
|
110
|
+
}
|
|
111
|
+
return dispatch(args)
|
|
112
|
+
}
|
|
113
|
+
if (action === 'check') {
|
|
114
|
+
action = await prompts.select('检查类型', choices([
|
|
115
|
+
['源码迁移风险扫描', 'scan'], ['本地 / 线上组件清单核对', 'verify'], ['项目代码检查', 'lint'], ['项目循环依赖检查', 'circular']
|
|
116
|
+
]))
|
|
117
|
+
}
|
|
118
|
+
if (action === 'advanced') {
|
|
119
|
+
action = await prompts.select('平台构建', choices([
|
|
120
|
+
['business 远程模块', 'remote'], ['独立组件包', 'pkg']
|
|
121
|
+
]))
|
|
122
|
+
}
|
|
123
|
+
const dir = await prompts.required('工程目录', '.')
|
|
124
|
+
if (action === 'scan' || action === 'verify') {
|
|
125
|
+
const online = action === 'verify' ? await prompts.text('线上 JS 地址或文件(留空仅列本地组件)') : ''
|
|
126
|
+
return dispatch([action, dir, ...(online ? ['--online', online] : [])])
|
|
127
|
+
}
|
|
128
|
+
const args = [action, '--dir', dir]
|
|
129
|
+
const project = findProject(path.resolve(cwd, dir))
|
|
130
|
+
const platformLib = action === 'lib' && project.scripts['build:component'] && !project.scripts['build:lib'] && !project.scripts.lib
|
|
131
|
+
if (['dev', 'build', 'lib', 'preview'].includes(action) && !platformLib) {
|
|
132
|
+
const selectedMode = await mode()
|
|
133
|
+
if (selectedMode) args.push('--mode', selectedMode)
|
|
134
|
+
}
|
|
135
|
+
if (action === 'dev' || action === 'preview') {
|
|
136
|
+
const port = await prompts.text('端口(留空使用工程默认值)')
|
|
137
|
+
if (port) args.push('--port', port)
|
|
138
|
+
}
|
|
139
|
+
if (action === 'build') {
|
|
140
|
+
if (project.pkg.name === 'epx-frame' || project.scripts['build:component']) {
|
|
141
|
+
if (await prompts.confirm('将 business 拆为远程模块,构建瘦平台?')) args.push('--thin')
|
|
142
|
+
if (await prompts.confirm('裁剪页面设计器?')) args.push('--no-designer')
|
|
143
|
+
if (await prompts.confirm('裁剪看板?')) args.push('--no-dashboard')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (action === 'lib') {
|
|
147
|
+
if ((project.scripts['build:lib'] || project.scripts.lib) && await prompts.confirm('构建按需产品库?')) args.push('--lazy')
|
|
148
|
+
}
|
|
149
|
+
if (action === 'pkg') args.push(await prompts.required('包名或 all', 'all'))
|
|
150
|
+
if (action === 'lint' && await prompts.confirm('自动修复代码?')) args.push('--fix')
|
|
151
|
+
return dispatch(args)
|
|
152
|
+
} finally { prompts.close() }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function main (argv = process.argv.slice(2), cwd = process.env.INIT_CWD || process.cwd()) {
|
|
156
|
+
let [command, ...args] = argv
|
|
157
|
+
if (command === 'serve') command = 'dev'
|
|
158
|
+
if (command === 'upgrade') command = 'migrate'
|
|
159
|
+
const separator = args.indexOf('--')
|
|
160
|
+
if (args.slice(0, separator < 0 ? args.length : separator).some(arg => arg === '--help' || arg === '-h')) return main(['help'], cwd)
|
|
161
|
+
if (!command || command === 'menu') return interactive(cwd)
|
|
162
|
+
if (['create', 'migrate'].includes(command) && !args.length) return interactive(cwd, command)
|
|
163
|
+
if (['dev', 'build', 'lib', 'preview', 'remote', 'pkg', 'lint', 'circular'].includes(command)) return runProject(command, args, cwd)
|
|
164
|
+
if (['help', '--help', '-h'].includes(command)) {
|
|
165
|
+
console.log(`@epxing/cli · 工程开发与升级工具(独立于平台版本)
|
|
166
|
+
|
|
167
|
+
epx / epx menu 交互菜单(↑↓ + Enter;管道支持数字输入)
|
|
168
|
+
epx dev [-m <模式>] [-p <端口>] [--dir <工程目录>]
|
|
169
|
+
epx build [-m <模式>] [--thin] [--no-designer] [--no-dashboard] [--dir <目录>]
|
|
170
|
+
epx lib [--lazy] [-m <模式>] [--dir <目录>] 产品 build:lib / 平台 build:component
|
|
171
|
+
epx build --lib 同 epx lib
|
|
172
|
+
epx preview [-p <端口>] [--dir <目录>]
|
|
173
|
+
epx create <名称|路径> --epx-frame <目标版本> [--dir <目录>]
|
|
174
|
+
epx migrate [项目目录] --epx-frame <目标版本> [--apply] [--transform-vue] [--to-vite]
|
|
175
|
+
epx scan [项目目录]
|
|
176
|
+
epx verify [项目目录] [--online <线上JS地址|文件>]
|
|
177
|
+
epx skill install [--dir <项目目录>] [--skills-dir <技能目录>] [--force]
|
|
178
|
+
epx lint [--fix] [--dir <目录>]
|
|
179
|
+
epx circular [--dir <目录>]
|
|
180
|
+
epx remote [--dir <平台目录>]
|
|
181
|
+
epx pkg <包名|all> [--dir <平台目录>]
|
|
182
|
+
epx --version
|
|
183
|
+
|
|
184
|
+
dev / build / lib 等执行当前工程的 package.json 脚本,使用工程本地依赖。
|
|
185
|
+
工程命令可用 -- 后的参数透传给脚本;serve / upgrade 分别是 dev / migrate 的别名。
|
|
186
|
+
create、migrate 不带参数时进入引导;直接带参数时 --epx-frame 必填。
|
|
187
|
+
创建和升级自动写入 runtime: "vue3"。迁移默认 dry-run,--apply 才落盘。
|
|
188
|
+
工具不会自动安装平台依赖;.vue 转换工具仅在执行 --apply --transform-vue 时下载并缓存。`)
|
|
189
|
+
return 0
|
|
190
|
+
}
|
|
191
|
+
if (['--version', '-v', 'version'].includes(command)) {
|
|
192
|
+
console.log(JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version)
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
if (command === 'skill') {
|
|
196
|
+
if (args.shift() !== 'install') throw new Error('用法:epx skill install [--dir <项目目录>]')
|
|
197
|
+
return installSkill(args, cwd)
|
|
198
|
+
}
|
|
199
|
+
if (command === 'create') {
|
|
200
|
+
const target = requireTarget(args)
|
|
201
|
+
const chosenDir = takeOption(args, '--dir')
|
|
202
|
+
const nameOrPath = args.shift()
|
|
203
|
+
if (!nameOrPath || args.length || nameOrPath.startsWith('-')) throw new Error('用法:epx create <名称|路径> --epx-frame <目标版本> [--dir <目录>]')
|
|
204
|
+
const dir = path.resolve(cwd, chosenDir || nameOrPath)
|
|
205
|
+
const name = /[\\/]/.test(nameOrPath) ? path.basename(path.resolve(cwd, nameOrPath)) : nameOrPath
|
|
206
|
+
const { scaffold } = await import(pathToFileURL(path.join(payload, 'tools/epx-create/scaffold.mjs')).href)
|
|
207
|
+
scaffold({ name, dir, epxFrameSpec: target })
|
|
208
|
+
return 0
|
|
209
|
+
}
|
|
210
|
+
if (command === 'migrate') {
|
|
211
|
+
const target = requireTarget(args)
|
|
212
|
+
const dirs = args.filter(arg => !arg.startsWith('-'))
|
|
213
|
+
if (dirs.length > 1) throw new Error('一次迁移一个项目目录')
|
|
214
|
+
const dir = dirs[0] || '.'
|
|
215
|
+
return runTool('epx-migrate/migrate.mjs', [path.resolve(cwd, dir), ...args.filter(arg => arg !== dir), '--epx-frame', target], cwd)
|
|
216
|
+
}
|
|
217
|
+
if (command === 'scan' || command === 'verify') {
|
|
218
|
+
const online = command === 'verify' ? takeOption(args, '--online') : null
|
|
219
|
+
if (args.length > 1 || args.some(arg => arg.startsWith('-'))) throw new Error(`用法:epx ${command} [项目目录]${command === 'verify' ? ' [--online <地址|文件>]' : ''}`)
|
|
220
|
+
const script = command === 'scan' ? 'detect-vue3-risks.mjs' : 'verify-components.mjs'
|
|
221
|
+
return runTool('epx-migrate/' + script, [path.resolve(cwd, args[0] || '.'), ...(online ? ['--online', online] : [])], cwd)
|
|
222
|
+
}
|
|
223
|
+
throw new Error(`未知命令:${command},运行 epx help 查看用法。`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let isMain = false
|
|
227
|
+
try { isMain = !!process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url) } catch (error) { /* 作为模块导入 */ }
|
|
228
|
+
if (isMain) {
|
|
229
|
+
main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = error.exitCode || 1 })
|
|
230
|
+
}
|
package/dist/project.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* eslint-disable no-console -- 输出项目命令 */
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
export function findProject (cwd) {
|
|
8
|
+
let dir = path.resolve(cwd)
|
|
9
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new Error(`工程目录不存在:${dir}`)
|
|
10
|
+
while (true) {
|
|
11
|
+
const file = path.join(dir, 'package.json')
|
|
12
|
+
if (fs.existsSync(file)) {
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
14
|
+
return { dir, pkg, scripts: pkg.scripts || {} }
|
|
15
|
+
}
|
|
16
|
+
const parent = path.dirname(dir)
|
|
17
|
+
if (parent === dir) throw new Error(`未找到项目 package.json:${cwd}。请进入项目目录,或指定 --dir。`)
|
|
18
|
+
dir = parent
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function npmEntry () {
|
|
23
|
+
const nodeDir = path.dirname(process.execPath)
|
|
24
|
+
const roots = [nodeDir, ...String(process.env.PATH || process.env.Path || '').split(path.delimiter)]
|
|
25
|
+
const candidates = [process.env.npm_execpath && /npm-cli\.js$/.test(process.env.npm_execpath) ? process.env.npm_execpath : null]
|
|
26
|
+
for (const dir of roots) {
|
|
27
|
+
if (!dir) continue
|
|
28
|
+
candidates.push(path.join(dir, 'node_modules/npm/bin/npm-cli.js'), path.resolve(dir, '../lib/node_modules/npm/bin/npm-cli.js'))
|
|
29
|
+
const executable = path.join(dir, 'npm')
|
|
30
|
+
try {
|
|
31
|
+
const real = fs.realpathSync(executable)
|
|
32
|
+
if (real.endsWith('.js')) candidates.push(real)
|
|
33
|
+
} catch (error) { /* 继续查找 */ }
|
|
34
|
+
}
|
|
35
|
+
const found = candidates.find(file => file && fs.existsSync(file))
|
|
36
|
+
if (!found) throw new Error('未找到 npm-cli.js。请使用带 npm 的 Node.js 环境运行 epx。')
|
|
37
|
+
return found
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function execute (file, args, cwd, extraEnv) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const child = spawn(process.execPath, [file, ...args], { cwd, stdio: 'inherit', env: { ...process.env, ...extraEnv } })
|
|
43
|
+
const interrupt = () => child.kill('SIGINT')
|
|
44
|
+
const terminate = () => child.kill('SIGTERM')
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
process.removeListener('SIGINT', interrupt)
|
|
47
|
+
process.removeListener('SIGTERM', terminate)
|
|
48
|
+
}
|
|
49
|
+
process.on('SIGINT', interrupt)
|
|
50
|
+
process.on('SIGTERM', terminate)
|
|
51
|
+
child.once('error', error => { cleanup(); reject(error) })
|
|
52
|
+
child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1)) })
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function localBinary (project, name) {
|
|
57
|
+
const require = createRequire(path.join(project.dir, 'package.json'))
|
|
58
|
+
try {
|
|
59
|
+
const manifest = require.resolve(name + '/package.json')
|
|
60
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'))
|
|
61
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[name]
|
|
62
|
+
const file = bin && path.resolve(path.dirname(manifest), bin)
|
|
63
|
+
if (file && fs.existsSync(file)) return file
|
|
64
|
+
} catch (error) { /* 由调用方提示项目安装依赖 */ }
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function takeValue (args, flag, short) {
|
|
69
|
+
const index = args.findIndex(arg => arg === flag || arg.startsWith(flag + '=') || (short && (arg === short || arg.startsWith(short))))
|
|
70
|
+
if (index < 0) return null
|
|
71
|
+
const arg = args[index]
|
|
72
|
+
let value
|
|
73
|
+
if (arg.startsWith(flag + '=')) { value = arg.slice(flag.length + 1); args.splice(index, 1) } else if (short && arg.startsWith(short) && arg !== short) { value = arg.slice(short.length); args.splice(index, 1) } else {
|
|
74
|
+
value = args[index + 1]
|
|
75
|
+
if (!value || value.startsWith('-')) throw new Error(`${flag} 缺少值`)
|
|
76
|
+
args.splice(index, 2)
|
|
77
|
+
}
|
|
78
|
+
if (!value) throw new Error(`${flag} 缺少值`)
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseProjectArgs (argv) {
|
|
83
|
+
const args = [...argv]
|
|
84
|
+
const separator = args.indexOf('--')
|
|
85
|
+
const forward = separator >= 0 ? args.splice(separator).slice(1) : []
|
|
86
|
+
const flag = name => { const index = args.indexOf(name); if (index < 0) return false; args.splice(index, 1); return true }
|
|
87
|
+
const result = {
|
|
88
|
+
dir: takeValue(args, '--dir'),
|
|
89
|
+
mode: takeValue(args, '--mode', '-m'),
|
|
90
|
+
port: takeValue(args, '--port', '-p'),
|
|
91
|
+
thin: flag('--thin'),
|
|
92
|
+
noDesigner: flag('--no-designer'),
|
|
93
|
+
noDashboard: flag('--no-dashboard'),
|
|
94
|
+
lazy: flag('--lazy'),
|
|
95
|
+
lib: flag('--lib'),
|
|
96
|
+
fix: flag('--fix'),
|
|
97
|
+
forward,
|
|
98
|
+
positional: args
|
|
99
|
+
}
|
|
100
|
+
if (result.mode && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(result.mode)) throw new Error('mode 仅支持字母、数字、点、下划线和短横线')
|
|
101
|
+
if (result.port && (!/^\d+$/.test(result.port) || +result.port < 1 || +result.port > 65535)) throw new Error('端口需为 1–65535 的整数')
|
|
102
|
+
return result
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function projectPlan (command, options, project) {
|
|
106
|
+
const { scripts } = project
|
|
107
|
+
const pick = (...names) => names.find(name => name && typeof scripts[name] === 'string' && scripts[name].trim())
|
|
108
|
+
const args = []
|
|
109
|
+
const env = {}
|
|
110
|
+
let script
|
|
111
|
+
let binary
|
|
112
|
+
const platform = project.pkg.name === 'epx-frame' || !!scripts['build:component']
|
|
113
|
+
if (options.lib) {
|
|
114
|
+
if (command !== 'build') throw new Error('--lib 用于 epx build;也可直接使用 epx lib')
|
|
115
|
+
command = 'lib'
|
|
116
|
+
}
|
|
117
|
+
if (options.positional.length && !(command === 'pkg' && options.positional.length === 1 && !options.positional[0].startsWith('-'))) throw new Error(`不支持的参数:${options.positional.join(' ')}`)
|
|
118
|
+
if (options.port && !['dev', 'preview'].includes(command)) throw new Error('--port 仅用于 dev 或 preview')
|
|
119
|
+
if (options.lazy && command !== 'lib') throw new Error('--lazy 仅用于 lib 或 build --lib')
|
|
120
|
+
if (options.fix && command !== 'lint') throw new Error('--fix 仅用于 lint')
|
|
121
|
+
if ((options.thin || options.noDesigner || options.noDashboard) && (command !== 'build' || !platform)) throw new Error('平台裁剪选项仅用于平台源码中的 epx build')
|
|
122
|
+
if (options.mode && !['dev', 'build', 'lib', 'preview'].includes(command)) throw new Error('--mode 仅用于 dev、build、lib 或 preview')
|
|
123
|
+
if (command === 'dev') {
|
|
124
|
+
script = pick('dev', 'serve', 'start')
|
|
125
|
+
if (!script) binary = 'vite'
|
|
126
|
+
} else if (command === 'build') {
|
|
127
|
+
const modeScript = options.mode && pick('build:' + options.mode, options.mode === 'localize' && 'build:local')
|
|
128
|
+
script = modeScript || pick('build', 'build:lib')
|
|
129
|
+
if (!script) binary = 'vite'
|
|
130
|
+
if (modeScript) options = { ...options, mode: null }
|
|
131
|
+
if (options.thin) env.EXTERNALIZE_BUSINESS = 'true'
|
|
132
|
+
if (options.noDesigner) env.INCLUDE_DESIGNER = 'false'
|
|
133
|
+
if (options.noDashboard) env.INCLUDE_DASHBOARD = 'false'
|
|
134
|
+
} else if (command === 'lib') {
|
|
135
|
+
script = options.lazy ? pick('build:lib:lazy') : pick('build:lib', 'lib', 'build:component')
|
|
136
|
+
if (options.lazy && !script) {
|
|
137
|
+
script = pick('build:lib', 'lib')
|
|
138
|
+
env.LAZY_LIB = 'true'
|
|
139
|
+
}
|
|
140
|
+
if (!script && !options.lazy && /--target(?:=|\s+)lib\b/.test(scripts.build || '')) script = 'build'
|
|
141
|
+
if (!script) throw new Error('当前工程缺少库构建脚本。产品请提供 build:lib(按需库可用 build:lib:lazy),平台使用 build:component。')
|
|
142
|
+
if (script === 'build:component' && options.mode) throw new Error('平台聚合库包含多步构建,请在 build:component 中配置环境;epx lib 不向该聚合脚本追加 --mode。')
|
|
143
|
+
} else if (command === 'preview') {
|
|
144
|
+
script = pick('preview', 'vite:preview')
|
|
145
|
+
if (!script) binary = 'vite'
|
|
146
|
+
} else if (command === 'remote') script = pick('build:remote')
|
|
147
|
+
else if (command === 'pkg') {
|
|
148
|
+
const name = options.positional[0]
|
|
149
|
+
if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('用法:epx pkg <包名|all> [--dir <平台目录>]')
|
|
150
|
+
script = name === 'all' ? pick('build:pkgs') : pick('build:pkg')
|
|
151
|
+
if (name !== 'all') env.PKG = name
|
|
152
|
+
} else if (command === 'lint') {
|
|
153
|
+
script = options.fix ? pick('lint:fix', 'lint') : pick('lint')
|
|
154
|
+
if (options.fix && script !== 'lint:fix') args.push('--fix')
|
|
155
|
+
} else if (command === 'circular') script = pick('circular')
|
|
156
|
+
if (!script && !binary) throw new Error(`当前工程未提供 ${command} 对应的 package.json 脚本`)
|
|
157
|
+
if (options.mode) args.push('--mode', options.mode)
|
|
158
|
+
if (options.port) args.push('--port', options.port)
|
|
159
|
+
args.push(...options.forward)
|
|
160
|
+
if (['build', 'lib', 'remote', 'pkg'].includes(command) && !process.env.NODE_OPTIONS) env.NODE_OPTIONS = '--max-old-space-size=8192'
|
|
161
|
+
return { command, script, binary, args, env }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function runProject (command, argv, cwd) {
|
|
165
|
+
const options = parseProjectArgs(argv)
|
|
166
|
+
const project = findProject(options.dir ? path.resolve(cwd, options.dir) : cwd)
|
|
167
|
+
const plan = projectPlan(command, options, project)
|
|
168
|
+
console.log(`项目:${project.pkg.name || project.dir}(${project.dir})`)
|
|
169
|
+
if (plan.script) {
|
|
170
|
+
console.log(`→ npm run ${plan.script}${plan.args.length ? ' -- ' + plan.args.join(' ') : ''}`)
|
|
171
|
+
return execute(npmEntry(), ['run', plan.script, ...(plan.args.length ? ['--', ...plan.args] : [])], project.dir, plan.env)
|
|
172
|
+
}
|
|
173
|
+
const file = localBinary(project, plan.binary)
|
|
174
|
+
if (!file) throw new Error(`当前工程没有可用脚本或本地 ${plan.binary},请先在工程内安装依赖并配置相应脚本。`)
|
|
175
|
+
const args = [...(command === 'dev' ? [] : [command]), ...plan.args]
|
|
176
|
+
console.log(`→ 本地 ${plan.binary} ${args.join(' ')}`)
|
|
177
|
+
return execute(file, args, project.dir, plan.env)
|
|
178
|
+
}
|
package/dist/prompts.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import readline from 'node:readline'
|
|
2
|
+
|
|
3
|
+
export class Cancelled extends Error {
|
|
4
|
+
constructor () { super('已取消'); this.exitCode = 130 }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// 非 TTY 共用一个输入队列,避免多步问答丢掉管道中提前到达的行。
|
|
8
|
+
export class Prompts {
|
|
9
|
+
constructor (input = process.stdin, output = process.stdout) {
|
|
10
|
+
this.input = input
|
|
11
|
+
this.output = output
|
|
12
|
+
this.tty = !!(input.isTTY && output.isTTY && typeof input.setRawMode === 'function')
|
|
13
|
+
this.lines = []
|
|
14
|
+
this.closed = false
|
|
15
|
+
if (!this.tty) {
|
|
16
|
+
this.rl = readline.createInterface({ input, terminal: false })
|
|
17
|
+
this.rl.on('line', line => {
|
|
18
|
+
if (this.pending) { const pending = this.pending; this.pending = null; pending.resolve(line) } else this.lines.push(line)
|
|
19
|
+
})
|
|
20
|
+
this.rl.on('close', () => { this.closed = true; if (this.pending) { this.pending.reject(new Cancelled()); this.pending = null } })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
close () { this.rl?.close() }
|
|
25
|
+
|
|
26
|
+
line () {
|
|
27
|
+
if (this.lines.length) return Promise.resolve(this.lines.shift())
|
|
28
|
+
if (this.closed) return Promise.reject(new Cancelled())
|
|
29
|
+
return new Promise((resolve, reject) => { this.pending = { resolve, reject } })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async text (question, defaultValue = '') {
|
|
33
|
+
const prompt = question + (defaultValue ? `(回车用 ${defaultValue})` : '') + ':'
|
|
34
|
+
let answer
|
|
35
|
+
if (!this.tty) { this.output.write(prompt); answer = await this.line() } else {
|
|
36
|
+
answer = await new Promise((resolve, reject) => {
|
|
37
|
+
let settled = false
|
|
38
|
+
const rl = readline.createInterface({ input: this.input, output: this.output })
|
|
39
|
+
rl.once('close', () => { if (!settled) reject(new Cancelled()) })
|
|
40
|
+
rl.once('SIGINT', () => { settled = true; rl.close(); reject(new Cancelled()) })
|
|
41
|
+
rl.question(prompt, value => { settled = true; rl.close(); resolve(value) })
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return answer.trim() || defaultValue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async required (question, defaultValue = '') {
|
|
48
|
+
while (true) { const value = await this.text(question, defaultValue); if (value) return value; this.output.write('此项不能为空。\n') }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async select (question, choices) {
|
|
52
|
+
if (!this.tty) {
|
|
53
|
+
this.output.write(question + '\n' + choices.map((choice, index) => ` ${index + 1}) ${choice.label}`).join('\n') + '\n')
|
|
54
|
+
while (true) {
|
|
55
|
+
const answer = await this.text('输入序号', '1')
|
|
56
|
+
if (/^\d+$/.test(answer) && choices[+answer - 1]) return choices[+answer - 1].value
|
|
57
|
+
this.output.write('请选择有效序号。\n')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
let index = 0
|
|
62
|
+
let drawn = false
|
|
63
|
+
const draw = () => {
|
|
64
|
+
if (drawn) this.output.write(`\x1b[${choices.length + 1}A\x1b[0J`)
|
|
65
|
+
this.output.write(question + '(↑↓ 选择,Enter 确认,Esc 取消)\n')
|
|
66
|
+
choices.forEach((choice, i) => this.output.write(i === index ? `\x1b[36m❯ ${choice.label}\x1b[0m\n` : ` ${choice.label}\n`))
|
|
67
|
+
drawn = true
|
|
68
|
+
}
|
|
69
|
+
const cleanup = () => { this.input.removeListener('keypress', onKey); this.input.setRawMode(false); this.input.pause() }
|
|
70
|
+
const onKey = (text, key) => {
|
|
71
|
+
if (!key) return
|
|
72
|
+
if (key.name === 'up' || key.name === 'k') { index = (index - 1 + choices.length) % choices.length; draw() } else if (key.name === 'down' || key.name === 'j') { index = (index + 1) % choices.length; draw() } else if (key.name === 'return' || key.name === 'enter') { cleanup(); resolve(choices[index].value) } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { cleanup(); this.output.write('\n'); reject(new Cancelled()) }
|
|
73
|
+
}
|
|
74
|
+
readline.emitKeypressEvents(this.input)
|
|
75
|
+
this.input.setRawMode(true)
|
|
76
|
+
this.input.resume()
|
|
77
|
+
this.input.on('keypress', onKey)
|
|
78
|
+
draw()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
confirm (question, defaultValue = false) {
|
|
83
|
+
const choices = [{ label: '否', value: false }, { label: '是', value: true }]
|
|
84
|
+
return this.select(question, defaultValue ? choices.reverse() : choices)
|
|
85
|
+
}
|
|
86
|
+
}
|