@epxing/cli 0.1.0 → 0.3.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 CHANGED
@@ -1,9 +1,15 @@
1
1
  # @epxing/cli
2
2
 
3
- 独立的产品/项目开发工具,包含创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖,不包含 Vue、Element Plus、设计器或 `epx-frame`。迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
3
+ 独立的工程开发工具,包含交互菜单、开发服务、工程与产品库构建、创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖。启动、构建调用当前工程的脚本及本地依赖;迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
4
4
 
5
5
  ```powershell
6
- npm install -g @epxing/cli@0.1.0 --registry https://registry.npmjs.org/
6
+ npm install -g @epxing/cli@0.3.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 --full # 产品完整打包
12
+ epx lib --lazy # 显式选择懒加载,兼容已有命令
7
13
  epx create my-product --epx-frame <目标平台版本>
8
14
  epx migrate E:/products/old-product --epx-frame <目标平台版本>
9
15
  epx migrate E:/products/old-product --epx-frame <目标平台版本> --apply
@@ -12,7 +18,35 @@ epx skill install --dir E:/products/my-product
12
18
 
13
19
  如已全局安装旧的 `epx-frame` CLI,先用 `npm uninstall -g epx-frame` 释放 `epx` 命令。产品工程里的 `epx-frame` 依赖保留。工具不在安装阶段运行迁移、下载平台或改产品文件;产品仍需安装目标框架依赖才能开发和构建。
14
20
 
15
- `create` 和 `migrate --apply` 自动写入 `runtime: "vue3"`。迁移默认预览;apply 要求目标 Git 工作树干净。实际执行 `.vue` 转换时才按需安装并缓存 gogocode。标识表示迁移目标,仍需完成转换、构建和业务复测。
21
+ 使用 fnm/nvm 时,不同 Node 版本的全局包分别安装。切换到日常使用的 Node 版本后更新 CLI,再用 `epx help` 确认开头为 `@epxing/cli`、`epx --version` 确认版本。旧仓库的 `pnpm epx` 仍是该仓库自己的脚本。
22
+
23
+ ## 命令
24
+
25
+ | 命令 | 行为 |
26
+ | --- | --- |
27
+ | `epx` / `epx menu` | 交互选择操作、工程目录、环境、端口和构建方式;管道支持数字输入 |
28
+ | `epx dev` / `epx serve` | 使用工程 `dev`、`serve` 或 `start` 脚本启动服务 |
29
+ | `epx build` | 使用工程的构建脚本;只有 `build:lib` 的产品自动构建产品库 |
30
+ | `epx lib` / `epx build --lib` / `epx lib --lazy` | 产品默认懒加载,优先 `build:lib:lazy`,否则使用 `build:lib` / `lib`;平台聚合库仍完整构建 |
31
+ | `epx lib --full` / `epx build --lib --full` | 产品完整打包,明确设置 `LAZY_LIB=false`;兼容旧 `--target lib` 脚本 |
32
+ | `epx preview` | 使用 `preview` / `vite:preview`;没有脚本时使用工程本地 Vite |
33
+ | `epx create` | 无参数进入创建引导,也可用参数直接创建 |
34
+ | `epx migrate` / `epx upgrade` | 无参数引导:目标版本、迁移范围、预览、确认应用 |
35
+ | `epx skill install` | 安装技能,支持自定义目录和明确覆盖 |
36
+ | `epx scan` / `epx verify` | 迁移风险扫描 / 本地与线上组件清单校验 |
37
+ | `epx lint [--fix]` / `epx circular` | 执行工程已有的代码检查、循环依赖检查脚本 |
38
+ | `epx remote` / `epx pkg <包名\|all>` | 使用平台已有 `build:remote` / `build:pkg` / `build:pkgs` 脚本 |
39
+ | `epx help` / `epx --version` | 帮助 / 工具版本 |
40
+
41
+ 工程命令支持 `--dir <目录>`,默认从当前目录向上找到最近的 `package.json`;不会改为 CLI 全局安装目录。没有项目脚本时,dev/build/preview 可使用项目已安装的 Vite,不会自动下载构建工具。工程仍需先安装自身依赖。
42
+
43
+ 产品懒加载构建需要 Vite 库配置(`build.lib`)、Vue 3 编译器和入口中的 `import.meta.glob('./产品目录/**/*.vue', { eager: true })`。CLI 使用产品本地 Vite 加载原配置;未接入按需插件时通过临时配置补入共享插件,不修改产品源码或原配置。已有按需插件保留其 eager 等自定义设置。构建后验证本次生成的 `index.manifest.json`、组件 chunks 和 `index.full.js`,缺少或沿用旧产物会报错,避免完整包被误判为懒加载成功。部署时上传整个输出目录(包含 CSS、chunks 和兜底包)。`--full` 会关闭按需插件;`--full` 与 `--lazy` 互斥。旧 webpack 产品先迁移到 Vite,或使用 `--full` 完整打包。自定义非 Vite 脚本须自行生成标准 `lib/` 按需产物。直接执行 `npm run build:lib` 仍由产品原脚本决定构建方式。
44
+
45
+ `--mode/-m`、`--port/-p` 支持旧的 `-mprd`、`-p3000` 写法。build 优先匹配 `build:<mode>`;`localize` 也匹配 `build:local`,其余情况向构建脚本传入 `--mode`。平台 `build:component` 包含多步构建,不追加 `--mode`,环境由工程脚本统一配置。
46
+
47
+ 平台 `epx build` 保留 `--thin`、`--no-designer`、`--no-dashboard`;产品工程不接受平台裁剪选项。需向工程脚本透传其他参数时使用 `--`,如 `epx dev -- --host 0.0.0.0`。脚本原有的 pre/post 生命周期、自定义环境和多步构建会保留,失败退出码向上传递。
48
+
49
+ `create` 和 `migrate --apply` 自动写入 `runtime: "vue3"`。带参数直接调用时 `--epx-frame` 必填,交互引导也会要求输入目标平台。迁移默认预览;apply 要求目标 Git 工作树干净。实际执行 `.vue` 转换时才按需安装并缓存 gogocode。标识表示迁移目标,仍需完成转换、构建和业务复测。
16
50
 
17
51
  `epx scan <目录>` 扫描静态风险,`epx verify <目录> --online <地址|文件>` 对照组件清单。技能安装到 `.claude/skills/epxing-frontend-vue3-migrate/`,可用 `--skills-dir` 指定其他客户端要求的位置。已有技能默认不覆盖;确认替换使用 `--force`。平台基类源码分析仍需准备对应版本源码或文档。
18
52
 
@@ -21,7 +55,7 @@ epx skill install --dir E:/products/my-product
21
55
  ```powershell
22
56
  cd frontend/frame/packages/cli
23
57
  npm login --registry https://registry.npmjs.org/
24
- # 首次使用 0.1.0;后续发布前调整为未发布过的新版本
58
+ # 发布前调整为未发布过的新版本
25
59
  # npm version <新的CLI版本> --no-git-tag-version
26
60
  $previousPublicPublish = $env:EPX_PUBLISH_PUBLIC
27
61
  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,135 @@ 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 (!platformLib) {
148
+ const mode = await prompts.select('产品库构建形态', [{ label: '懒加载(默认)', value: 'lazy' }, { label: '完整打包', value: 'full' }])
149
+ if (mode === 'full') args.push('--full')
150
+ }
151
+ }
152
+ if (action === 'pkg') args.push(await prompts.required('包名或 all', 'all'))
153
+ if (action === 'lint' && await prompts.confirm('自动修复代码?')) args.push('--fix')
154
+ return dispatch(args)
155
+ } finally { prompts.close() }
156
+ }
157
+
59
158
  export async function main (argv = process.argv.slice(2), cwd = process.env.INIT_CWD || process.cwd()) {
60
- const [command, ...args] = argv
61
- if (!command || ['help', '--help', '-h'].includes(command)) {
62
- console.log(`@epxing/cli · 产品开发工具(独立于平台版本)
159
+ let [command, ...args] = argv
160
+ if (command === 'serve') command = 'dev'
161
+ if (command === 'upgrade') command = 'migrate'
162
+ const separator = args.indexOf('--')
163
+ if (args.slice(0, separator < 0 ? args.length : separator).some(arg => arg === '--help' || arg === '-h')) return main(['help'], cwd)
164
+ if (!command || command === 'menu') return interactive(cwd)
165
+ if (['create', 'migrate'].includes(command) && !args.length) return interactive(cwd, command)
166
+ if (['dev', 'build', 'lib', 'preview', 'remote', 'pkg', 'lint', 'circular'].includes(command)) return runProject(command, args, cwd)
167
+ if (['help', '--help', '-h'].includes(command)) {
168
+ console.log(`@epxing/cli · 工程开发与升级工具(独立于平台版本)
63
169
 
170
+ epx / epx menu 交互菜单(↑↓ + Enter;管道支持数字输入)
171
+ epx dev [-m <模式>] [-p <端口>] [--dir <工程目录>]
172
+ epx build [-m <模式>] [--thin] [--no-designer] [--no-dashboard] [--dir <目录>]
173
+ epx lib [--full | --lazy] [-m <模式>] [--dir <目录>] 产品默认懒加载,--full 完整打包;平台聚合库保持完整
174
+ epx build --lib 同 epx lib
175
+ epx preview [-p <端口>] [--dir <目录>]
64
176
  epx create <名称|路径> --epx-frame <目标版本> [--dir <目录>]
65
177
  epx migrate [项目目录] --epx-frame <目标版本> [--apply] [--transform-vue] [--to-vite]
66
178
  epx scan [项目目录]
67
179
  epx verify [项目目录] [--online <线上JS地址|文件>]
68
180
  epx skill install [--dir <项目目录>] [--skills-dir <技能目录>] [--force]
181
+ epx lint [--fix] [--dir <目录>]
182
+ epx circular [--dir <目录>]
183
+ epx remote [--dir <平台目录>]
184
+ epx pkg <包名|all> [--dir <平台目录>]
69
185
  epx --version
70
186
 
187
+ dev / build / lib 等执行当前工程的 package.json 脚本,使用工程本地依赖。
188
+ 工程命令可用 -- 后的参数透传给脚本;serve / upgrade 分别是 dev / migrate 的别名。
189
+ create、migrate 不带参数时进入引导;直接带参数时 --epx-frame 必填。
71
190
  创建和升级自动写入 runtime: "vue3"。迁移默认 dry-run,--apply 才落盘。
72
191
  工具不会自动安装平台依赖;.vue 转换工具仅在执行 --apply --transform-vue 时下载并缓存。`)
73
192
  return 0
@@ -104,11 +223,11 @@ export async function main (argv = process.argv.slice(2), cwd = process.env.INIT
104
223
  const script = command === 'scan' ? 'detect-vue3-risks.mjs' : 'verify-components.mjs'
105
224
  return runTool('epx-migrate/' + script, [path.resolve(cwd, args[0] || '.'), ...(online ? ['--online', online] : [])], cwd)
106
225
  }
107
- throw new Error(`未知命令:${command},运行 epx help 查看用法。平台构建命令仍在框架仓库使用 pnpm epx。`)
226
+ throw new Error(`未知命令:${command},运行 epx help 查看用法。`)
108
227
  }
109
228
 
110
229
  let isMain = false
111
230
  try { isMain = !!process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url) } catch (error) { /* 作为模块导入 */ }
112
231
  if (isMain) {
113
- main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = 1 })
232
+ main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = error.exitCode || 1 })
114
233
  }
@@ -0,0 +1,54 @@
1
+ import h from"node:fs";import y from"node:path";import{build as B}from"epxing:project-vite";import j from"node:fs";import v from"node:path";import{parse as V,babelParse as T}from"epxing:project-sfc";function W(e){return j.readFileSync(e,"utf8").split(`\r
2
+ `).join(`
3
+ `)}function K(e){let s=e.split(`\r
4
+ `).join(`
5
+ `),n="";try{let{descriptor:r}=V(s);n=r.script&&r.script.content||r.scriptSetup&&r.scriptSetup.content||""}catch{return null}if(!n)return null;try{let i=T(n,{sourceType:"module",plugins:["jsx","typescript","decorators-legacy"]}).program.body,t=l=>{if(!l||l.type!=="ObjectExpression")return null;for(let u of l.properties){if(u.type!=="ObjectProperty"||u.computed)continue;if((u.key.type==="Identifier"?u.key.name:u.key.type==="StringLiteral"?u.key.value:null)==="name"&&u.value.type==="StringLiteral")return u.value.value}return null},a=i.find(l=>l.type==="ExportDefaultDeclaration");if(!a)return null;let o=t(a.declaration);if(o)return o;if(a.declaration.type==="Identifier"){let l=a.declaration.name;for(let u of i)if(u.type==="VariableDeclaration"){for(let m of u.declarations)if(m.id.type==="Identifier"&&m.id.name===l){let b=t(m.init);if(b)return b}}}if(a.declaration.type==="CallExpression")for(let l of a.declaration.arguments){let u=t(l);if(u)return u}return null}catch{return null}}function U(e){return e.replace(/(\.\/|\.vue|\/)/g,"")}function O(e,s){let n=v.resolve(e,s),r=[],i=t=>{let a=j.readdirSync(t,{withFileTypes:!0}).sort((o,l)=>o.name<l.name?-1:1);for(let o of a){let l=v.join(t,o.name);if(o.isDirectory())i(l);else if(o.name.endsWith(".vue")){let m="./"+v.relative(e,l).split(v.sep).join("/");r.push({globKey:m,file:l,key:K(W(l))||U(m)})}}};return j.existsSync(n)&&i(n),r.sort((t,a)=>t.globKey<a.globKey?-1:1)}function z(e){let s=new Map;for(let n of e){if(s.has(n.key))throw new Error(`[lazy-lib] \u7EC4\u4EF6\u6CE8\u518C\u540D\u51B2\u7A81\uFF1A'${n.key}' \u540C\u65F6\u6765\u81EA
6
+ ${s.get(n.key)}
7
+ ${n.file}
8
+ \u5355\u4F53\u5F62\u6001\u4E0B\u662F\u540E\u8005\u9759\u9ED8\u8986\u76D6\u524D\u8005\uFF1B\u6309\u9700\u5F62\u6001\u4E0B\u4E24\u4E2A chunk \u62A2\u540C\u4E00\u4E2A\u540D\u5B57\uFF0C\u5FC5\u987B\u5148\u6539\u6389\u5176\u4E2D\u4E00\u4E2A\u7EC4\u4EF6\u7684 name\uFF08\u6216\u6587\u4EF6\u8DEF\u5F84\uFF09\u3002`);s.set(n.key,n.file)}return e}var G=[{test:e=>e==="vue",global:"window.frame && window.frame.Vue"},{test:e=>e==="vuex",global:"window.frame && window.frame.vuex"},{test:e=>e==="vue-router",global:"window.frame && window.frame.router"},{test:e=>e==="epx-frame"||e.startsWith("epx-frame/"),global:"window.epxFrame"}],L=/^[A-Za-z_$][\w$]*$/,R=(e,s)=>{if(!s)return null;let n=typeof s=="function"?s(e):s[e];return typeof n=="string"&&n?n:null};function q(e,s){let n=G.find(i=>i.test(e));if(n)return n.global;let r=R(e,s);if(r)return`window.${r}`;throw new Error(`[lazy-lib] external '${e}' \u6CA1\u6709\u5BF9\u5E94\u7684\u8FD0\u884C\u65F6\u5168\u5C40\u6620\u5C04\u3002
9
+ ESM chunk \u91CC\u7684\u88F8\u8BF4\u660E\u7B26\u6D4F\u89C8\u5668\u89E3\u6790\u4E0D\u4E86\uFF0C\u5FC5\u987B\u7ECF shim \u6865\u5230 window \u4E0A\u7684\u5168\u5C40\u3002
10
+ \u5E73\u53F0\u5185\u7F6E\u6620\u5C04\uFF1Avue \u2192 window.frame.Vue\uFF1Bvuex \u2192 window.frame.vuex\uFF1B
11
+ vue-router \u2192 window.frame.router\uFF1Bepx-frame\uFF08\u542B\u5B50\u8DEF\u5F84\uFF09\u2192 window.epxFrame\u3002
12
+ \u4E09\u6761\u51FA\u8DEF\uFF1A
13
+ \u2460 \u5B83\u7531**\u4EA7\u54C1\u9879\u76EE**\u77E5\u9053\u5168\u5C40\u540D \u2192 \u5728 vite.config.mjs \u7684 output.globals \u91CC\u7ED9 '${e}' \u914D\u4E0A\uFF08\u63A8\u8350\uFF09\uFF1B
14
+ \u2461 \u5B83\u7531**\u5E73\u53F0**\u63D0\u4F9B \u2192 \u53BB build/lazy-lib/gen-shims.mjs \u7684 GLOBALS \u8865\u4E00\u6761\u6620\u5C04\uFF1B
15
+ \u2462 \u5B83\u4E0D\u8BE5\u88AB external\uFF08\u7EC4\u4EF6\u771F\u7684\u9700\u8981\u628A\u5B83\u6253\u8FDB\u4EA7\u7269\uFF09\u2192 \u4ECE external \u5217\u8868\u91CC\u53BB\u6389\u5B83\u3002`)}function S(e){return"shims/"+e.replace(/[^\w.-]/g,"_")+".js"}function D(e){let s=new Map;for(let n of e){let r=S(n);if(s.has(r)&&s.get(r)!==n)throw new Error(`[lazy-lib] shim \u6587\u4EF6\u540D\u51B2\u7A81\uFF1A'${n}' \u548C '${s.get(r)}' \u90FD\u751F\u6210 '${r}'\u3002
16
+ \u5F53\u524D\u7F16\u7801\u65B9\u6848\uFF08/ \u548C\u975E\u6CD5\u5B57\u7B26\u66FF\u6362\u4E3A _\uFF09\u5728 GLOBALS \u7684\u5C01\u95ED\u96C6\u5185\u662F\u5B89\u5168\u7684\u3002
17
+ \u5C06\u6765\u82E5\u771F\u9700\u8981\u652F\u6301\u4F1A\u78B0\u649E\u7684\u8BF4\u660E\u7B26\uFF0C\u518D\u6362\u7F16\u7801\u65B9\u6848\uFF08\u5982 hash \u540E\u7F00\uFF09\u3002`);s.set(r,n)}}function F(e,s,n){let r=q(e,n),i=[...s],t=i.filter(o=>o!=="default"&&L.test(o)).sort();for(let o of i)if(o!=="default"&&!L.test(o))throw new Error(`[lazy-lib] \u65E0\u6CD5\u4E3A external '${e}' \u751F\u6210 shim\uFF1Aimport binding '${o}' \u4E0D\u662F\u5408\u6CD5 JS \u6807\u8BC6\u7B26\u3002
18
+ \u539F\u56E0\uFF1A'*' \u6765\u81EA import * as ns from '...' \u7C7B\u547D\u540D\u7A7A\u95F4\u5BFC\u5165\uFF1Bshim \u65E0\u6CD5\u9759\u6001\u679A\u4E3E\u4F7F\u7528\u65B9\u4F1A\u8BBF\u95EE\u54EA\u4E9B\u540D\u5B57\u3002
19
+ \u6539\u6CD5\uFF1A\u6539\u7528\u5177\u540D\u5BFC\u5165\uFF0C\u5982 import { someExport } from ... \u800C\u975E import * as ns from ...`);return[`// \u6784\u5EFA\u671F\u751F\u6210\uFF08epx-frame lazy-lib\uFF09\uFF1A\u8FD0\u884C\u65F6\u4ECE ${r} \u53D6\u5E73\u53F0\u5355\u4F8B\uFF0C\u52FF\u624B\u6539\u3002`,`const S = ${r}`,...t.map(o=>`export const ${o} = S[${JSON.stringify(o)}]`),"export default (S && S.default) || S"].join(`
20
+ `)+`
21
+ `}function N(e){let s=new Map;for(let n in e){let r=e[n];if(r.type==="chunk")for(let i in r.importedBindings||{}){s.has(i)||s.set(i,new Set);for(let t of r.importedBindings[i])s.get(i).add(t)}}return s}import k from"node:path";var Z=(e,s)=>{let n=k.resolve(e),r=k.resolve(s);return n===r||n.startsWith(r+k.sep)||n.startsWith(r+"/")},E=e=>k.resolve(String(e).split("?")[0]);function A(e,s,n,r){let i=new Set((r||[]).map(E)),t=[...e()].filter(d=>Z(d,n)),a=new Set(t),o=new Map,l=[],u=[],m=d=>{let g=new Set;for(let f of d){let c=E(f);i.has(c)&&g.add(c)}return g.size},b=d=>{let g=o.get(d)||0;if(g===2)return;if(g===1){let c=[...l.slice(l.indexOf(d)),d];m(c)>=2&&u.push(c);return}o.set(d,1),l.push(d);let f=s(d)||{};for(let c of f.importedIds||[])a.has(c)&&b(c);l.pop(),o.set(d,2)};for(let d of t)b(d);return u}var $="\0epx-lazy-glob",I=/import\.meta\.glob\(\s*(['"])([^'"]+)\1\s*,\s*\{\s*eager\s*:\s*true\s*\}\s*\)/;function X(e={}){let{enabled:s=!1,glob:n,eager:r=[],pkgName:i}=e,t=null,a=null,o=[],l=new Set,u="",m="",b="",d="",g=[];return{name:"epx-lazy-lib",apply:"build",enforce:"pre",config(f){return s?(g=(Array.isArray(f.plugins)?f.plugins.flat(1/0):[]).filter(c=>c&&c.name!=="epx-lazy-lib"),{build:{emptyOutDir:!1}}):null},configResolved(f){t=f,m=y.resolve(f.root,f.build.outDir);let c=f.build.lib&&f.build.lib.entry;b=Array.isArray(c)?c[0]:typeof c=="object"?Object.values(c)[0]:c,u=y.dirname(b),d=i||y.basename(f.root)},async buildStart(){if(!s)return;n||this.error("[lazy-lib] \u7F3A\u5C11 glob \u9009\u9879\uFF08\u5F62\u5982 './<\u4EA7\u54C1\u540D>/**/*.vue'\uFF09");let f=Y(t.root,m);f&&this.error(f);let c=n.replace(/^\.\//,"").split("/")[0];o=z(O(u,c)),o.length||this.error(`[lazy-lib] \u6309 glob \u4E00\u4E2A .vue \u90FD\u6CA1\u626B\u5230\uFF0C\u65E0\u6CD5\u6309\u9700\u62C6\u5305\u3002
22
+ glob \u9009\u9879\uFF1A${n}
23
+ \u5B9E\u9645\u626B\u63CF\u76EE\u5F55\uFF1A${y.join(u,c)}\uFF08= \u5165\u53E3\u6240\u5728\u76EE\u5F55 ${u} + glob \u7B2C\u4E00\u6BB5 ${c}\uFF09
24
+ \u6700\u53EF\u80FD\u7684\u539F\u56E0\uFF1Aglob \u662F\u76F8\u5BF9**\u5165\u53E3\u6587\u4EF6\u6240\u5728\u76EE\u5F55**\u5199\u7684\uFF0C\u522B\u6309\u4ED3\u5E93\u6839\u591A\u5199\u4E00\u5C42 \`src/\`\u2014\u2014
25
+ \u5165\u53E3\u662F src/index.js \u65F6\u5E94\u5199 './<\u4EA7\u54C1\u540D>/**/*.vue'\uFF0C\u4E0D\u662F './src/<\u4EA7\u54C1\u540D>/**/*.vue'\u3002`),l=new Set([...r,...Q(b,u,o)]),h.rmSync(m,{recursive:!0,force:!0}),h.mkdirSync(m,{recursive:!0});let x=o.filter(p=>!l.has(p.key));a=await te(t,g,x,m,u,o)},resolveId(f){return f===$?$:null},load(f){return f!==$?null:re({pkgName:d,entries:o,eagerKeys:l,manifest:a})},transform(f,c){if(!s||y.resolve(c.split("?")[0])!==y.resolve(b))return null;let x=I.exec(f);return x||this.error(`[lazy-lib] \u5165\u53E3\u91CC\u6CA1\u627E\u5230\u671F\u671B\u5F62\u6001\u7684 import.meta.glob\u3002
26
+ \u671F\u671B\uFF1Aconst files = import.meta.glob('./<\u4EA7\u54C1\u540D>/**/*.vue', { eager: true })
27
+ \u6587\u4EF6\uFF1A${b}
28
+ \uFF08\u4E0D\u505A\u9759\u9ED8\u964D\u7EA7\uFF1A\u90A3\u4F1A\u6084\u6084\u6253\u51FA\u4E00\u4E2A\u5355\u4F53\uFF0C\u800C\u4F60\u4EE5\u4E3A\u81EA\u5DF1\u5F00\u4E86\u6309\u9700\u3002\uFF09`),x[2]!==n&&this.error(`[lazy-lib] \u63D2\u4EF6\u7684 glob \u9009\u9879\u4E0E\u5165\u53E3\u91CC import.meta.glob \u7684\u5B57\u9762\u91CF\u4E0D\u4E00\u81F4\uFF0C\u4E24\u8FB9\u5FC5\u987B\u9010\u5B57\u76F8\u540C\u3002
29
+ \u63D2\u4EF6\u9009\u9879\uFF1A${n}
30
+ \u5165\u53E3\u5B57\u9762\u91CF\uFF1A${x[2]}
31
+ \u6587\u4EF6\uFF1A${b}
32
+ \uFF08\u4E0D\u4E00\u81F4\u65F6\u58F3\u7684\u952E\u96C6\u4F1A\u4E0E\u6574\u4F53\u5355\u4F53\u5206\u53C9\uFF0C\u964D\u7EA7\u515C\u5E95\u4F1A\u627E\u4E0D\u5230\u7EC4\u4EF6\u3002\uFF09`),{code:`import __epxLazyGlob from ${JSON.stringify($)}
33
+ `+f.replace(I,"__epxLazyGlob"),map:null}},async closeBundle(){if(!s||!a)return;let f={};for(let c of o)f[c.key]=l.has(c.key)?null:a[c.key]||null;h.writeFileSync(y.join(m,"index.manifest.json"),JSON.stringify({v:1,pkgName:d,components:f,eager:[...l].sort()},null,2)+`
34
+ `),ee(m),M(m,t.build.copyPublicDir===!1?"":t.publicDir||""),await ne(t,g,b,m,d),J(m,"index.deps.json"),H(m,t.build.assetsDir)}}}function Y(e,s){let n=y.resolve(e),r=y.resolve(s);return r===n||!r.startsWith(n+y.sep)?`[lazy-lib] build.outDir \u5FC5\u987B\u662F\u9879\u76EE\u6839**\u4E4B\u5185**\u7684\u5B50\u76EE\u5F55\uFF0C\u4E14\u4E0D\u80FD\u5C31\u662F\u9879\u76EE\u6839\u672C\u8EAB\u3002
35
+ \u9879\u76EE\u6839\uFF1A${n}
36
+ outDir\uFF1A${r}
37
+ \u539F\u56E0\uFF1A\u6309\u9700\u62C6\u5305\u7684\u4E09\u8D9F\u4EA7\u7269\u8981\u843D\u8FDB\u540C\u4E00\u4E2A\u76EE\u5F55\uFF0C\u672C\u63D2\u4EF6\u4F1A\u5728\u6784\u5EFA\u5F00\u59CB\u65F6\u628A outDir \u6574\u4E2A\u5220\u6389\u91CD\u5EFA
38
+ \uFF08\u9012\u5F52\u3001\u5F3A\u5236\uFF09\uFF0C\u8FD9\u4E00\u6B65\u7ED5\u5F00\u4E86 vite \u81EA\u5DF1\u7684 emptyOutDir \u4FDD\u62A4\u3002outDir \u89E3\u6790\u6210\u9879\u76EE\u6839\u65F6\uFF0C
39
+ \u8FD9\u4E00\u5220\u4F1A\u628A\u4EA7\u54C1\u7684\u6E90\u7801\u4E00\u8D77\u5220\u6389\u3002\u6539\u6CD5\uFF1A\u7ED9 build.outDir \u4E00\u4E2A\u771F\u6B63\u7684\u5B50\u76EE\u5F55\uFF0C\u5982 'lib'\u3002`:null}function M(e,s,n=""){for(let r of h.readdirSync(e,{withFileTypes:!0})){let i=y.join(e,r.name),t=n?n+"/"+r.name:r.name;if(r.isDirectory())M(i,s,t);else if(r.name.endsWith(".css")){if(s&&h.existsSync(y.join(s,t)))continue;h.rmSync(i,{force:!0})}}}function H(e,s){let n=y.join(e,s||"assets");if(!h.existsSync(n))return[];let r=h.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isFile()).map(t=>t.name);if(!r.length){try{h.rmdirSync(n)}catch{}return[]}let i=s||"assets";return console.warn(["",`[lazy-lib] \u26A0\uFE0F \u4EA7\u51FA\u4E86 ${r.length} \u4E2A\u6587\u4EF6\u578B\u8D44\u6E90\u5230 ${i}/\uFF1A`,...r.map(t=>" "+t),` \u8FDC\u7A0B\u5E93\u5F62\u6001\u4E0B\u5B83\u4EEC\u4F1A\u88AB\u5F15\u7528\u4E3A**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08/${i}/xxx\uFF09\uFF0C`," \u89E3\u6790\u5230\u5E73\u53F0\u9875\u9762\u7684 origin \u6839\u76EE\u5F55 \u2192 \u8FD0\u884C\u65F6 404\uFF0C\u6784\u5EFA\u671F\u770B\u4E0D\u51FA\u6765\u3002"," \u4FEE\u6CD5\uFF1A\u628A build.assetsInlineLimit \u8C03\u5927\u5230\u8986\u76D6\u5B83\u4EEC\uFF08\u4EA7\u54C1\u9879\u76EE\u666E\u904D\u8BBE 50*1024\uFF09\uFF0C"," \u6216\u628A\u8D85\u5927\u8D44\u6E90\u5355\u72EC\u4F20\u6587\u4EF6\u670D\u52A1\u5668\u3001\u4EE3\u7801\u91CC\u5199\u5B8C\u6574 URL\u3002",""].join(`
40
+ `)),r}var C=e=>({assetsInlineLimit:e.assetsInlineLimit,assetsDir:e.assetsDir,target:e.target,cssTarget:e.cssTarget,sourcemap:e.sourcemap,minify:e.minify});function J(e,s,n=""){for(let r of h.readdirSync(e,{withFileTypes:!0})){let i=y.join(e,r.name),t=n?n+"/"+r.name:r.name;r.isDirectory()?J(i,s,t):r.name.endsWith(".deps.json")&&t!==s&&h.rmSync(i,{force:!0})}}function Q(e,s,n){let r=h.readFileSync(e,"utf8").split(`\r
41
+ `).join(`
42
+ `),i=[],t=/^\s*import\s+\w+\s+from\s+['"](\.[^'"]+\.vue)['"]/gm,a;for(;a=t.exec(r);){let o=y.resolve(y.dirname(e),a[1]),l=n.find(u=>y.resolve(u.file)===o);l&&i.push(l.key)}return i}function ee(e){let s=y.join(e,"index.deps.json"),n=y.join(e,"index.js");if(!h.existsSync(s)||!h.existsSync(n))return;let r=[],i=!1;try{let a=JSON.parse(h.readFileSync(s,"utf8"));r=Array.isArray(a.components)?a.components:[],i=!!a.all}catch{return}let t=`
43
+ /*__EPX_FRAME_DEPS__${JSON.stringify(r)}__*/
44
+ `+(i?`/*__EPX_FRAME_DEPS_ALL__*/
45
+ `:"");h.appendFileSync(n,t)}async function te(e,s,n,r,i,t){let a={};for(let p of n)a[p.key]=p.file;let o=new Set,l=(t||n).map(p=>p.file),u=await B({root:e.root,logLevel:"silent",configFile:!1,plugins:[...s,{name:"epx-lazy-lib-cycle-guard",buildEnd(){let p=A(()=>this.getModuleIds(),_=>this.getModuleInfo(_),i,l);p.length&&this.error(`[lazy-lib] \u5165\u53E3\u7EC4\u4EF6\u4E4B\u95F4\u5B58\u5728\u5FAA\u73AF\u4F9D\u8D56\uFF0C\u6309\u9700\u62C6\u5305\u4F1A\u5728\u8FD0\u884C\u65F6\u89E6\u53D1 TDZ\uFF08Cannot access ... before initialization\uFF09\u5E76\u767D\u5C4F\uFF1A
46
+ `+p.map(_=>" "+_.join(`
47
+ -> `)).join(`
48
+ `)+`
49
+ \u4E00\u4E2A .vue = \u4E00\u4E2A chunk\uFF0C\u6210\u73AF\u7684\u4E24\u4E2A\u7EC4\u4EF6\u8DE8 chunk \u540E\u521D\u59CB\u5316\u987A\u5E8F\u65AD\u88C2\u3002\u4E24\u6761\u51FA\u8DEF\uFF1A
50
+ \u2460 \u628A\u73AF\u62C6\u6389\uFF1A\u62BD\u51FA\u53CC\u65B9\u90FD\u8981\u7684\u90A3\u90E8\u5206\u5230\u72EC\u7ACB\u6A21\u5757\uFF0C\u6539\u6210\u4E24\u4E2A\u7EC4\u4EF6\u90FD import \u5B83\uFF1B
51
+ \u2461 \u8FD9\u4E2A\u5E93\u6682\u65F6\u4E0D\u7528\u6309\u9700\u5F62\u6001\uFF0C\u56DE\u5230\u6574\u4F53\u5355\u4F53\uFF08\u4E0D\u8BBE LAZY_LIB / enabled:false\uFF09\u3002
52
+ \uFF08eager \u9009\u9879\u5E2E\u4E0D\u4E0A\u5FD9\uFF1A\u5B83\u53EA\u628A .vue \u4ECE\u7B2C\u2460\u8D9F\u7684 input \u91CC\u62FF\u6389\uFF0C\u7EC4\u4EF6\u4ECD\u5728\u6A21\u5757\u56FE\u91CC\uFF0C\u73AF\u8FD8\u5728\u3002\uFF09`)}}],css:e.css,resolve:e.resolve,define:e.define,publicDir:!1,build:{outDir:r,emptyOutDir:!1,cssCodeSplit:!1,...C(e.build),rollupOptions:{input:a,external:e.build.rollupOptions.external,preserveEntrySignatures:"strict",output:{format:"es",entryFileNames:"chunks/[name]-[hash].js",chunkFileNames:"chunks/[name]-[hash].js",paths:p=>(o.add(p),"./"+S(p))}}}}),m=Array.isArray(u)?u[0]:u,b=m.output.reduce((p,_)=>(p[_.fileName]=_,p),{}),d=e.build.rollupOptions.output&&e.build.rollupOptions.output.globals,g=N(b),f=new Map([...o].map(p=>["./"+S(p),p])),c=new Map;for(let[p,_]of g){let w=f.get(p);if(w){c.has(w)||c.set(w,new Set);for(let P of _)c.get(w).add(P)}}D([...o]);for(let[p,_]of c){let w=y.join(r,"chunks",S(p));h.mkdirSync(y.dirname(w),{recursive:!0}),h.writeFileSync(w,F(p,_,d))}let x={};for(let p of m.output)p.type==="chunk"&&p.isEntry&&(x[p.name]=p.fileName);return x}async function ne(e,s,n,r,i){await B({root:e.root,logLevel:"silent",configFile:!1,plugins:s,css:e.css,resolve:e.resolve,define:e.define,publicDir:!1,build:{outDir:r,emptyOutDir:!1,cssCodeSplit:!1,...C(e.build),lib:{entry:n,name:`__epxFullLibs__.${i}`,formats:["umd"],fileName:()=>"index.full.js"},rollupOptions:{external:e.build.rollupOptions.external,output:{globals:e.build.rollupOptions.output&&e.build.rollupOptions.output.globals,exports:"default",assetFileNames:t=>(t.names||[t.name]).some(a=>String(a).endsWith(".css"))?"index.css":"assets/[name]-[hash][extname]"}}}})}function re({pkgName:e,entries:s,eagerKeys:n,manifest:r}){let i=s.filter(o=>n.has(o.key)),t=[];i.forEach((o,l)=>{t.push(`import __eager${l} from ${JSON.stringify(o.file.split(y.sep).join("/"))}`)});let a={};for(let o of s)n.has(o.key)||(a[o.key]=r[o.key]);return t.push(""),t.push("// \u6784\u5EFA\u671F\u751F\u6210\uFF08epx-frame lazy-lib\uFF09\uFF1A\u628A import.meta.glob \u7684 eager \u7ED3\u679C\u6362\u6210\u6309\u9700 wrapper\u3002"),t.push(`const __pkg = ${JSON.stringify(e)}`),t.push(`const __manifest = ${JSON.stringify(a,null,2)}`),t.push("const __frame = (typeof window !== 'undefined' && window.frame) || {}"),t.push("const __cur = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || ''"),t.push("const __base = __cur ? __cur.slice(0, __cur.lastIndexOf('/') + 1) : ''"),t.push("let __lazy = {}"),t.push("if (typeof __frame.registerLazyLibrary === 'function') {"),t.push(" __lazy = __frame.registerLazyLibrary(__pkg, { base: __base, components: __manifest, full: 'index.full.js' })"),t.push("} else if (__frame.Vue && __frame.Vue.defineAsyncComponent) {"),t.push(" // \u964D\u7EA7\u2461\uFF1A\u5E73\u53F0\u6CA1\u6709\u8BE5 API\uFF08\u65E7\u7248\u5E73\u53F0\uFF09\u65F6\u58F3\u81EA\u5EFA wrapper\uFF0C\u529F\u80FD\u7B49\u4EF7\uFF0C\u65E0\u96C6\u4E2D\u9884\u53D6\u3002"),t.push(" for (const k in __manifest) {"),t.push(" const f = __manifest[k]"),t.push(" const w = __frame.Vue.defineAsyncComponent(() => import(/* @vite-ignore */ __base + f))"),t.push(" w.name = k"),t.push(" __lazy[k] = w"),t.push(" }"),t.push("} else {"),t.push(" console.error('[frame.lazy] \u5E73\u53F0\u65E2\u6CA1\u6709 registerLazyLibrary \u4E5F\u6CA1\u6709 Vue \u2014\u2014 \u8BF7\u5347\u7EA7\u5E73\u53F0\uFF0C\u6216\u6539\u7528\u6574\u4F53\u5355\u4F53\u5F62\u6001\u6784\u5EFA\u672C\u5E93', __pkg)"),t.push("}"),t.push("const __out = {}"),s.forEach(o=>{let l=i.indexOf(o),u=l>=0?`__eager${l}`:`__lazy[${JSON.stringify(o.key)}]`;t.push(`__out[${JSON.stringify(o.globKey)}] = { default: ${u} }`)}),t.push("export default __out"),t.join(`
53
+ `)+`
54
+ `}export{Y as checkOutDir,X as default,M as rmCssFiles,J as rmStrayDepsJson,H as warnEmittedAssets,ee as writeFrameDepsMarker};
@@ -0,0 +1,115 @@
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 { fileURLToPath, pathToFileURL } from 'node:url'
6
+ import { manifestStamp } from './lib-vite-config.mjs'
7
+
8
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ function compilerEntry (require) {
11
+ const contexts = [require]
12
+ for (const name of ['@vitejs/plugin-vue', 'vue', 'epx-frame']) {
13
+ try { contexts.push(createRequire(require.resolve(name))) } catch (error) { /* 尝试其他项目依赖 */ }
14
+ }
15
+ for (const resolve of contexts) {
16
+ for (const name of ['@vue/compiler-sfc', 'vue/compiler-sfc']) {
17
+ try { return resolve.resolve(name) } catch (error) { /* 继续查找 */ }
18
+ }
19
+ }
20
+ throw new Error('按需构建需要产品本地的 Vue 3 SFC 编译器,请先安装产品依赖。')
21
+ }
22
+
23
+ function configArgument (args) {
24
+ let value
25
+ for (let i = 0; i < args.length; i++) {
26
+ if (args[i] === '--config' || args[i] === '-c') value = args[++i]
27
+ else if (args[i].startsWith('--config=')) value = args[i].slice('--config='.length)
28
+ }
29
+ return value
30
+ }
31
+
32
+ function configInScript (script) {
33
+ const matches = [...String(script).matchAll(/(?:^|\s)(?:--config|-c)(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s]+))/g)]
34
+ const last = matches.at(-1)
35
+ return last && (last[1] || last[2] || last[3])
36
+ }
37
+
38
+ function verifyLazy (outDir, previousStamp) {
39
+ const file = path.join(outDir, 'index.manifest.json')
40
+ if (!fs.existsSync(file) || manifestStamp(outDir) === previousStamp || !fs.existsSync(path.join(outDir, 'index.full.js'))) {
41
+ throw new Error('没有生成新的按需库清单和完整兜底包,不能视为懒加载构建成功。请检查构建脚本或使用 epx lib --full。')
42
+ }
43
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'))
44
+ const entries = Object.entries(manifest.components || {})
45
+ if (!entries.length) throw new Error('按需库组件清单为空')
46
+ for (const [name, chunk] of entries) {
47
+ if (chunk === null && manifest.eager?.includes(name)) continue
48
+ if (typeof chunk !== 'string') throw new Error('按需组件没有对应的 chunk:' + name)
49
+ const resolved = path.resolve(outDir, chunk)
50
+ const relative = path.relative(outDir, resolved)
51
+ if (relative.startsWith('..') || path.isAbsolute(relative) || !fs.existsSync(resolved)) throw new Error('按需组件 chunk 不存在或路径无效:' + name)
52
+ }
53
+ console.log('✓ 按需库已验证:组件清单、组件 chunks、index.full.js;部署时请上传整个输出目录。')
54
+ }
55
+
56
+ export function prepareLibraryBuild (project, plan) {
57
+ if (!plan.libraryMode) return { args: plan.args, verify () {}, cleanup () {} }
58
+ const lazy = plan.libraryMode === 'lazy'
59
+ console.log('产品库构建形态:' + (lazy ? '懒加载(默认)' : '完整(--full)'))
60
+ const require = createRequire(path.join(project.dir, 'package.json'))
61
+ let viteAvailable = false
62
+ try { require.resolve('vite/package.json'); viteAvailable = true } catch (error) { /* 兼容自定义非 Vite 构建脚本 */ }
63
+ const explicitConfig = configArgument(plan.args) || configInScript(project.scripts[plan.script])
64
+ const configFile = explicitConfig
65
+ ? path.resolve(project.dir, explicitConfig)
66
+ : ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'].map(ext => path.join(project.dir, 'vite.config.' + ext)).find(file => fs.existsSync(file))
67
+ if (explicitConfig && !fs.existsSync(configFile)) throw new Error('Vite 配置不存在:' + configFile)
68
+ if (!viteAvailable || !configFile) {
69
+ const outDir = path.join(project.dir, 'lib')
70
+ const previousStamp = manifestStamp(outDir)
71
+ return { args: plan.args, verify () { if (lazy) verifyLazy(outDir, previousStamp) }, cleanup () {} }
72
+ }
73
+ const cache = path.resolve(project.dir, 'node_modules/.cache/epxing-cli')
74
+ fs.mkdirSync(cache, { recursive: true })
75
+ const dir = fs.mkdtempSync(path.join(cache, 'lib-'))
76
+ const cleanup = () => {
77
+ const resolved = path.resolve(dir)
78
+ if (path.dirname(resolved) !== cache || !path.basename(resolved).startsWith('lib-')) throw new Error('Unexpected library build cache directory')
79
+ fs.rmSync(resolved, { recursive: true, force: true })
80
+ }
81
+ try {
82
+ const receipt = path.join(dir, 'receipt.json')
83
+ const wrapper = path.join(dir, 'vite.config.mjs')
84
+ let importLazy = ''
85
+ if (lazy) {
86
+ const source = fs.readFileSync(path.resolve(moduleDir, '../dist/lazy-lib.mjs'), 'utf8')
87
+ const compiler = pathToFileURL(compilerEntry(require)).href
88
+ const prepared = source.replaceAll('"epxing:project-vite"', '"vite"').replaceAll('"epxing:project-sfc"', JSON.stringify(compiler))
89
+ if (prepared.includes('"epxing:project-')) throw new Error('按需构建资源包含未解析的项目依赖')
90
+ fs.writeFileSync(path.join(dir, 'lazy-lib.mjs'), prepared)
91
+ importLazy = "import lazyLib from './lazy-lib.mjs'\n"
92
+ }
93
+ const options = JSON.stringify({ projectRoot: project.dir, configFile, lazy, receipt, pkgName: project.pkg.name })
94
+ fs.writeFileSync(wrapper, "import { loadConfigFromFile } from 'vite'\n" +
95
+ `import { configureLibrary } from ${JSON.stringify(pathToFileURL(path.join(moduleDir, 'lib-vite-config.mjs')).href)}\n` + importLazy +
96
+ `export default env => configureLibrary(env, { ...${options}, loadConfigFromFile${lazy ? ', lazyLib' : ''} })\n`)
97
+ const args = []
98
+ for (let i = 0; i < plan.args.length; i++) {
99
+ const arg = plan.args[i]
100
+ if (arg === '--config' || arg === '-c') { i++; continue }
101
+ if (!arg.startsWith('--config=')) args.push(arg)
102
+ }
103
+ args.push('--config', wrapper)
104
+ return {
105
+ args,
106
+ cleanup,
107
+ verify () {
108
+ if (!fs.existsSync(receipt)) throw new Error('产品脚本没有使用本次 Vite 构建配置,无法确认构建形态。请检查脚本是否透传 --config。')
109
+ const result = JSON.parse(fs.readFileSync(receipt, 'utf8'))
110
+ if (lazy) verifyLazy(result.outDir, result.previousStamp)
111
+ else console.log('✓ 完整产品库构建完成。')
112
+ }
113
+ }
114
+ } catch (error) { cleanup(); throw error }
115
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ async function flatten (plugins) {
5
+ const result = []
6
+ for (const value of plugins || []) {
7
+ const plugin = await value
8
+ if (Array.isArray(plugin)) result.push(...await flatten(plugin))
9
+ else if (plugin) result.push(plugin)
10
+ }
11
+ return result
12
+ }
13
+
14
+ export function manifestStamp (outDir) {
15
+ try { return fs.statSync(path.join(outDir, 'index.manifest.json'), { bigint: true }).mtimeNs.toString() } catch (error) { return null }
16
+ }
17
+
18
+ // 在产品自己的 Vite 中执行。通过 loadConfigFromFile 保留原配置文件位置、函数配置和 TS 支持。
19
+ export async function configureLibrary (env, options) {
20
+ const { projectRoot, configFile, lazy, receipt, loadConfigFromFile, lazyLib, pkgName } = options
21
+ process.env.LAZY_LIB = lazy ? 'true' : 'false'
22
+ const loaded = await loadConfigFromFile(env, configFile, projectRoot)
23
+ if (!loaded) throw new Error('无法加载产品 Vite 配置:' + configFile)
24
+ const config = loaded.config
25
+ const plugins = await flatten(config.plugins)
26
+ const root = path.resolve(projectRoot, config.root || '.')
27
+ if (!config.build?.lib) throw new Error('当前 Vite 配置没有 build.lib,请为产品配置远程库入口。')
28
+ if (!lazy) {
29
+ config.plugins = plugins.filter(plugin => plugin.name !== 'epx-lazy-lib')
30
+ } else {
31
+ config.plugins = plugins
32
+ if (!plugins.some(plugin => plugin.name === 'epx-lazy-lib')) {
33
+ const entry = config.build.lib.entry
34
+ const file = Array.isArray(entry) ? entry[0] : typeof entry === 'object' ? Object.values(entry)[0] : entry
35
+ if (!file) throw new Error('无法确定产品库入口 build.lib.entry')
36
+ const source = fs.readFileSync(path.resolve(root, file), 'utf8')
37
+ const match = /import\.meta\.glob\(\s*(['"])([^'"]+)\1\s*,\s*\{\s*eager\s*:\s*true\s*\}\s*\)/.exec(source)
38
+ if (!match) throw new Error('按需库需要入口中的 eager import.meta.glob;请先迁移产品入口,或使用 epx lib --full。')
39
+ config.plugins.push(lazyLib({ enabled: true, glob: match[2], pkgName }))
40
+ }
41
+ }
42
+ // 嵌套的 chunks/full 构建会复用插件对象,仅记录外层构建,避免覆盖构建前的时间戳。
43
+ let recorded = false
44
+ config.plugins.push({
45
+ name: 'epxing-cli-library-receipt',
46
+ configResolved (resolved) {
47
+ if (recorded) return
48
+ recorded = true
49
+ const outDir = path.resolve(resolved.root, resolved.build.outDir)
50
+ fs.writeFileSync(receipt, JSON.stringify({ outDir, previousStamp: manifestStamp(outDir) }))
51
+ }
52
+ })
53
+ return config
54
+ }
@@ -0,0 +1,195 @@
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
+ import { prepareLibraryBuild } from './lib-build.mjs'
7
+
8
+ export function findProject (cwd) {
9
+ let dir = path.resolve(cwd)
10
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new Error(`工程目录不存在:${dir}`)
11
+ while (true) {
12
+ const file = path.join(dir, 'package.json')
13
+ if (fs.existsSync(file)) {
14
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'))
15
+ return { dir, pkg, scripts: pkg.scripts || {} }
16
+ }
17
+ const parent = path.dirname(dir)
18
+ if (parent === dir) throw new Error(`未找到项目 package.json:${cwd}。请进入项目目录,或指定 --dir。`)
19
+ dir = parent
20
+ }
21
+ }
22
+
23
+ function npmEntry () {
24
+ const nodeDir = path.dirname(process.execPath)
25
+ const roots = [nodeDir, ...String(process.env.PATH || process.env.Path || '').split(path.delimiter)]
26
+ const candidates = [process.env.npm_execpath && /npm-cli\.js$/.test(process.env.npm_execpath) ? process.env.npm_execpath : null]
27
+ for (const dir of roots) {
28
+ if (!dir) continue
29
+ candidates.push(path.join(dir, 'node_modules/npm/bin/npm-cli.js'), path.resolve(dir, '../lib/node_modules/npm/bin/npm-cli.js'))
30
+ const executable = path.join(dir, 'npm')
31
+ try {
32
+ const real = fs.realpathSync(executable)
33
+ if (real.endsWith('.js')) candidates.push(real)
34
+ } catch (error) { /* 继续查找 */ }
35
+ }
36
+ const found = candidates.find(file => file && fs.existsSync(file))
37
+ if (!found) throw new Error('未找到 npm-cli.js。请使用带 npm 的 Node.js 环境运行 epx。')
38
+ return found
39
+ }
40
+
41
+ function execute (file, args, cwd, extraEnv) {
42
+ return new Promise((resolve, reject) => {
43
+ const child = spawn(process.execPath, [file, ...args], { cwd, stdio: 'inherit', env: { ...process.env, ...extraEnv } })
44
+ const interrupt = () => child.kill('SIGINT')
45
+ const terminate = () => child.kill('SIGTERM')
46
+ const cleanup = () => {
47
+ process.removeListener('SIGINT', interrupt)
48
+ process.removeListener('SIGTERM', terminate)
49
+ }
50
+ process.on('SIGINT', interrupt)
51
+ process.on('SIGTERM', terminate)
52
+ child.once('error', error => { cleanup(); reject(error) })
53
+ child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1)) })
54
+ })
55
+ }
56
+
57
+ function localBinary (project, name) {
58
+ const require = createRequire(path.join(project.dir, 'package.json'))
59
+ try {
60
+ const manifest = require.resolve(name + '/package.json')
61
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'))
62
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[name]
63
+ const file = bin && path.resolve(path.dirname(manifest), bin)
64
+ if (file && fs.existsSync(file)) return file
65
+ } catch (error) { /* 由调用方提示项目安装依赖 */ }
66
+ return null
67
+ }
68
+
69
+ function takeValue (args, flag, short) {
70
+ const index = args.findIndex(arg => arg === flag || arg.startsWith(flag + '=') || (short && (arg === short || arg.startsWith(short))))
71
+ if (index < 0) return null
72
+ const arg = args[index]
73
+ let value
74
+ 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 {
75
+ value = args[index + 1]
76
+ if (!value || value.startsWith('-')) throw new Error(`${flag} 缺少值`)
77
+ args.splice(index, 2)
78
+ }
79
+ if (!value) throw new Error(`${flag} 缺少值`)
80
+ return value
81
+ }
82
+
83
+ export function parseProjectArgs (argv) {
84
+ const args = [...argv]
85
+ const separator = args.indexOf('--')
86
+ const forward = separator >= 0 ? args.splice(separator).slice(1) : []
87
+ const flag = name => { const index = args.indexOf(name); if (index < 0) return false; args.splice(index, 1); return true }
88
+ const result = {
89
+ dir: takeValue(args, '--dir'),
90
+ mode: takeValue(args, '--mode', '-m'),
91
+ port: takeValue(args, '--port', '-p'),
92
+ thin: flag('--thin'),
93
+ noDesigner: flag('--no-designer'),
94
+ noDashboard: flag('--no-dashboard'),
95
+ lazy: flag('--lazy'),
96
+ full: flag('--full'),
97
+ lib: flag('--lib'),
98
+ fix: flag('--fix'),
99
+ forward,
100
+ positional: args
101
+ }
102
+ if (result.mode && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(result.mode)) throw new Error('mode 仅支持字母、数字、点、下划线和短横线')
103
+ if (result.port && (!/^\d+$/.test(result.port) || +result.port < 1 || +result.port > 65535)) throw new Error('端口需为 1–65535 的整数')
104
+ return result
105
+ }
106
+
107
+ export function projectPlan (command, options, project) {
108
+ const { scripts } = project
109
+ const pick = (...names) => names.find(name => name && typeof scripts[name] === 'string' && scripts[name].trim())
110
+ const args = []
111
+ const env = {}
112
+ let script
113
+ let binary
114
+ let libraryMode
115
+ const platform = project.pkg.name === 'epx-frame' || !!scripts['build:component']
116
+ if (options.lib) {
117
+ if (command !== 'build') throw new Error('--lib 用于 epx build;也可直接使用 epx lib')
118
+ command = 'lib'
119
+ }
120
+ if (command === 'build' && !scripts.build && scripts['build:lib']) command = 'lib'
121
+ if (options.lazy && options.full) throw new Error('--lazy 和 --full 不能同时使用')
122
+ if (options.full && command !== 'lib') throw new Error('--full 仅用于 lib 或 build --lib')
123
+ if (options.positional.length && !(command === 'pkg' && options.positional.length === 1 && !options.positional[0].startsWith('-'))) throw new Error(`不支持的参数:${options.positional.join(' ')}`)
124
+ if (options.port && !['dev', 'preview'].includes(command)) throw new Error('--port 仅用于 dev 或 preview')
125
+ if (options.lazy && command !== 'lib') throw new Error('--lazy 仅用于 lib 或 build --lib')
126
+ if (options.fix && command !== 'lint') throw new Error('--fix 仅用于 lint')
127
+ if ((options.thin || options.noDesigner || options.noDashboard) && (command !== 'build' || !platform)) throw new Error('平台裁剪选项仅用于平台源码中的 epx build')
128
+ if (options.mode && !['dev', 'build', 'lib', 'preview'].includes(command)) throw new Error('--mode 仅用于 dev、build、lib 或 preview')
129
+ if (command === 'dev') {
130
+ script = pick('dev', 'serve', 'start')
131
+ if (!script) binary = 'vite'
132
+ } else if (command === 'build') {
133
+ const modeScript = options.mode && pick('build:' + options.mode, options.mode === 'localize' && 'build:local')
134
+ script = modeScript || pick('build', 'build:lib')
135
+ if (!script) binary = 'vite'
136
+ if (modeScript) options = { ...options, mode: null }
137
+ if (options.thin) env.EXTERNALIZE_BUSINESS = 'true'
138
+ if (options.noDesigner) env.INCLUDE_DESIGNER = 'false'
139
+ if (options.noDashboard) env.INCLUDE_DASHBOARD = 'false'
140
+ } else if (command === 'lib') {
141
+ if (platform && !pick('build:lib', 'lib', 'build:lib:lazy')) {
142
+ if (options.lazy) throw new Error('平台聚合库不支持产品懒加载模式')
143
+ script = pick('build:component')
144
+ } else {
145
+ libraryMode = options.full ? 'full' : 'lazy'
146
+ env.LAZY_LIB = options.full ? 'false' : 'true'
147
+ script = options.full ? pick('build:lib:full', 'build:lib', 'lib', 'build:lib:lazy') : pick('build:lib:lazy', 'build:lib', 'lib')
148
+ if (!script && /--target(?:=|\s+)lib\b/.test(scripts.build || '')) {
149
+ if (!options.full) throw new Error('旧 webpack 产品请先迁移到 Vite,或使用 epx lib --full 完整打包')
150
+ script = 'build'
151
+ }
152
+ }
153
+ if (!script) throw new Error('当前工程缺少库构建脚本。产品请提供 build:lib(按需库可用 build:lib:lazy),平台使用 build:component。')
154
+ if (script === 'build:component' && options.mode) throw new Error('平台聚合库包含多步构建,请在 build:component 中配置环境;epx lib 不向该聚合脚本追加 --mode。')
155
+ } else if (command === 'preview') {
156
+ script = pick('preview', 'vite:preview')
157
+ if (!script) binary = 'vite'
158
+ } else if (command === 'remote') script = pick('build:remote')
159
+ else if (command === 'pkg') {
160
+ const name = options.positional[0]
161
+ if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('用法:epx pkg <包名|all> [--dir <平台目录>]')
162
+ script = name === 'all' ? pick('build:pkgs') : pick('build:pkg')
163
+ if (name !== 'all') env.PKG = name
164
+ } else if (command === 'lint') {
165
+ script = options.fix ? pick('lint:fix', 'lint') : pick('lint')
166
+ if (options.fix && script !== 'lint:fix') args.push('--fix')
167
+ } else if (command === 'circular') script = pick('circular')
168
+ if (!script && !binary) throw new Error(`当前工程未提供 ${command} 对应的 package.json 脚本`)
169
+ if (options.mode) args.push('--mode', options.mode)
170
+ if (options.port) args.push('--port', options.port)
171
+ args.push(...options.forward)
172
+ if (['build', 'lib', 'remote', 'pkg'].includes(command) && !process.env.NODE_OPTIONS) env.NODE_OPTIONS = '--max-old-space-size=8192'
173
+ return { command, script, binary, args, env, libraryMode }
174
+ }
175
+
176
+ export async function runProject (command, argv, cwd) {
177
+ const options = parseProjectArgs(argv)
178
+ const project = findProject(options.dir ? path.resolve(cwd, options.dir) : cwd)
179
+ const plan = projectPlan(command, options, project)
180
+ console.log(`项目:${project.pkg.name || project.dir}(${project.dir})`)
181
+ if (plan.script) {
182
+ const library = prepareLibraryBuild(project, plan)
183
+ try {
184
+ console.log(`→ npm run ${plan.script}${library.args.length ? ' -- ' + library.args.join(' ') : ''}`)
185
+ const code = await execute(npmEntry(), ['run', plan.script, ...(library.args.length ? ['--', ...library.args] : [])], project.dir, plan.env)
186
+ if (code === 0) library.verify()
187
+ return code
188
+ } finally { library.cleanup() }
189
+ }
190
+ const file = localBinary(project, plan.binary)
191
+ if (!file) throw new Error(`当前工程没有可用脚本或本地 ${plan.binary},请先在工程内安装依赖并配置相应脚本。`)
192
+ const args = [...(command === 'dev' ? [] : [command]), ...plan.args]
193
+ console.log(`→ 本地 ${plan.binary} ${args.join(' ')}`)
194
+ return execute(file, args, project.dir, plan.env)
195
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@epxing/cli",
3
- "version": "0.1.0",
4
- "description": "EPX 产品和项目创建、Vue3 迁移及升级技能安装工具",
3
+ "version": "0.3.0",
4
+ "description": "EPX 交互式工程开发、服务启动、构建、Vue3 迁移及技能安装工具",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "epx": "dist/cli.mjs"