@quicktvui/web-cli 1.0.8 → 2.1.1
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 +405 -100
- package/lib/BundleWatcher.js +192 -0
- package/lib/DevBuildManager.js +295 -0
- package/lib/DevServer.js +586 -0
- package/lib/HotReloader.js +154 -0
- package/lib/index.js +52 -122
- package/package.json +42 -19
- package/templates/dev-renderer.html +370 -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,274 @@ 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
|
+
}
|
|
115
204
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
// 清除 HotReloader 中缓存的 bundle-update 事件
|
|
214
|
+
// 防止 SSE 重连后再次触发页面刷新导致无限循环
|
|
215
|
+
hotReloader.clearLastEvent()
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// 检测已有构建产物
|
|
220
|
+
const existingBundle = devBuild.detectExistingBundle()
|
|
221
|
+
const shouldAutoBuild = !args.noAutoBuild && devScript
|
|
222
|
+
|
|
223
|
+
if (existingBundle) {
|
|
224
|
+
signale.success(
|
|
225
|
+
`检测到已有构建产物: ${distDir}/${existingBundle.entry} (${formatSize(existingBundle.size)})`
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// 更新 bundle URL
|
|
229
|
+
devServer.setBundleUrlPath(`/dist/dev/${existingBundle.entry}`)
|
|
230
|
+
} else if (shouldAutoBuild) {
|
|
231
|
+
signale.info('未检测到构建产物,将自动启动 dev 构建')
|
|
232
|
+
} else {
|
|
233
|
+
signale.warn('未检测到构建产物,且未启用自动构建')
|
|
234
|
+
signale.warn('请先手动运行: npm run dev')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 启动文件监听
|
|
238
|
+
bundleWatcher.start()
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// 端口预检测
|
|
242
|
+
// ============================================================================
|
|
243
|
+
const net = require('net')
|
|
244
|
+
|
|
245
|
+
// 检测 web-cli 端口(39001)
|
|
246
|
+
const webCliPortAvailable = await checkPortAvailable(net, args.port)
|
|
247
|
+
if (!webCliPortAvailable) {
|
|
248
|
+
const processInfo = getPortProcessInfo(args.port)
|
|
249
|
+
const pidInfo = processInfo
|
|
250
|
+
? processInfo.name
|
|
251
|
+
? `PID: ${processInfo.pid} (${processInfo.name})`
|
|
252
|
+
: `PID: ${processInfo.pid}`
|
|
253
|
+
: '无法获取进程信息'
|
|
254
|
+
signale.warn(`Web CLI 端口 ${args.port} 已被占用: ${pidInfo}`)
|
|
255
|
+
|
|
256
|
+
const killed = await promptKillPort(args.port, processInfo)
|
|
257
|
+
if (!killed) {
|
|
258
|
+
signale.error(`端口 ${args.port} 已被占用,请使用 --port 指定其他端口`)
|
|
259
|
+
process.exit(1)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 检测 debug-server 端口(38989)— 仅在使用 qt-dev 的项目中提示
|
|
264
|
+
// Vue3 项目使用 qt-dev,会启动 debug-server 监听 38989
|
|
265
|
+
// Vue2 项目使用纯 webpack,不涉及 38989 端口
|
|
266
|
+
const isQtDevProject =
|
|
267
|
+
devScript &&
|
|
268
|
+
(devScript.script.includes('qt-dev') ||
|
|
269
|
+
devScript.script.includes('es3-debug-server') ||
|
|
270
|
+
devScript.script.includes('hippy-debug-server'))
|
|
271
|
+
if (isQtDevProject) {
|
|
272
|
+
const debugServerPort = 38989
|
|
273
|
+
const debugPortAvailable = await checkPortAvailable(net, debugServerPort)
|
|
274
|
+
if (!debugPortAvailable) {
|
|
275
|
+
const processInfo = getPortProcessInfo(debugServerPort)
|
|
276
|
+
const pidInfo = processInfo
|
|
277
|
+
? processInfo.name
|
|
278
|
+
? `PID: ${processInfo.pid} (${processInfo.name})`
|
|
279
|
+
: `PID: ${processInfo.pid}`
|
|
280
|
+
: '无法获取进程信息'
|
|
281
|
+
signale.warn(`Debug Server 端口 ${debugServerPort} 已被占用: ${pidInfo}`)
|
|
282
|
+
signale.warn('如遇 dev 启动失败,请先关闭占用该端口的进程')
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 先启动开发服务器(确保浏览器打开时服务器已就绪)
|
|
287
|
+
try {
|
|
288
|
+
await devServer.start()
|
|
289
|
+
signale.success(`开发服务器已启动: http://localhost:${args.port}`)
|
|
290
|
+
|
|
291
|
+
// 构建状态通知
|
|
292
|
+
hotReloader.notifyBuildStatus(
|
|
293
|
+
existingBundle ? 'ready' : 'building',
|
|
294
|
+
existingBundle ? 'Bundle 已就绪' : '等待构建完成...'
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
// 仅在不需要自动构建且已有产物时立即打开浏览器
|
|
298
|
+
// 否则等 dev 构建完成后再打开(在 onReady 回调中)
|
|
299
|
+
if (args.open && existingBundle && !shouldAutoBuild && !browserOpened) {
|
|
300
|
+
browserOpened = true
|
|
301
|
+
const bundleUrlPath = bundleWatcher.getBundleUrlPath() || '/dist/dev/index.bundle'
|
|
302
|
+
const url = `http://localhost:${args.port}?bundle=${bundleUrlPath}`
|
|
303
|
+
openBrowser(url)
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
signale.error('开发服务器启动失败:', err.message)
|
|
307
|
+
process.exit(1)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 启动 dev 构建(服务器已就绪,构建完成后在 onReady 回调中打开浏览器)
|
|
311
|
+
if (shouldAutoBuild && !existingBundle) {
|
|
312
|
+
try {
|
|
313
|
+
await devBuild.start()
|
|
314
|
+
} catch (err) {
|
|
315
|
+
signale.error('dev 构建启动失败:', err.message)
|
|
316
|
+
signale.warn('你可以手动运行 npm run dev,然后使用 --no-auto-build 重新启动 CLI')
|
|
317
|
+
}
|
|
318
|
+
} else if (shouldAutoBuild && existingBundle) {
|
|
319
|
+
// 已有构建产物,在后台启动 dev 构建
|
|
320
|
+
// 等待 webpack 编译完成后再打开浏览器(而不是立即打开)
|
|
321
|
+
signale.info('已有构建产物,后台启动 dev 构建用于热更新...')
|
|
322
|
+
devBuild.start().catch((err) => {
|
|
323
|
+
signale.warn('后台 dev 构建启动失败:', err.message)
|
|
118
324
|
})
|
|
325
|
+
}
|
|
119
326
|
|
|
120
|
-
|
|
327
|
+
// 优雅退出
|
|
328
|
+
const cleanup = () => {
|
|
329
|
+
signale.info('正在停止...')
|
|
330
|
+
devBuild.stop()
|
|
331
|
+
bundleWatcher.stop()
|
|
332
|
+
devServer.stop()
|
|
333
|
+
signale.success('已停止')
|
|
334
|
+
process.exit(0)
|
|
121
335
|
}
|
|
122
336
|
|
|
123
|
-
|
|
337
|
+
process.on('SIGINT', cleanup)
|
|
338
|
+
process.on('SIGTERM', cleanup)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// v1 模式 (webpack 自构建,已废弃)
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
function runV1() {
|
|
346
|
+
signale.pending('正在启动 QuickTVUI Web CLI v1 (webpack 模式)...')
|
|
124
347
|
|
|
125
348
|
// 查找项目根目录
|
|
126
349
|
const projectRoot = findProjectRoot()
|
|
@@ -141,7 +364,6 @@ async function main() {
|
|
|
141
364
|
let mainEntry
|
|
142
365
|
let entryType
|
|
143
366
|
|
|
144
|
-
// 入口候选列表
|
|
145
367
|
const entryCandidates = [
|
|
146
368
|
{ name: 'src/main-native.ts', path: './src/main-native.ts', type: 'package' },
|
|
147
369
|
{ name: 'src/main-native.js', path: './src/main-native.js', type: 'package' },
|
|
@@ -162,10 +384,7 @@ async function main() {
|
|
|
162
384
|
}
|
|
163
385
|
|
|
164
386
|
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 字段')
|
|
387
|
+
signale.error('无法找到有效的入口文件')
|
|
169
388
|
process.exit(1)
|
|
170
389
|
}
|
|
171
390
|
|
|
@@ -194,17 +413,15 @@ async function main() {
|
|
|
194
413
|
}
|
|
195
414
|
|
|
196
415
|
/**
|
|
197
|
-
* 使用 webpack Node API 启动开发服务器
|
|
416
|
+
* 使用 webpack Node API 启动开发服务器 (v1)
|
|
198
417
|
*/
|
|
199
418
|
function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
200
419
|
const webpack = require('webpack')
|
|
201
420
|
const WebpackDevServer = require('webpack-dev-server')
|
|
202
421
|
const { merge } = require('webpack-merge')
|
|
203
422
|
|
|
204
|
-
// 加载内置配置
|
|
205
423
|
let config = require(configPath)
|
|
206
424
|
|
|
207
|
-
// 如果用户指定了自定义配置,进行合并
|
|
208
425
|
if (userConfigPath) {
|
|
209
426
|
if (!fs.existsSync(userConfigPath)) {
|
|
210
427
|
signale.error(`自定义配置文件不存在: ${userConfigPath}`)
|
|
@@ -230,8 +447,7 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
|
230
447
|
signale.success(`开发服务器已启动: http://localhost:${port}`)
|
|
231
448
|
|
|
232
449
|
if (shouldOpen) {
|
|
233
|
-
|
|
234
|
-
openBrowser(url)
|
|
450
|
+
openBrowser(`http://localhost:${port}`)
|
|
235
451
|
}
|
|
236
452
|
})
|
|
237
453
|
|
|
@@ -243,45 +459,45 @@ function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
|
243
459
|
})
|
|
244
460
|
}
|
|
245
461
|
|
|
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
|
-
}
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// 工具函数
|
|
464
|
+
// ============================================================================
|
|
259
465
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
})
|
|
466
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
467
|
+
let currentDir = startDir
|
|
468
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
469
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
470
|
+
return currentDir
|
|
471
|
+
}
|
|
472
|
+
currentDir = path.dirname(currentDir)
|
|
473
|
+
}
|
|
474
|
+
return null
|
|
475
|
+
}
|
|
271
476
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
477
|
+
function detectDevScript(pkg, scriptName) {
|
|
478
|
+
const scripts = pkg.scripts || {}
|
|
479
|
+
const candidates = [scriptName, 'dev', 'dev:android', 'build:dev']
|
|
480
|
+
for (const name of candidates) {
|
|
481
|
+
if (name && scripts[name]) {
|
|
482
|
+
return { name, script: scripts[name] }
|
|
276
483
|
}
|
|
484
|
+
}
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
277
487
|
|
|
278
|
-
|
|
279
|
-
|
|
488
|
+
function checkOpenSSLVersion(version) {
|
|
489
|
+
if (!version) return false
|
|
490
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
|
|
491
|
+
if (!match) return false
|
|
492
|
+
return parseInt(match[1], 10) >= 3
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function formatSize(bytes) {
|
|
496
|
+
if (bytes < 1024) return bytes + ' B'
|
|
497
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
498
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
280
499
|
}
|
|
281
500
|
|
|
282
|
-
/**
|
|
283
|
-
* 打开浏览器
|
|
284
|
-
*/
|
|
285
501
|
function openBrowser(url) {
|
|
286
502
|
const platform = process.platform
|
|
287
503
|
const command =
|
|
@@ -294,30 +510,119 @@ function openBrowser(url) {
|
|
|
294
510
|
|
|
295
511
|
if (result.code === 0) {
|
|
296
512
|
signale.success('已打开浏览器')
|
|
297
|
-
|
|
513
|
+
} else {
|
|
514
|
+
signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
|
|
298
515
|
}
|
|
516
|
+
}
|
|
299
517
|
|
|
300
|
-
|
|
518
|
+
/**
|
|
519
|
+
* 检测端口是否可用(通过尝试绑定)
|
|
520
|
+
*/
|
|
521
|
+
function checkPortAvailable(net, port) {
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
const tester = net.createServer()
|
|
524
|
+
tester.once('error', (err) => {
|
|
525
|
+
if (err.code === 'EADDRINUSE') {
|
|
526
|
+
resolve(false)
|
|
527
|
+
} else {
|
|
528
|
+
resolve(false)
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
tester.once('listening', () => {
|
|
532
|
+
tester.close()
|
|
533
|
+
resolve(true)
|
|
534
|
+
})
|
|
535
|
+
tester.listen(port)
|
|
536
|
+
})
|
|
301
537
|
}
|
|
302
538
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
539
|
+
/**
|
|
540
|
+
* 获取占用端口的进程信息
|
|
541
|
+
*/
|
|
542
|
+
function getPortProcessInfo(port) {
|
|
543
|
+
try {
|
|
544
|
+
const { execSync } = require('child_process')
|
|
545
|
+
const isWindows = process.platform === 'win32'
|
|
546
|
+
let cmd
|
|
547
|
+
if (isWindows) {
|
|
548
|
+
cmd = `netstat -ano | findstr :${port} | findstr LISTENING`
|
|
549
|
+
} else {
|
|
550
|
+
cmd = `lsof -i :${port} -P -n -t 2>/dev/null`
|
|
308
551
|
}
|
|
309
|
-
|
|
552
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim()
|
|
553
|
+
if (!result) return null
|
|
554
|
+
|
|
555
|
+
if (isWindows) {
|
|
556
|
+
const match = result.match(/LISTENING\s+(\d+)/)
|
|
557
|
+
return match ? { pid: match[1], name: '' } : null
|
|
558
|
+
} else {
|
|
559
|
+
const pid = result.split('\n')[0].trim()
|
|
560
|
+
if (!pid) return null
|
|
561
|
+
let name = ''
|
|
562
|
+
try {
|
|
563
|
+
name = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, { encoding: 'utf-8' }).trim()
|
|
564
|
+
} catch (e) {
|
|
565
|
+
/* ignore */
|
|
566
|
+
}
|
|
567
|
+
return { pid, name: name || 'unknown' }
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return null
|
|
310
571
|
}
|
|
311
|
-
return null
|
|
312
572
|
}
|
|
313
573
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
574
|
+
/**
|
|
575
|
+
* 提示用户是否杀掉占用端口的进程
|
|
576
|
+
*/
|
|
577
|
+
function promptKillPort(port, processInfo) {
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
const readline = require('readline')
|
|
580
|
+
const rl = readline.createInterface({
|
|
581
|
+
input: process.stdin,
|
|
582
|
+
output: process.stdout,
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
const timeout = setTimeout(() => {
|
|
586
|
+
rl.close()
|
|
587
|
+
resolve(false)
|
|
588
|
+
}, 30000)
|
|
589
|
+
|
|
590
|
+
rl.question(` 是否杀掉该进程并继续? [y/N] `, (answer) => {
|
|
591
|
+
clearTimeout(timeout)
|
|
592
|
+
rl.close()
|
|
593
|
+
const confirmed = (answer || '').trim().toLowerCase() === 'y'
|
|
594
|
+
|
|
595
|
+
if (confirmed) {
|
|
596
|
+
if (!processInfo || !processInfo.pid) {
|
|
597
|
+
signale.error('无法获取占用端口的进程 PID,请手动查找并关闭')
|
|
598
|
+
resolve(false)
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const { execSync } = require('child_process')
|
|
603
|
+
const isWindows = process.platform === 'win32'
|
|
604
|
+
if (isWindows) {
|
|
605
|
+
execSync(`taskkill /PID ${processInfo.pid} /F`, { encoding: 'utf-8' })
|
|
606
|
+
} else {
|
|
607
|
+
execSync(`kill -9 ${processInfo.pid}`, { encoding: 'utf-8' })
|
|
608
|
+
}
|
|
609
|
+
signale.success(`已杀掉进程 ${processInfo.pid}`)
|
|
610
|
+
setTimeout(() => resolve(true), 500)
|
|
611
|
+
} catch (e) {
|
|
612
|
+
signale.error(`杀掉进程失败: ${e.message}`)
|
|
613
|
+
resolve(false)
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
resolve(false)
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
})
|
|
319
620
|
}
|
|
320
621
|
|
|
622
|
+
// ============================================================================
|
|
623
|
+
// 启动
|
|
624
|
+
// ============================================================================
|
|
625
|
+
|
|
321
626
|
main().catch((err) => {
|
|
322
627
|
signale.error('启动失败:', err)
|
|
323
628
|
process.exit(1)
|