@epxing/cli 0.1.0 → 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 +35 -4
- package/dist/cli.mjs +121 -5
- package/dist/project.mjs +178 -0
- package/dist/prompts.mjs +86 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# @epxing/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
独立的工程开发工具,包含交互菜单、开发服务、工程与产品库构建、创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖。启动、构建调用当前工程的脚本及本地依赖;迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
|
-
npm install -g @epxing/cli@0.
|
|
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 # 产品按需库(工程需支持此构建形态)
|
|
7
12
|
epx create my-product --epx-frame <目标平台版本>
|
|
8
13
|
epx migrate E:/products/old-product --epx-frame <目标平台版本>
|
|
9
14
|
epx migrate E:/products/old-product --epx-frame <目标平台版本> --apply
|
|
@@ -12,7 +17,33 @@ epx skill install --dir E:/products/my-product
|
|
|
12
17
|
|
|
13
18
|
如已全局安装旧的 `epx-frame` CLI,先用 `npm uninstall -g epx-frame` 释放 `epx` 命令。产品工程里的 `epx-frame` 依赖保留。工具不在安装阶段运行迁移、下载平台或改产品文件;产品仍需安装目标框架依赖才能开发和构建。
|
|
14
19
|
|
|
15
|
-
|
|
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。标识表示迁移目标,仍需完成转换、构建和业务复测。
|
|
16
47
|
|
|
17
48
|
`epx scan <目录>` 扫描静态风险,`epx verify <目录> --online <地址|文件>` 对照组件清单。技能安装到 `.claude/skills/epxing-frontend-vue3-migrate/`,可用 `--skills-dir` 指定其他客户端要求的位置。已有技能默认不覆盖;确认替换使用 `--force`。平台基类源码分析仍需准备对应版本源码或文档。
|
|
18
49
|
|
|
@@ -21,7 +52,7 @@ epx skill install --dir E:/products/my-product
|
|
|
21
52
|
```powershell
|
|
22
53
|
cd frontend/frame/packages/cli
|
|
23
54
|
npm login --registry https://registry.npmjs.org/
|
|
24
|
-
#
|
|
55
|
+
# 发布前调整为未发布过的新版本
|
|
25
56
|
# npm version <新的CLI版本> --no-git-tag-version
|
|
26
57
|
$previousPublicPublish = $env:EPX_PUBLISH_PUBLIC
|
|
27
58
|
try {
|
package/dist/cli.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import fs from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { findProject, runProject } from './project.mjs'
|
|
8
|
+
import { Prompts } from './prompts.mjs'
|
|
7
9
|
|
|
8
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
11
|
const payload = path.join(packageRoot, 'dist')
|
|
@@ -56,18 +58,132 @@ function installSkill (args, cwd) {
|
|
|
56
58
|
return 0
|
|
57
59
|
}
|
|
58
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
|
+
|
|
59
155
|
export async function main (argv = process.argv.slice(2), cwd = process.env.INIT_CWD || process.cwd()) {
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
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 · 工程开发与升级工具(独立于平台版本)
|
|
63
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 <目录>]
|
|
64
173
|
epx create <名称|路径> --epx-frame <目标版本> [--dir <目录>]
|
|
65
174
|
epx migrate [项目目录] --epx-frame <目标版本> [--apply] [--transform-vue] [--to-vite]
|
|
66
175
|
epx scan [项目目录]
|
|
67
176
|
epx verify [项目目录] [--online <线上JS地址|文件>]
|
|
68
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 <平台目录>]
|
|
69
182
|
epx --version
|
|
70
183
|
|
|
184
|
+
dev / build / lib 等执行当前工程的 package.json 脚本,使用工程本地依赖。
|
|
185
|
+
工程命令可用 -- 后的参数透传给脚本;serve / upgrade 分别是 dev / migrate 的别名。
|
|
186
|
+
create、migrate 不带参数时进入引导;直接带参数时 --epx-frame 必填。
|
|
71
187
|
创建和升级自动写入 runtime: "vue3"。迁移默认 dry-run,--apply 才落盘。
|
|
72
188
|
工具不会自动安装平台依赖;.vue 转换工具仅在执行 --apply --transform-vue 时下载并缓存。`)
|
|
73
189
|
return 0
|
|
@@ -104,11 +220,11 @@ export async function main (argv = process.argv.slice(2), cwd = process.env.INIT
|
|
|
104
220
|
const script = command === 'scan' ? 'detect-vue3-risks.mjs' : 'verify-components.mjs'
|
|
105
221
|
return runTool('epx-migrate/' + script, [path.resolve(cwd, args[0] || '.'), ...(online ? ['--online', online] : [])], cwd)
|
|
106
222
|
}
|
|
107
|
-
throw new Error(`未知命令:${command},运行 epx help
|
|
223
|
+
throw new Error(`未知命令:${command},运行 epx help 查看用法。`)
|
|
108
224
|
}
|
|
109
225
|
|
|
110
226
|
let isMain = false
|
|
111
227
|
try { isMain = !!process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url) } catch (error) { /* 作为模块导入 */ }
|
|
112
228
|
if (isMain) {
|
|
113
|
-
main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = 1 })
|
|
229
|
+
main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = error.exitCode || 1 })
|
|
114
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
|
+
}
|