@quicktvui/web-cli 1.0.8 → 2.1.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 +101 -113
- package/bin/qt-web-cli-watch.js +0 -0
- package/bin/qt-web-cli.js +401 -100
- package/lib/BundleWatcher.js +192 -0
- package/lib/DevBuildManager.js +295 -0
- package/lib/DevServer.js +586 -0
- package/lib/HotReloader.js +142 -0
- package/lib/index.js +52 -122
- package/package.json +42 -19
- package/templates/dev-renderer.html +357 -0
package/bin/qt-web-cli.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* QuickTVUI Web
|
|
5
|
-
*
|
|
4
|
+
* QuickTVUI Web CLI v2
|
|
5
|
+
*
|
|
6
|
+
* v2 架构:委托构建 + Bundle 加载模式
|
|
7
|
+
* - 调用项目的 dev 脚本构建
|
|
8
|
+
* - 监听 dist/dev 目录
|
|
9
|
+
* - 启动轻量静态服务器加载 dist/dev 的 bundle
|
|
10
|
+
*
|
|
11
|
+
* v1 回退:当项目没有 dev 脚本时,使用原有的 webpack 自构建模式
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
const path = require('path')
|
|
@@ -13,8 +19,8 @@ const { exec } = require('shelljs')
|
|
|
13
19
|
|
|
14
20
|
// 解析命令行参数
|
|
15
21
|
const args = minimist(process.argv.slice(2), {
|
|
16
|
-
string: ['port', 'config'],
|
|
17
|
-
boolean: ['help', 'open', 'v', 'info', 'version', 'watch'],
|
|
22
|
+
string: ['port', 'config', 'devScript', 'distDir'],
|
|
23
|
+
boolean: ['help', 'open', 'v', 'info', 'version', 'watch', 'noAutoBuild', 'v1'],
|
|
18
24
|
alias: {
|
|
19
25
|
p: 'port',
|
|
20
26
|
c: 'config',
|
|
@@ -26,6 +32,8 @@ const args = minimist(process.argv.slice(2), {
|
|
|
26
32
|
port: 39001,
|
|
27
33
|
open: process.env.QUICKTVUI_NO_OPEN !== 'true',
|
|
28
34
|
watch: false,
|
|
35
|
+
noAutoBuild: false,
|
|
36
|
+
v1: false,
|
|
29
37
|
},
|
|
30
38
|
})
|
|
31
39
|
|
|
@@ -36,12 +44,12 @@ if (args.v || args.version) {
|
|
|
36
44
|
process.exit(0)
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
//
|
|
47
|
+
// 显示详细信息
|
|
40
48
|
if (args.info) {
|
|
41
49
|
const cliPkg = require(path.join(__dirname, '../package.json'))
|
|
42
50
|
console.log(`@quicktvui/web-cli v${cliPkg.version}`)
|
|
51
|
+
console.log(` Mode: v2 (delegate build)`)
|
|
43
52
|
try {
|
|
44
|
-
// 尝试多种路径查找 web-renderer
|
|
45
53
|
let rendererPkg
|
|
46
54
|
const possiblePaths = [
|
|
47
55
|
'@quicktvui/web-renderer/package.json',
|
|
@@ -52,9 +60,7 @@ if (args.info) {
|
|
|
52
60
|
try {
|
|
53
61
|
rendererPkg = require(pkgPath)
|
|
54
62
|
break
|
|
55
|
-
} catch (e) {
|
|
56
|
-
// 继续尝试下一个路径
|
|
57
|
-
}
|
|
63
|
+
} catch (e) {}
|
|
58
64
|
}
|
|
59
65
|
if (rendererPkg) {
|
|
60
66
|
console.log(`@quicktvui/web-renderer v${rendererPkg.version}`)
|
|
@@ -70,57 +76,270 @@ if (args.info) {
|
|
|
70
76
|
// 显示帮助信息
|
|
71
77
|
if (args.help) {
|
|
72
78
|
console.log(`
|
|
73
|
-
QuickTVUI Web CLI
|
|
79
|
+
QuickTVUI Web CLI v2
|
|
74
80
|
|
|
75
81
|
Usage:
|
|
76
82
|
qt-web-cli [options]
|
|
77
83
|
|
|
78
84
|
Options:
|
|
79
|
-
-p, --port <port>
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
--
|
|
85
|
+
-p, --port <port> 开发服务器端口 (默认: 39001)
|
|
86
|
+
-o, --open 自动打开浏览器
|
|
87
|
+
-h, --help 显示帮助信息
|
|
88
|
+
-v, --version 显示 CLI 版本
|
|
89
|
+
--info 显示 CLI 和 web-renderer 版本
|
|
90
|
+
--dev-script <name> 指定要运行的 npm script (默认: dev)
|
|
91
|
+
--dist-dir <path> 指定构建产物目录 (默认: dist/dev)
|
|
92
|
+
--no-auto-build 跳过自动调用 dev 脚本,仅启动服务器
|
|
93
|
+
--v1 使用 v1 模式 (webpack 自构建,已废弃)
|
|
94
|
+
|
|
95
|
+
v2 模式 (默认):
|
|
96
|
+
自动检测项目的 dev 脚本,调用 npm run dev 构建产物
|
|
97
|
+
监听 dist/dev/ 目录变化,通过 web-runtime 加载 bundle
|
|
98
|
+
支持热更新 (SSE)
|
|
99
|
+
|
|
100
|
+
v1 模式 (--v1):
|
|
101
|
+
使用内置 webpack 配置自行构建项目 (已废弃)
|
|
86
102
|
|
|
87
103
|
Examples:
|
|
88
|
-
qt-web-cli
|
|
89
|
-
qt-web-cli --
|
|
90
|
-
qt-web-cli --
|
|
91
|
-
qt-web-cli --
|
|
92
|
-
qt-web-cli
|
|
93
|
-
qt-web-cli --info
|
|
94
|
-
|
|
95
|
-
特点:
|
|
96
|
-
- 自动检测项目入口 (main-native.ts/js, main.ts/js)
|
|
97
|
-
- 零配置启动 web 开发服务器
|
|
98
|
-
- 支持文件变化自动重启 (--watch)
|
|
104
|
+
qt-web-cli # v2 模式,自动构建+启动
|
|
105
|
+
qt-web-cli --no-auto-build # v2 模式,仅启动服务器(需先手动 npm run dev)
|
|
106
|
+
qt-web-cli --dev-script build # 使用 build 脚本替代 dev
|
|
107
|
+
qt-web-cli --port 8080 # 指定端口
|
|
108
|
+
qt-web-cli --v1 # 使用 v1 模式
|
|
99
109
|
`)
|
|
100
110
|
process.exit(0)
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// 主入口
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
103
117
|
async function main() {
|
|
104
|
-
//
|
|
105
|
-
if (args.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
// 强制使用 v1 模式
|
|
119
|
+
if (args.v1) {
|
|
120
|
+
signale.warn('v1 模式已废弃,建议迁移到 v2 模式')
|
|
121
|
+
return runV1()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
signale.pending('正在启动 QuickTVUI Web CLI v2 ...')
|
|
125
|
+
|
|
126
|
+
// 查找项目根目录
|
|
127
|
+
const projectRoot = findProjectRoot()
|
|
128
|
+
if (!projectRoot) {
|
|
129
|
+
signale.error('无法找到项目根目录')
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
signale.info(`项目根目录: ${projectRoot}`)
|
|
134
|
+
|
|
135
|
+
// 读取 package.json
|
|
136
|
+
let pkg = {}
|
|
137
|
+
try {
|
|
138
|
+
pkg = require(path.join(projectRoot, 'package.json'))
|
|
139
|
+
} catch (e) {}
|
|
140
|
+
|
|
141
|
+
// 检测是否支持 v2 模式
|
|
142
|
+
const devScript = detectDevScript(pkg, args.devScript)
|
|
143
|
+
const distDir = args.distDir || 'dist/dev'
|
|
144
|
+
const distFullPath = path.join(projectRoot, distDir)
|
|
145
|
+
|
|
146
|
+
if (!devScript && !fs.existsSync(distFullPath)) {
|
|
147
|
+
signale.warn('项目未配置 dev 脚本且 dist/dev 目录不存在,回退到 v1 模式')
|
|
148
|
+
return runV1()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// v2 模式
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
const DevBuildManager = require('../lib/DevBuildManager')
|
|
156
|
+
const BundleWatcher = require('../lib/BundleWatcher')
|
|
157
|
+
const DevServer = require('../lib/DevServer')
|
|
158
|
+
const HotReloader = require('../lib/HotReloader')
|
|
159
|
+
|
|
160
|
+
// 初始化各模块
|
|
161
|
+
const hotReloader = new HotReloader()
|
|
162
|
+
const bundleWatcher = new BundleWatcher(projectRoot, {
|
|
163
|
+
onChange: (event) => {
|
|
164
|
+
if (event.isBundle) {
|
|
165
|
+
// 通知浏览器刷新
|
|
166
|
+
hotReloader.notifyBundleUpdate({
|
|
167
|
+
file: event.file,
|
|
168
|
+
bundleEntry: event.bundleEntry,
|
|
169
|
+
bundleUrlPath: event.bundleUrlPath,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const devServer = new DevServer(projectRoot, {
|
|
176
|
+
port: args.port,
|
|
177
|
+
hotReloader,
|
|
178
|
+
bundleUrlPath: `/dist/dev/index.bundle`,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// 记录是否已打开浏览器(避免重复打开)
|
|
182
|
+
let browserOpened = false
|
|
183
|
+
|
|
184
|
+
const devBuild = new DevBuildManager(projectRoot, {
|
|
185
|
+
pkg,
|
|
186
|
+
devScript: args.devScript || 'dev',
|
|
187
|
+
onOutput: (text, stream) => {
|
|
188
|
+
// 将构建输出透传到控制台
|
|
189
|
+
if (stream === 'stdout') {
|
|
190
|
+
process.stdout.write(text)
|
|
191
|
+
} else {
|
|
192
|
+
process.stderr.write(text)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
onReady: () => {
|
|
196
|
+
// 构建完成后,通知浏览器
|
|
197
|
+
hotReloader.notifyBuildStatus('ready', '构建完成')
|
|
198
|
+
|
|
199
|
+
// 更新 bundle URL
|
|
200
|
+
const bundleUrlPath = bundleWatcher.getBundleUrlPath()
|
|
201
|
+
if (bundleUrlPath) {
|
|
202
|
+
devServer.setBundleUrlPath(bundleUrlPath)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 构建就绪后打开浏览器(只打开一次)
|
|
206
|
+
if (args.open && !browserOpened) {
|
|
207
|
+
browserOpened = true
|
|
208
|
+
const urlPath = bundleUrlPath || '/dist/dev/index.bundle'
|
|
209
|
+
const url = `http://localhost:${args.port}?bundle=${urlPath}`
|
|
210
|
+
openBrowser(url)
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// 检测已有构建产物
|
|
216
|
+
const existingBundle = devBuild.detectExistingBundle()
|
|
217
|
+
const shouldAutoBuild = !args.noAutoBuild && devScript
|
|
218
|
+
|
|
219
|
+
if (existingBundle) {
|
|
220
|
+
signale.success(
|
|
221
|
+
`检测到已有构建产物: ${distDir}/${existingBundle.entry} (${formatSize(existingBundle.size)})`
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// 更新 bundle URL
|
|
225
|
+
devServer.setBundleUrlPath(`/dist/dev/${existingBundle.entry}`)
|
|
226
|
+
} else if (shouldAutoBuild) {
|
|
227
|
+
signale.info('未检测到构建产物,将自动启动 dev 构建')
|
|
228
|
+
} else {
|
|
229
|
+
signale.warn('未检测到构建产物,且未启用自动构建')
|
|
230
|
+
signale.warn('请先手动运行: npm run dev')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 启动文件监听
|
|
234
|
+
bundleWatcher.start()
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// 端口预检测
|
|
238
|
+
// ============================================================================
|
|
239
|
+
const net = require('net')
|
|
240
|
+
|
|
241
|
+
// 检测 web-cli 端口(39001)
|
|
242
|
+
const webCliPortAvailable = await checkPortAvailable(net, args.port)
|
|
243
|
+
if (!webCliPortAvailable) {
|
|
244
|
+
const processInfo = getPortProcessInfo(args.port)
|
|
245
|
+
const pidInfo = processInfo
|
|
246
|
+
? processInfo.name
|
|
247
|
+
? `PID: ${processInfo.pid} (${processInfo.name})`
|
|
248
|
+
: `PID: ${processInfo.pid}`
|
|
249
|
+
: '无法获取进程信息'
|
|
250
|
+
signale.warn(`Web CLI 端口 ${args.port} 已被占用: ${pidInfo}`)
|
|
251
|
+
|
|
252
|
+
const killed = await promptKillPort(args.port, processInfo)
|
|
253
|
+
if (!killed) {
|
|
254
|
+
signale.error(`端口 ${args.port} 已被占用,请使用 --port 指定其他端口`)
|
|
255
|
+
process.exit(1)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 检测 debug-server 端口(38989)— 仅在使用 qt-dev 的项目中提示
|
|
260
|
+
// Vue3 项目使用 qt-dev,会启动 debug-server 监听 38989
|
|
261
|
+
// Vue2 项目使用纯 webpack,不涉及 38989 端口
|
|
262
|
+
const isQtDevProject =
|
|
263
|
+
devScript &&
|
|
264
|
+
(devScript.script.includes('qt-dev') ||
|
|
265
|
+
devScript.script.includes('es3-debug-server') ||
|
|
266
|
+
devScript.script.includes('hippy-debug-server'))
|
|
267
|
+
if (isQtDevProject) {
|
|
268
|
+
const debugServerPort = 38989
|
|
269
|
+
const debugPortAvailable = await checkPortAvailable(net, debugServerPort)
|
|
270
|
+
if (!debugPortAvailable) {
|
|
271
|
+
const processInfo = getPortProcessInfo(debugServerPort)
|
|
272
|
+
const pidInfo = processInfo
|
|
273
|
+
? processInfo.name
|
|
274
|
+
? `PID: ${processInfo.pid} (${processInfo.name})`
|
|
275
|
+
: `PID: ${processInfo.pid}`
|
|
276
|
+
: '无法获取进程信息'
|
|
277
|
+
signale.warn(`Debug Server 端口 ${debugServerPort} 已被占用: ${pidInfo}`)
|
|
278
|
+
signale.warn('如遇 dev 启动失败,请先关闭占用该端口的进程')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
115
281
|
|
|
116
|
-
|
|
117
|
-
|
|
282
|
+
// 先启动开发服务器(确保浏览器打开时服务器已就绪)
|
|
283
|
+
try {
|
|
284
|
+
await devServer.start()
|
|
285
|
+
signale.success(`开发服务器已启动: http://localhost:${args.port}`)
|
|
286
|
+
|
|
287
|
+
// 构建状态通知
|
|
288
|
+
hotReloader.notifyBuildStatus(
|
|
289
|
+
existingBundle ? 'ready' : 'building',
|
|
290
|
+
existingBundle ? 'Bundle 已就绪' : '等待构建完成...'
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// 仅在不需要自动构建且已有产物时立即打开浏览器
|
|
294
|
+
// 否则等 dev 构建完成后再打开(在 onReady 回调中)
|
|
295
|
+
if (args.open && existingBundle && !shouldAutoBuild && !browserOpened) {
|
|
296
|
+
browserOpened = true
|
|
297
|
+
const bundleUrlPath = bundleWatcher.getBundleUrlPath() || '/dist/dev/index.bundle'
|
|
298
|
+
const url = `http://localhost:${args.port}?bundle=${bundleUrlPath}`
|
|
299
|
+
openBrowser(url)
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
signale.error('开发服务器启动失败:', err.message)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 启动 dev 构建(服务器已就绪,构建完成后在 onReady 回调中打开浏览器)
|
|
307
|
+
if (shouldAutoBuild && !existingBundle) {
|
|
308
|
+
try {
|
|
309
|
+
await devBuild.start()
|
|
310
|
+
} catch (err) {
|
|
311
|
+
signale.error('dev 构建启动失败:', err.message)
|
|
312
|
+
signale.warn('你可以手动运行 npm run dev,然后使用 --no-auto-build 重新启动 CLI')
|
|
313
|
+
}
|
|
314
|
+
} else if (shouldAutoBuild && existingBundle) {
|
|
315
|
+
// 已有构建产物,在后台启动 dev 构建
|
|
316
|
+
// 等待 webpack 编译完成后再打开浏览器(而不是立即打开)
|
|
317
|
+
signale.info('已有构建产物,后台启动 dev 构建用于热更新...')
|
|
318
|
+
devBuild.start().catch((err) => {
|
|
319
|
+
signale.warn('后台 dev 构建启动失败:', err.message)
|
|
118
320
|
})
|
|
321
|
+
}
|
|
119
322
|
|
|
120
|
-
|
|
323
|
+
// 优雅退出
|
|
324
|
+
const cleanup = () => {
|
|
325
|
+
signale.info('正在停止...')
|
|
326
|
+
devBuild.stop()
|
|
327
|
+
bundleWatcher.stop()
|
|
328
|
+
devServer.stop()
|
|
329
|
+
signale.success('已停止')
|
|
330
|
+
process.exit(0)
|
|
121
331
|
}
|
|
122
332
|
|
|
123
|
-
|
|
333
|
+
process.on('SIGINT', cleanup)
|
|
334
|
+
process.on('SIGTERM', cleanup)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// v1 模式 (webpack 自构建,已废弃)
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
function runV1() {
|
|
342
|
+
signale.pending('正在启动 QuickTVUI Web CLI v1 (webpack 模式)...')
|
|
124
343
|
|
|
125
344
|
// 查找项目根目录
|
|
126
345
|
const projectRoot = findProjectRoot()
|
|
@@ -141,7 +360,6 @@ async function main() {
|
|
|
141
360
|
let mainEntry
|
|
142
361
|
let entryType
|
|
143
362
|
|
|
144
|
-
// 入口候选列表
|
|
145
363
|
const entryCandidates = [
|
|
146
364
|
{ name: 'src/main-native.ts', path: './src/main-native.ts', type: 'package' },
|
|
147
365
|
{ name: 'src/main-native.js', path: './src/main-native.js', type: 'package' },
|
|
@@ -162,10 +380,7 @@ async function main() {
|
|
|
162
380
|
}
|
|
163
381
|
|
|
164
382
|
if (!mainEntry) {
|
|
165
|
-
signale.error('
|
|
166
|
-
signale.error(' - src/main-native.ts 或 src/main-native.js')
|
|
167
|
-
signale.error(' - src/main.ts 或 src/main.js')
|
|
168
|
-
signale.error(' - package.json 中的 main 字段')
|
|
383
|
+
signale.error('无法找到有效的入口文件')
|
|
169
384
|
process.exit(1)
|
|
170
385
|
}
|
|
171
386
|
|
|
@@ -194,17 +409,15 @@ async function main() {
|
|
|
194
409
|
}
|
|
195
410
|
|
|
196
411
|
/**
|
|
197
|
-
* 使用 webpack Node API 启动开发服务器
|
|
412
|
+
* 使用 webpack Node API 启动开发服务器 (v1)
|
|
198
413
|
*/
|
|
199
414
|
function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
200
415
|
const webpack = require('webpack')
|
|
201
416
|
const WebpackDevServer = require('webpack-dev-server')
|
|
202
417
|
const { merge } = require('webpack-merge')
|
|
203
418
|
|
|
204
|
-
// 加载内置配置
|
|
205
419
|
let config = require(configPath)
|
|
206
420
|
|
|
207
|
-
// 如果用户指定了自定义配置,进行合并
|
|
208
421
|
if (userConfigPath) {
|
|
209
422
|
if (!fs.existsSync(userConfigPath)) {
|
|
210
423
|
signale.error(`自定义配置文件不存在: ${userConfigPath}`)
|
|
@@ -230,8 +443,7 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
|
230
443
|
signale.success(`开发服务器已启动: http://localhost:${port}`)
|
|
231
444
|
|
|
232
445
|
if (shouldOpen) {
|
|
233
|
-
|
|
234
|
-
openBrowser(url)
|
|
446
|
+
openBrowser(`http://localhost:${port}`)
|
|
235
447
|
}
|
|
236
448
|
})
|
|
237
449
|
|
|
@@ -243,45 +455,45 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
|
243
455
|
})
|
|
244
456
|
}
|
|
245
457
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
function waitForServer(url, timeout = 30000) {
|
|
250
|
-
const http = require('http')
|
|
251
|
-
const startTime = Date.now()
|
|
252
|
-
|
|
253
|
-
return new Promise((resolve, reject) => {
|
|
254
|
-
function check() {
|
|
255
|
-
if (Date.now() - startTime > timeout) {
|
|
256
|
-
reject(new Error('等待服务器超时'))
|
|
257
|
-
return
|
|
258
|
-
}
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// 工具函数
|
|
460
|
+
// ============================================================================
|
|
259
461
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
})
|
|
462
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
463
|
+
let currentDir = startDir
|
|
464
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
465
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
466
|
+
return currentDir
|
|
467
|
+
}
|
|
468
|
+
currentDir = path.dirname(currentDir)
|
|
469
|
+
}
|
|
470
|
+
return null
|
|
471
|
+
}
|
|
271
472
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
473
|
+
function detectDevScript(pkg, scriptName) {
|
|
474
|
+
const scripts = pkg.scripts || {}
|
|
475
|
+
const candidates = [scriptName, 'dev', 'dev:android', 'build:dev']
|
|
476
|
+
for (const name of candidates) {
|
|
477
|
+
if (name && scripts[name]) {
|
|
478
|
+
return { name, script: scripts[name] }
|
|
276
479
|
}
|
|
480
|
+
}
|
|
481
|
+
return null
|
|
482
|
+
}
|
|
277
483
|
|
|
278
|
-
|
|
279
|
-
|
|
484
|
+
function checkOpenSSLVersion(version) {
|
|
485
|
+
if (!version) return false
|
|
486
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
|
|
487
|
+
if (!match) return false
|
|
488
|
+
return parseInt(match[1], 10) >= 3
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function formatSize(bytes) {
|
|
492
|
+
if (bytes < 1024) return bytes + ' B'
|
|
493
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
494
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
280
495
|
}
|
|
281
496
|
|
|
282
|
-
/**
|
|
283
|
-
* 打开浏览器
|
|
284
|
-
*/
|
|
285
497
|
function openBrowser(url) {
|
|
286
498
|
const platform = process.platform
|
|
287
499
|
const command =
|
|
@@ -294,30 +506,119 @@ function openBrowser(url) {
|
|
|
294
506
|
|
|
295
507
|
if (result.code === 0) {
|
|
296
508
|
signale.success('已打开浏览器')
|
|
297
|
-
|
|
509
|
+
} else {
|
|
510
|
+
signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
|
|
298
511
|
}
|
|
512
|
+
}
|
|
299
513
|
|
|
300
|
-
|
|
514
|
+
/**
|
|
515
|
+
* 检测端口是否可用(通过尝试绑定)
|
|
516
|
+
*/
|
|
517
|
+
function checkPortAvailable(net, port) {
|
|
518
|
+
return new Promise((resolve) => {
|
|
519
|
+
const tester = net.createServer()
|
|
520
|
+
tester.once('error', (err) => {
|
|
521
|
+
if (err.code === 'EADDRINUSE') {
|
|
522
|
+
resolve(false)
|
|
523
|
+
} else {
|
|
524
|
+
resolve(false)
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
tester.once('listening', () => {
|
|
528
|
+
tester.close()
|
|
529
|
+
resolve(true)
|
|
530
|
+
})
|
|
531
|
+
tester.listen(port)
|
|
532
|
+
})
|
|
301
533
|
}
|
|
302
534
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
535
|
+
/**
|
|
536
|
+
* 获取占用端口的进程信息
|
|
537
|
+
*/
|
|
538
|
+
function getPortProcessInfo(port) {
|
|
539
|
+
try {
|
|
540
|
+
const { execSync } = require('child_process')
|
|
541
|
+
const isWindows = process.platform === 'win32'
|
|
542
|
+
let cmd
|
|
543
|
+
if (isWindows) {
|
|
544
|
+
cmd = `netstat -ano | findstr :${port} | findstr LISTENING`
|
|
545
|
+
} else {
|
|
546
|
+
cmd = `lsof -i :${port} -P -n -t 2>/dev/null`
|
|
308
547
|
}
|
|
309
|
-
|
|
548
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim()
|
|
549
|
+
if (!result) return null
|
|
550
|
+
|
|
551
|
+
if (isWindows) {
|
|
552
|
+
const match = result.match(/LISTENING\s+(\d+)/)
|
|
553
|
+
return match ? { pid: match[1], name: '' } : null
|
|
554
|
+
} else {
|
|
555
|
+
const pid = result.split('\n')[0].trim()
|
|
556
|
+
if (!pid) return null
|
|
557
|
+
let name = ''
|
|
558
|
+
try {
|
|
559
|
+
name = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, { encoding: 'utf-8' }).trim()
|
|
560
|
+
} catch (e) {
|
|
561
|
+
/* ignore */
|
|
562
|
+
}
|
|
563
|
+
return { pid, name: name || 'unknown' }
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {
|
|
566
|
+
return null
|
|
310
567
|
}
|
|
311
|
-
return null
|
|
312
568
|
}
|
|
313
569
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
570
|
+
/**
|
|
571
|
+
* 提示用户是否杀掉占用端口的进程
|
|
572
|
+
*/
|
|
573
|
+
function promptKillPort(port, processInfo) {
|
|
574
|
+
return new Promise((resolve) => {
|
|
575
|
+
const readline = require('readline')
|
|
576
|
+
const rl = readline.createInterface({
|
|
577
|
+
input: process.stdin,
|
|
578
|
+
output: process.stdout,
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
const timeout = setTimeout(() => {
|
|
582
|
+
rl.close()
|
|
583
|
+
resolve(false)
|
|
584
|
+
}, 30000)
|
|
585
|
+
|
|
586
|
+
rl.question(` 是否杀掉该进程并继续? [y/N] `, (answer) => {
|
|
587
|
+
clearTimeout(timeout)
|
|
588
|
+
rl.close()
|
|
589
|
+
const confirmed = (answer || '').trim().toLowerCase() === 'y'
|
|
590
|
+
|
|
591
|
+
if (confirmed) {
|
|
592
|
+
if (!processInfo || !processInfo.pid) {
|
|
593
|
+
signale.error('无法获取占用端口的进程 PID,请手动查找并关闭')
|
|
594
|
+
resolve(false)
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const { execSync } = require('child_process')
|
|
599
|
+
const isWindows = process.platform === 'win32'
|
|
600
|
+
if (isWindows) {
|
|
601
|
+
execSync(`taskkill /PID ${processInfo.pid} /F`, { encoding: 'utf-8' })
|
|
602
|
+
} else {
|
|
603
|
+
execSync(`kill -9 ${processInfo.pid}`, { encoding: 'utf-8' })
|
|
604
|
+
}
|
|
605
|
+
signale.success(`已杀掉进程 ${processInfo.pid}`)
|
|
606
|
+
setTimeout(() => resolve(true), 500)
|
|
607
|
+
} catch (e) {
|
|
608
|
+
signale.error(`杀掉进程失败: ${e.message}`)
|
|
609
|
+
resolve(false)
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
resolve(false)
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
})
|
|
319
616
|
}
|
|
320
617
|
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// 启动
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
321
622
|
main().catch((err) => {
|
|
322
623
|
signale.error('启动失败:', err)
|
|
323
624
|
process.exit(1)
|