@quicktvui/web-cli 1.0.0-beta.39 → 1.0.0-beta.40
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/bin/qt-web-cli-watch.js +202 -0
- package/bin/qt-web-cli.js +26 -62
- package/lib/webpack.config.js +0 -3
- package/package.json +5 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QuickTVUI Web CLI Watcher
|
|
5
|
+
* 文件监听器 - 监听文件变化并重启开发服务器
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const { spawn } = require('child_process')
|
|
11
|
+
const signale = require('signale')
|
|
12
|
+
|
|
13
|
+
// 查找项目根目录
|
|
14
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
15
|
+
let currentDir = startDir
|
|
16
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
17
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
18
|
+
return currentDir
|
|
19
|
+
}
|
|
20
|
+
currentDir = path.dirname(currentDir)
|
|
21
|
+
}
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const projectRoot = findProjectRoot()
|
|
26
|
+
if (!projectRoot) {
|
|
27
|
+
signale.error('无法找到项目根目录')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let childProcess = null
|
|
32
|
+
let isRestarting = false
|
|
33
|
+
let isFirstStart = true
|
|
34
|
+
|
|
35
|
+
function startDevServer() {
|
|
36
|
+
const cliPath = path.join(__dirname, 'qt-web-cli.js')
|
|
37
|
+
const args = [
|
|
38
|
+
cliPath,
|
|
39
|
+
...process.argv.slice(2).filter((arg) => arg !== '--watch' && arg !== '-w'),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
signale.info(`启动开发服务器: node ${args.join(' ')}`)
|
|
43
|
+
|
|
44
|
+
// 只在第一次启动时打开浏览器
|
|
45
|
+
const env = { ...process.env }
|
|
46
|
+
if (!isFirstStart) {
|
|
47
|
+
env.QUICKTVUI_NO_OPEN = 'true'
|
|
48
|
+
}
|
|
49
|
+
isFirstStart = false
|
|
50
|
+
|
|
51
|
+
childProcess = spawn('node', args, {
|
|
52
|
+
cwd: projectRoot,
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
env,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
childProcess.on('error', (err) => {
|
|
58
|
+
signale.error(`启动失败: ${err.message}`)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
childProcess.on('exit', (code, signal) => {
|
|
63
|
+
if (!isRestarting && code !== 0) {
|
|
64
|
+
signale.error(`开发服务器异常退出 (code: ${code}, signal: ${signal})`)
|
|
65
|
+
process.exit(code || 1)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stopDevServer() {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
if (childProcess) {
|
|
73
|
+
signale.info('正在停止开发服务器...')
|
|
74
|
+
|
|
75
|
+
let resolved = false
|
|
76
|
+
|
|
77
|
+
childProcess.on('exit', () => {
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
resolved = true
|
|
80
|
+
signale.success('开发服务器已停止')
|
|
81
|
+
resolve()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 先发送 SIGTERM 信号优雅退出
|
|
86
|
+
childProcess.kill('SIGTERM')
|
|
87
|
+
|
|
88
|
+
// 如果 10 秒后还没退出,强制杀死
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (childProcess && !resolved) {
|
|
91
|
+
signale.warn('服务器未能在 10 秒内退出,强制终止...')
|
|
92
|
+
childProcess.kill('SIGKILL')
|
|
93
|
+
}
|
|
94
|
+
}, 10000)
|
|
95
|
+
} else {
|
|
96
|
+
resolve()
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function restartDevServer() {
|
|
102
|
+
if (isRestarting) return
|
|
103
|
+
isRestarting = true
|
|
104
|
+
|
|
105
|
+
signale.pending('检测到文件变化,正在重启开发服务器...')
|
|
106
|
+
|
|
107
|
+
// 清理 dist 目录
|
|
108
|
+
const distPath = path.join(projectRoot, 'dist/web')
|
|
109
|
+
if (fs.existsSync(distPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const rimraf = require('rimraf')
|
|
112
|
+
rimraf.sync(distPath)
|
|
113
|
+
signale.info('已清理 dist/web 目录')
|
|
114
|
+
} catch (e) {
|
|
115
|
+
signale.warn('清理 dist 目录失败:', e.message)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await stopDevServer()
|
|
120
|
+
|
|
121
|
+
// 等待 3 秒确保端口完全释放
|
|
122
|
+
signale.info('等待端口释放...')
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
124
|
+
|
|
125
|
+
// 重新启动
|
|
126
|
+
startDevServer()
|
|
127
|
+
|
|
128
|
+
// 等待 5 秒后再允许下一次重启
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
isRestarting = false
|
|
131
|
+
}, 5000)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setupWatcher() {
|
|
135
|
+
const chokidar = require('chokidar')
|
|
136
|
+
|
|
137
|
+
// 监听的目录
|
|
138
|
+
const watchPaths = [
|
|
139
|
+
path.join(projectRoot, 'src'),
|
|
140
|
+
path.join(projectRoot, 'packages/quicktvui'),
|
|
141
|
+
path.join(projectRoot, 'packages/component'),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
signale.info(`监听目录: ${watchPaths.map((p) => path.relative(projectRoot, p)).join(', ')}`)
|
|
145
|
+
|
|
146
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
147
|
+
ignored: [/node_modules/, /\.git/, /dist/, /\.DS_Store/, /\.log$/, /\?.*$/, /\.map$/],
|
|
148
|
+
persistent: true,
|
|
149
|
+
ignoreInitial: true,
|
|
150
|
+
usePolling: true,
|
|
151
|
+
interval: 100,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
watcher
|
|
155
|
+
.on('ready', () => {
|
|
156
|
+
signale.success('文件监听器已就绪')
|
|
157
|
+
})
|
|
158
|
+
.on('change', (filePath) => {
|
|
159
|
+
const relativePath = path.relative(projectRoot, filePath)
|
|
160
|
+
signale.info(`文件已修改: ${relativePath}`)
|
|
161
|
+
restartDevServer()
|
|
162
|
+
})
|
|
163
|
+
.on('add', (filePath) => {
|
|
164
|
+
const relativePath = path.relative(projectRoot, filePath)
|
|
165
|
+
signale.info(`文件已添加: ${relativePath}`)
|
|
166
|
+
restartDevServer()
|
|
167
|
+
})
|
|
168
|
+
.on('unlink', (filePath) => {
|
|
169
|
+
const relativePath = path.relative(projectRoot, filePath)
|
|
170
|
+
signale.info(`文件已删除: ${relativePath}`)
|
|
171
|
+
restartDevServer()
|
|
172
|
+
})
|
|
173
|
+
.on('error', (error) => {
|
|
174
|
+
signale.error(`文件监听错误: ${error}`)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
signale.success('文件监听已启动 (--watch 模式)')
|
|
178
|
+
|
|
179
|
+
// 处理退出信号
|
|
180
|
+
process.on('SIGINT', async () => {
|
|
181
|
+
signale.info('正在关闭...')
|
|
182
|
+
await stopDevServer()
|
|
183
|
+
watcher.close()
|
|
184
|
+
process.exit(0)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
process.on('SIGTERM', async () => {
|
|
188
|
+
await stopDevServer()
|
|
189
|
+
watcher.close()
|
|
190
|
+
process.exit(0)
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 主流程
|
|
195
|
+
signale.pending('正在启动 QuickTVUI Web 开发服务器 (watch 模式)...')
|
|
196
|
+
signale.info(`项目根目录: ${projectRoot}`)
|
|
197
|
+
|
|
198
|
+
// 启动开发服务器
|
|
199
|
+
startDevServer()
|
|
200
|
+
|
|
201
|
+
// 设置文件监听
|
|
202
|
+
setupWatcher()
|
package/bin/qt-web-cli.js
CHANGED
|
@@ -14,16 +14,18 @@ const { exec } = require('shelljs')
|
|
|
14
14
|
// 解析命令行参数
|
|
15
15
|
const args = minimist(process.argv.slice(2), {
|
|
16
16
|
string: ['port', 'config'],
|
|
17
|
-
boolean: ['help', 'open', 'v', 'info', 'version'],
|
|
17
|
+
boolean: ['help', 'open', 'v', 'info', 'version', 'watch'],
|
|
18
18
|
alias: {
|
|
19
19
|
p: 'port',
|
|
20
20
|
c: 'config',
|
|
21
21
|
h: 'help',
|
|
22
22
|
o: 'open',
|
|
23
|
+
w: 'watch',
|
|
23
24
|
},
|
|
24
25
|
default: {
|
|
25
26
|
port: 39001,
|
|
26
|
-
open: true,
|
|
27
|
+
open: process.env.QUICKTVUI_NO_OPEN !== 'true',
|
|
28
|
+
watch: false,
|
|
27
29
|
},
|
|
28
30
|
})
|
|
29
31
|
|
|
@@ -77,6 +79,7 @@ Options:
|
|
|
77
79
|
-p, --port <port> 开发服务器端口 (默认: 39001)
|
|
78
80
|
-c, --config <path> 自定义 webpack 配置文件路径
|
|
79
81
|
-o, --open 自动打开浏览器
|
|
82
|
+
-w, --watch 监听文件变化自动重启服务器
|
|
80
83
|
-h, --help 显示帮助信息
|
|
81
84
|
-v, --version 显示 CLI 版本
|
|
82
85
|
--info 显示 CLI 和 web-renderer 版本
|
|
@@ -85,17 +88,38 @@ Examples:
|
|
|
85
88
|
qt-web-cli
|
|
86
89
|
qt-web-cli --port 8080
|
|
87
90
|
qt-web-cli --config ./my.webpack.js
|
|
91
|
+
qt-web-cli --watch
|
|
88
92
|
qt-web-cli -v
|
|
89
93
|
qt-web-cli --info
|
|
90
94
|
|
|
91
95
|
特点:
|
|
92
96
|
- 自动检测项目入口 (main-native.ts/js, main.ts/js)
|
|
93
97
|
- 零配置启动 web 开发服务器
|
|
98
|
+
- 支持文件变化自动重启 (--watch)
|
|
94
99
|
`)
|
|
95
100
|
process.exit(0)
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
async function main() {
|
|
104
|
+
// 如果启用了 watch 模式,使用 watcher 脚本
|
|
105
|
+
if (args.watch) {
|
|
106
|
+
const { spawn } = require('child_process')
|
|
107
|
+
const watcherPath = path.join(__dirname, 'qt-web-cli-watch.js')
|
|
108
|
+
const watcherArgs = process.argv.slice(2).filter((arg) => arg !== '--watch' && arg !== '-w')
|
|
109
|
+
|
|
110
|
+
const watcher = spawn('node', [watcherPath, ...watcherArgs], {
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
stdio: 'inherit',
|
|
113
|
+
env: process.env,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
watcher.on('exit', (code) => {
|
|
117
|
+
process.exit(code || 0)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
signale.pending('正在启动 QuickTVUI Web 开发服务器...')
|
|
100
124
|
|
|
101
125
|
// 查找项目根目录
|
|
@@ -119,9 +143,6 @@ async function main() {
|
|
|
119
143
|
|
|
120
144
|
// 入口候选列表
|
|
121
145
|
const entryCandidates = [
|
|
122
|
-
{ name: 'webMain (package.json)', path: pkg.webMain, type: 'web' },
|
|
123
|
-
{ name: 'src/main-web.ts', path: './src/main-web.ts', type: 'web' },
|
|
124
|
-
{ name: 'src/main-web.js', path: './src/main-web.js', type: 'web' },
|
|
125
146
|
{ name: 'src/main-native.ts', path: './src/main-native.ts', type: 'package' },
|
|
126
147
|
{ name: 'src/main-native.js', path: './src/main-native.js', type: 'package' },
|
|
127
148
|
{ name: 'src/main.ts', path: './src/main.ts', type: 'package' },
|
|
@@ -142,8 +163,6 @@ async function main() {
|
|
|
142
163
|
|
|
143
164
|
if (!mainEntry) {
|
|
144
165
|
signale.error('无法找到有效的入口文件,请检查以下路径:')
|
|
145
|
-
signale.error(' - package.json 中的 webMain 字段')
|
|
146
|
-
signale.error(' - src/main-web.ts 或 src/main-web.js')
|
|
147
166
|
signale.error(' - src/main-native.ts 或 src/main-native.js')
|
|
148
167
|
signale.error(' - src/main.ts 或 src/main.js')
|
|
149
168
|
signale.error(' - package.json 中的 main 字段')
|
|
@@ -257,10 +276,6 @@ function waitForServer(url, timeout = 30000) {
|
|
|
257
276
|
*/
|
|
258
277
|
function openBrowser(url) {
|
|
259
278
|
const platform = process.platform
|
|
260
|
-
if (platform === 'darwin' && focusExistingBrowserPage(url)) {
|
|
261
|
-
signale.success('已切换到已打开的浏览器页面')
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
279
|
const command =
|
|
265
280
|
platform === 'darwin'
|
|
266
281
|
? `open "${url}"`
|
|
@@ -277,57 +292,6 @@ function openBrowser(url) {
|
|
|
277
292
|
signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
|
|
278
293
|
}
|
|
279
294
|
|
|
280
|
-
function focusExistingBrowserPage(url) {
|
|
281
|
-
const match = /^https?:\/\/[^/]+/.exec(url)
|
|
282
|
-
const targetPrefix = match ? match[0] : url
|
|
283
|
-
const browserApps = ['Google Chrome', 'Safari', 'Arc', 'Microsoft Edge', 'Brave Browser']
|
|
284
|
-
const script = `
|
|
285
|
-
set targetPrefix to "${escapeAppleScriptString(targetPrefix)}"
|
|
286
|
-
set browserApps to {${browserApps.map((app) => `"${escapeAppleScriptString(app)}"`).join(', ')}}
|
|
287
|
-
repeat with appName in browserApps
|
|
288
|
-
tell application "System Events"
|
|
289
|
-
set isRunning to (name of processes) contains (appName as text)
|
|
290
|
-
end tell
|
|
291
|
-
if isRunning then
|
|
292
|
-
if (appName as text) is "Safari" then
|
|
293
|
-
tell application "Safari"
|
|
294
|
-
repeat with w in windows
|
|
295
|
-
repeat with t in tabs of w
|
|
296
|
-
if URL of t starts with targetPrefix then
|
|
297
|
-
set current tab of w to t
|
|
298
|
-
set index of w to 1
|
|
299
|
-
activate
|
|
300
|
-
return "FOUND"
|
|
301
|
-
end if
|
|
302
|
-
end repeat
|
|
303
|
-
end repeat
|
|
304
|
-
end tell
|
|
305
|
-
else
|
|
306
|
-
tell application (appName as text)
|
|
307
|
-
repeat with w in windows
|
|
308
|
-
repeat with t in tabs of w
|
|
309
|
-
if URL of t starts with targetPrefix then
|
|
310
|
-
set active tab index of w to (index of t)
|
|
311
|
-
set index of w to 1
|
|
312
|
-
activate
|
|
313
|
-
return "FOUND"
|
|
314
|
-
end if
|
|
315
|
-
end repeat
|
|
316
|
-
end repeat
|
|
317
|
-
end tell
|
|
318
|
-
end if
|
|
319
|
-
end if
|
|
320
|
-
end repeat
|
|
321
|
-
return "NOT_FOUND"
|
|
322
|
-
`.trim()
|
|
323
|
-
const result = exec(`osascript <<'APPLESCRIPT'\n${script}\nAPPLESCRIPT`, { silent: true })
|
|
324
|
-
return result.code === 0 && String(result.stdout || '').includes('FOUND')
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function escapeAppleScriptString(value) {
|
|
328
|
-
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
329
|
-
}
|
|
330
|
-
|
|
331
295
|
function findProjectRoot(startDir = process.cwd()) {
|
|
332
296
|
let currentDir = startDir
|
|
333
297
|
while (currentDir !== path.dirname(currentDir)) {
|
package/lib/webpack.config.js
CHANGED
|
@@ -24,9 +24,6 @@ try {
|
|
|
24
24
|
function detectAlias() {
|
|
25
25
|
const alias = {
|
|
26
26
|
'@': path.resolve(projectRoot, 'src'),
|
|
27
|
-
// 使用本地打包的 es3-router
|
|
28
|
-
'@extscreen/es3-router':
|
|
29
|
-
'/Volumes/WD/Users/chendd/Documents/huan/es/es-vue3/packages/ESRouter/dist/index.js',
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
const jsconfigPath = path.join(projectRoot, 'jsconfig.json')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quicktvui/web-cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.40",
|
|
4
4
|
"description": "CLI tool for QuickTVUI web development - zero configuration",
|
|
5
5
|
"author": "QuickTVUI Team",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"@babel/core": "^7.23.0",
|
|
35
35
|
"babel-loader": "^9.1.0",
|
|
36
36
|
"ts-loader": "^9.4.0",
|
|
37
|
-
"regenerator-runtime": "^0.14.0"
|
|
37
|
+
"regenerator-runtime": "^0.14.0",
|
|
38
|
+
"chokidar": "^3.5.3",
|
|
39
|
+
"rimraf": "^5.0.0"
|
|
38
40
|
},
|
|
39
41
|
"peerDependenciesMeta": {
|
|
40
42
|
"less-loader": {
|
|
@@ -53,4 +55,4 @@
|
|
|
53
55
|
"engines": {
|
|
54
56
|
"node": ">=16.0.0"
|
|
55
57
|
}
|
|
56
|
-
}
|
|
58
|
+
}
|