@quicktvui/web-cli 1.0.0-beta.9 → 1.0.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/bin/qt-web-cli-watch.js +202 -0
- package/bin/qt-web-cli.js +324 -0
- package/lib/entry-package.js +58 -17
- package/lib/webpack.config.js +89 -52
- package/package.json +28 -18
- package/templates/web-renderer.html +11 -0
- package/bin/qt-web-dev.js +0 -160
|
@@ -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()
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QuickTVUI Web Dev CLI
|
|
5
|
+
* 启动 web 开发服务器,无需手动配置入口文件
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const signale = require('signale')
|
|
11
|
+
const minimist = require('minimist')
|
|
12
|
+
const { exec } = require('shelljs')
|
|
13
|
+
|
|
14
|
+
// 解析命令行参数
|
|
15
|
+
const args = minimist(process.argv.slice(2), {
|
|
16
|
+
string: ['port', 'config'],
|
|
17
|
+
boolean: ['help', 'open', 'v', 'info', 'version', 'watch'],
|
|
18
|
+
alias: {
|
|
19
|
+
p: 'port',
|
|
20
|
+
c: 'config',
|
|
21
|
+
h: 'help',
|
|
22
|
+
o: 'open',
|
|
23
|
+
w: 'watch',
|
|
24
|
+
},
|
|
25
|
+
default: {
|
|
26
|
+
port: 39001,
|
|
27
|
+
open: process.env.QUICKTVUI_NO_OPEN !== 'true',
|
|
28
|
+
watch: false,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// 显示版本信息
|
|
33
|
+
if (args.v || args.version) {
|
|
34
|
+
const cliPkg = require(path.join(__dirname, '../package.json'))
|
|
35
|
+
console.log(`@quicktvui/web-cli v${cliPkg.version}`)
|
|
36
|
+
process.exit(0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 显示详细信息(包括 web-renderer 版本)
|
|
40
|
+
if (args.info) {
|
|
41
|
+
const cliPkg = require(path.join(__dirname, '../package.json'))
|
|
42
|
+
console.log(`@quicktvui/web-cli v${cliPkg.version}`)
|
|
43
|
+
try {
|
|
44
|
+
// 尝试多种路径查找 web-renderer
|
|
45
|
+
let rendererPkg
|
|
46
|
+
const possiblePaths = [
|
|
47
|
+
'@quicktvui/web-renderer/package.json',
|
|
48
|
+
path.join(__dirname, '../../web-renderer/package.json'),
|
|
49
|
+
path.join(__dirname, '../../../web-renderer/package.json'),
|
|
50
|
+
]
|
|
51
|
+
for (const pkgPath of possiblePaths) {
|
|
52
|
+
try {
|
|
53
|
+
rendererPkg = require(pkgPath)
|
|
54
|
+
break
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// 继续尝试下一个路径
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (rendererPkg) {
|
|
60
|
+
console.log(`@quicktvui/web-renderer v${rendererPkg.version}`)
|
|
61
|
+
} else {
|
|
62
|
+
console.log('@quicktvui/web-renderer (not installed)')
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log('@quicktvui/web-renderer (not installed)')
|
|
66
|
+
}
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 显示帮助信息
|
|
71
|
+
if (args.help) {
|
|
72
|
+
console.log(`
|
|
73
|
+
QuickTVUI Web CLI
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
qt-web-cli [options]
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
-p, --port <port> 开发服务器端口 (默认: 39001)
|
|
80
|
+
-c, --config <path> 自定义 webpack 配置文件路径
|
|
81
|
+
-o, --open 自动打开浏览器
|
|
82
|
+
-w, --watch 监听文件变化自动重启服务器
|
|
83
|
+
-h, --help 显示帮助信息
|
|
84
|
+
-v, --version 显示 CLI 版本
|
|
85
|
+
--info 显示 CLI 和 web-renderer 版本
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
qt-web-cli
|
|
89
|
+
qt-web-cli --port 8080
|
|
90
|
+
qt-web-cli --config ./my.webpack.js
|
|
91
|
+
qt-web-cli --watch
|
|
92
|
+
qt-web-cli -v
|
|
93
|
+
qt-web-cli --info
|
|
94
|
+
|
|
95
|
+
特点:
|
|
96
|
+
- 自动检测项目入口 (main-native.ts/js, main.ts/js)
|
|
97
|
+
- 零配置启动 web 开发服务器
|
|
98
|
+
- 支持文件变化自动重启 (--watch)
|
|
99
|
+
`)
|
|
100
|
+
process.exit(0)
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
|
|
123
|
+
signale.pending('正在启动 QuickTVUI Web 开发服务器...')
|
|
124
|
+
|
|
125
|
+
// 查找项目根目录
|
|
126
|
+
const projectRoot = findProjectRoot()
|
|
127
|
+
if (!projectRoot) {
|
|
128
|
+
signale.error('无法找到项目根目录')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
signale.info(`项目根目录: ${projectRoot}`)
|
|
133
|
+
|
|
134
|
+
// 读取 package.json
|
|
135
|
+
let pkg = {}
|
|
136
|
+
try {
|
|
137
|
+
pkg = require(path.join(projectRoot, 'package.json'))
|
|
138
|
+
} catch (e) {}
|
|
139
|
+
|
|
140
|
+
// 检测入口文件
|
|
141
|
+
let mainEntry
|
|
142
|
+
let entryType
|
|
143
|
+
|
|
144
|
+
// 入口候选列表
|
|
145
|
+
const entryCandidates = [
|
|
146
|
+
{ name: 'src/main-native.ts', path: './src/main-native.ts', type: 'package' },
|
|
147
|
+
{ name: 'src/main-native.js', path: './src/main-native.js', type: 'package' },
|
|
148
|
+
{ name: 'src/main.ts', path: './src/main.ts', type: 'package' },
|
|
149
|
+
{ name: 'src/main.js', path: './src/main.js', type: 'package' },
|
|
150
|
+
{ name: 'main (package.json)', path: pkg.main, type: 'package' },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for (const candidate of entryCandidates) {
|
|
154
|
+
if (!candidate.path) continue
|
|
155
|
+
const entryFullPath = path.resolve(projectRoot, candidate.path)
|
|
156
|
+
if (fs.existsSync(entryFullPath)) {
|
|
157
|
+
mainEntry = candidate.path
|
|
158
|
+
entryType = candidate.type
|
|
159
|
+
signale.info(`主入口: ${mainEntry}`)
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
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 字段')
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const mainEntryPath = path.resolve(projectRoot, mainEntry)
|
|
173
|
+
|
|
174
|
+
// 设置环境变量
|
|
175
|
+
process.env.NODE_ENV = 'development'
|
|
176
|
+
process.env.QUICKTVUI_PROJECT_ROOT = projectRoot
|
|
177
|
+
process.env.QUICKTVUI_MAIN_ENTRY = mainEntryPath
|
|
178
|
+
process.env.QUICKTVUI_ENTRY_TYPE = entryType
|
|
179
|
+
process.env.QUICKTVUI_PORT = String(args.port)
|
|
180
|
+
|
|
181
|
+
// 检测 OpenSSL 版本
|
|
182
|
+
const needLegacyProvider = checkOpenSSLVersion(process.versions.openssl)
|
|
183
|
+
if (needLegacyProvider) {
|
|
184
|
+
signale.warn(`检测到 OpenSSL ${process.versions.openssl},已添加 --openssl-legacy-provider`)
|
|
185
|
+
process.env.NODE_OPTIONS = '--openssl-legacy-provider'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
signale.pending(`启动开发服务器: http://localhost:${args.port}`)
|
|
189
|
+
|
|
190
|
+
// 使用 webpack Node API 启动开发服务器
|
|
191
|
+
const defaultConfigPath = path.resolve(__dirname, '../lib/webpack.config.js')
|
|
192
|
+
const userConfigPath = args.config ? path.resolve(projectRoot, args.config) : null
|
|
193
|
+
startDevServer(defaultConfigPath, args.port, args.open, userConfigPath)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 使用 webpack Node API 启动开发服务器
|
|
198
|
+
*/
|
|
199
|
+
function startDevServer(configPath, port, shouldOpen, userConfigPath) {
|
|
200
|
+
const webpack = require('webpack')
|
|
201
|
+
const WebpackDevServer = require('webpack-dev-server')
|
|
202
|
+
const { merge } = require('webpack-merge')
|
|
203
|
+
|
|
204
|
+
// 加载内置配置
|
|
205
|
+
let config = require(configPath)
|
|
206
|
+
|
|
207
|
+
// 如果用户指定了自定义配置,进行合并
|
|
208
|
+
if (userConfigPath) {
|
|
209
|
+
if (!fs.existsSync(userConfigPath)) {
|
|
210
|
+
signale.error(`自定义配置文件不存在: ${userConfigPath}`)
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
213
|
+
signale.info(`加载用户配置: ${userConfigPath}`)
|
|
214
|
+
const userConfig = require(userConfigPath)
|
|
215
|
+
config = merge(config, userConfig)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
config.devServer = config.devServer || {}
|
|
219
|
+
config.devServer.port = port
|
|
220
|
+
config.devServer.open = false
|
|
221
|
+
|
|
222
|
+
const compiler = webpack(config)
|
|
223
|
+
const server = new WebpackDevServer(config.devServer, compiler)
|
|
224
|
+
|
|
225
|
+
server.startCallback((err) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
signale.error('启动开发服务器失败:', err.message)
|
|
228
|
+
process.exit(1)
|
|
229
|
+
}
|
|
230
|
+
signale.success(`开发服务器已启动: http://localhost:${port}`)
|
|
231
|
+
|
|
232
|
+
if (shouldOpen) {
|
|
233
|
+
const url = `http://localhost:${port}`
|
|
234
|
+
openBrowser(url)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
process.on('SIGINT', () => {
|
|
239
|
+
server.stopCallback(() => {
|
|
240
|
+
signale.info('开发服务器已停止')
|
|
241
|
+
process.exit(0)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
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
|
+
}
|
|
259
|
+
|
|
260
|
+
const req = http.get(url, (res) => {
|
|
261
|
+
if (res.statusCode === 200 || res.statusCode === 304) {
|
|
262
|
+
resolve()
|
|
263
|
+
} else {
|
|
264
|
+
setTimeout(check, 500)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
req.on('error', () => {
|
|
269
|
+
setTimeout(check, 500)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
req.setTimeout(1000, () => {
|
|
273
|
+
req.destroy()
|
|
274
|
+
setTimeout(check, 500)
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
check()
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 打开浏览器
|
|
284
|
+
*/
|
|
285
|
+
function openBrowser(url) {
|
|
286
|
+
const platform = process.platform
|
|
287
|
+
const command =
|
|
288
|
+
platform === 'darwin'
|
|
289
|
+
? `open "${url}"`
|
|
290
|
+
: platform === 'win32'
|
|
291
|
+
? `cmd /c start "" "${url}"`
|
|
292
|
+
: `xdg-open "${url}"`
|
|
293
|
+
const result = exec(command, { silent: true })
|
|
294
|
+
|
|
295
|
+
if (result.code === 0) {
|
|
296
|
+
signale.success('已打开浏览器')
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
304
|
+
let currentDir = startDir
|
|
305
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
306
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
307
|
+
return currentDir
|
|
308
|
+
}
|
|
309
|
+
currentDir = path.dirname(currentDir)
|
|
310
|
+
}
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function checkOpenSSLVersion(version) {
|
|
315
|
+
if (!version) return false
|
|
316
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
|
|
317
|
+
if (!match) return false
|
|
318
|
+
return parseInt(match[1], 10) >= 3
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main().catch((err) => {
|
|
322
|
+
signale.error('启动失败:', err)
|
|
323
|
+
process.exit(1)
|
|
324
|
+
})
|
package/lib/entry-package.js
CHANGED
|
@@ -1,24 +1,65 @@
|
|
|
1
|
-
// @quicktvui/web-cli - web entry
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// 初始化流程参考 main-web.js
|
|
1
|
+
// @quicktvui/web-cli - web entry
|
|
2
|
+
// 通过 @quicktvui/web-renderer 导入所有依赖
|
|
3
|
+
// 动态加载主入口,确保初始化顺序正确
|
|
6
4
|
|
|
7
5
|
console.log('[Web Renderer] === Starting initialization ===')
|
|
8
6
|
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
initAsyncLocalStorage,
|
|
9
|
+
initAutoProxy,
|
|
10
|
+
setupSceneBuilder,
|
|
11
|
+
createWebEngine,
|
|
12
|
+
applyAllPatches,
|
|
13
|
+
TVFocusManager,
|
|
14
|
+
startWebEngine,
|
|
15
|
+
APP_NAME,
|
|
16
|
+
IJKPlayerComponent,
|
|
17
|
+
} from '@quicktvui/web-renderer'
|
|
10
18
|
|
|
11
|
-
//
|
|
12
|
-
|
|
19
|
+
// 初始化 async localStorage
|
|
20
|
+
initAsyncLocalStorage()
|
|
13
21
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
// 初始化自动代理
|
|
23
|
+
initAutoProxy()
|
|
24
|
+
|
|
25
|
+
// 注册 IJKPlayerComponent
|
|
26
|
+
global.__WEB_COMPONENTS__ = global.__WEB_COMPONENTS__ || {}
|
|
27
|
+
global.__WEB_COMPONENTS__['IJKPlayerComponent'] = IJKPlayerComponent
|
|
28
|
+
|
|
29
|
+
// 设置 SceneBuilder
|
|
30
|
+
setupSceneBuilder()
|
|
31
|
+
|
|
32
|
+
// 创建 Web 引擎
|
|
33
|
+
const engine = createWebEngine()
|
|
17
34
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
startWebRenderer()
|
|
35
|
+
// 应用所有补丁
|
|
36
|
+
applyAllPatches(engine)
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
// 初始化 TV 焦点管理器
|
|
39
|
+
global.__TV_FOCUS_MANAGER__ = new TVFocusManager()
|
|
40
|
+
console.log('[Web Renderer] TVFocusManager initialized')
|
|
41
|
+
|
|
42
|
+
// 注入全局 CSS
|
|
43
|
+
const styleEl = document.createElement('style')
|
|
44
|
+
styleEl.id = 'web-platform-reset'
|
|
45
|
+
styleEl.textContent = `
|
|
46
|
+
#app, #app * { align-items: flex-start; }
|
|
47
|
+
[style*="align-items"] { align-items: var(--align-items, center) !important; }
|
|
48
|
+
`
|
|
49
|
+
document.head.appendChild(styleEl)
|
|
50
|
+
console.log('[Web Renderer] Global CSS reset injected')
|
|
51
|
+
|
|
52
|
+
// 动态加载主入口,等待加载完成后再启动引擎
|
|
53
|
+
console.log('[Web Renderer] Importing main module...')
|
|
54
|
+
import(__QUICKTVUI_MAIN_ENTRY__)
|
|
55
|
+
.then(() => {
|
|
56
|
+
console.log('[Web Renderer] Main module loaded')
|
|
57
|
+
console.log('[Web Renderer] Starting engine...')
|
|
58
|
+
startWebEngine(engine, APP_NAME)
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
console.log('[Web Renderer] === Initialization complete ===')
|
|
61
|
+
}, 1000)
|
|
62
|
+
})
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
console.error('[Web Renderer] Failed to load main module:', err)
|
|
65
|
+
})
|
package/lib/webpack.config.js
CHANGED
|
@@ -9,8 +9,8 @@ const fs = require('fs')
|
|
|
9
9
|
// 从环境变量获取配置
|
|
10
10
|
const projectRoot = process.env.QUICKTVUI_PROJECT_ROOT || process.cwd()
|
|
11
11
|
const mainEntry = process.env.QUICKTVUI_MAIN_ENTRY || path.resolve(projectRoot, 'src/main.ts')
|
|
12
|
-
const entryType = process.env.QUICKTVUI_ENTRY_TYPE || 'package'
|
|
13
12
|
const port = parseInt(process.env.QUICKTVUI_PORT || '39001', 10)
|
|
13
|
+
const watchPoll = parseInt(process.env.QUICKTVUI_WATCH_POLL || '1000', 10)
|
|
14
14
|
|
|
15
15
|
// 尝试加载项目的 package.json
|
|
16
16
|
let pkg = {}
|
|
@@ -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')
|
|
@@ -67,6 +64,20 @@ function detectModules() {
|
|
|
67
64
|
if (fs.existsSync(cwdNodeModules) && !modules.includes(cwdNodeModules)) {
|
|
68
65
|
modules.push(cwdNodeModules)
|
|
69
66
|
}
|
|
67
|
+
// 添加 web-cli 自身的 node_modules 路径(支持全局安装)
|
|
68
|
+
const cliNodeModules = path.resolve(__dirname, '../node_modules')
|
|
69
|
+
if (fs.existsSync(cliNodeModules) && !modules.includes(cliNodeModules)) {
|
|
70
|
+
modules.push(cliNodeModules)
|
|
71
|
+
}
|
|
72
|
+
// 添加全局 node_modules 路径
|
|
73
|
+
try {
|
|
74
|
+
const globalNodeModules = require('child_process')
|
|
75
|
+
.execSync('npm root -g', { encoding: 'utf-8' })
|
|
76
|
+
.trim()
|
|
77
|
+
if (fs.existsSync(globalNodeModules) && !modules.includes(globalNodeModules)) {
|
|
78
|
+
modules.push(globalNodeModules)
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {}
|
|
70
81
|
return modules
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -85,23 +96,51 @@ function detectCSSLoader() {
|
|
|
85
96
|
return 'style-loader'
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
/**
|
|
100
|
+
* 检测全局 Sass 变量文件
|
|
101
|
+
* 注意:返回绝对路径,避免 sass 相对于当前文件解析路径的问题
|
|
102
|
+
*/
|
|
103
|
+
function detectSassAdditionalData() {
|
|
104
|
+
// 常见的全局变量文件路径
|
|
105
|
+
const possiblePaths = [
|
|
106
|
+
'src/styles/variables.scss',
|
|
107
|
+
'src/styles/variables.sass',
|
|
108
|
+
'src/styles/_variables.scss',
|
|
109
|
+
'src/styles/_variables.sass',
|
|
110
|
+
'src/assets/styles/variables.scss',
|
|
111
|
+
'src/assets/styles/variables.sass',
|
|
112
|
+
'src/assets/scss/variables.scss',
|
|
113
|
+
'src/assets/scss/variables.sass',
|
|
114
|
+
'src/assets/scss/_variables.scss',
|
|
115
|
+
'src/assets/scss/_variables.sass',
|
|
116
|
+
'src/scss/variables.scss',
|
|
117
|
+
'src/scss/variables.sass',
|
|
118
|
+
'src/scss/_variables.scss',
|
|
119
|
+
'src/scss/_variables.sass',
|
|
120
|
+
'src/css/variables.scss',
|
|
121
|
+
'src/css/variables.sass',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
for (const relativePath of possiblePaths) {
|
|
125
|
+
const fullPath = path.join(projectRoot, relativePath)
|
|
126
|
+
if (fs.existsSync(fullPath)) {
|
|
127
|
+
// 使用绝对路径,避免 sass 相对于当前编译文件解析路径的问题
|
|
128
|
+
return `@import "${fullPath}";\n`
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return ''
|
|
132
|
+
}
|
|
133
|
+
|
|
88
134
|
const cssLoader = detectCSSLoader()
|
|
135
|
+
const sassAdditionalData = detectSassAdditionalData()
|
|
89
136
|
|
|
90
|
-
//
|
|
91
|
-
|
|
137
|
+
// 获取 web-cli 目录下的 loader 路径
|
|
138
|
+
const cliDir = path.resolve(__dirname, '..')
|
|
139
|
+
const resolveLoader = (loaderName) => require.resolve(loaderName, { paths: [cliDir] })
|
|
92
140
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} else if (entryType === 'local') {
|
|
97
|
-
// 使用 entry-local.js + main 入口
|
|
98
|
-
const entryLocalFile = path.resolve(__dirname, 'entry-local.js')
|
|
99
|
-
entryFiles = ['regenerator-runtime/runtime', entryLocalFile, mainEntry]
|
|
100
|
-
} else {
|
|
101
|
-
// 使用 entry-package.js + main 入口
|
|
102
|
-
const entryPackageFile = path.resolve(__dirname, 'entry-package.js')
|
|
103
|
-
entryFiles = ['regenerator-runtime/runtime', entryPackageFile, mainEntry]
|
|
104
|
-
}
|
|
141
|
+
// 入口文件:只加载 entry-package.js,主入口通过动态 import 加载
|
|
142
|
+
const entryPackageFile = path.resolve(__dirname, 'entry-package.js')
|
|
143
|
+
const entryFiles = ['regenerator-runtime/runtime', entryPackageFile]
|
|
105
144
|
|
|
106
145
|
module.exports = {
|
|
107
146
|
mode: 'development',
|
|
@@ -109,8 +148,13 @@ module.exports = {
|
|
|
109
148
|
|
|
110
149
|
devServer: {
|
|
111
150
|
port,
|
|
112
|
-
hot:
|
|
151
|
+
hot: false,
|
|
113
152
|
liveReload: true,
|
|
153
|
+
headers: {
|
|
154
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
155
|
+
Pragma: 'no-cache',
|
|
156
|
+
Expires: '0',
|
|
157
|
+
},
|
|
114
158
|
proxy: [
|
|
115
159
|
{
|
|
116
160
|
context: ['/proxy'],
|
|
@@ -138,10 +182,6 @@ module.exports = {
|
|
|
138
182
|
},
|
|
139
183
|
},
|
|
140
184
|
],
|
|
141
|
-
static: {
|
|
142
|
-
directory: path.join(projectRoot, 'public'),
|
|
143
|
-
publicPath: '/',
|
|
144
|
-
},
|
|
145
185
|
client: {
|
|
146
186
|
overlay: { errors: true, warnings: false },
|
|
147
187
|
},
|
|
@@ -149,7 +189,7 @@ module.exports = {
|
|
|
149
189
|
|
|
150
190
|
watchOptions: {
|
|
151
191
|
aggregateTimeout: 1500,
|
|
152
|
-
poll:
|
|
192
|
+
poll: watchPoll,
|
|
153
193
|
ignored: /node_modules/,
|
|
154
194
|
},
|
|
155
195
|
|
|
@@ -175,28 +215,10 @@ module.exports = {
|
|
|
175
215
|
template: path.resolve(__dirname, '../templates/web-renderer.html'),
|
|
176
216
|
title: pkg.name || 'QuickTVUI Web',
|
|
177
217
|
}),
|
|
178
|
-
//
|
|
218
|
+
// 注入主入口路径和环境变量
|
|
179
219
|
new (require('webpack').DefinePlugin)({
|
|
180
220
|
__QUICKTVUI_PROJECT_ROOT__: JSON.stringify(projectRoot),
|
|
181
221
|
__QUICKTVUI_MAIN_ENTRY__: JSON.stringify(mainEntry),
|
|
182
|
-
// 预计算 src/web 模块路径(浏览器环境无法使用 path 模块)
|
|
183
|
-
__QUICKTVUI_MODULE_ASYNC_LOCAL_STORAGE__: JSON.stringify(
|
|
184
|
-
path.join(projectRoot, 'src/web/core/asyncLocalStorage')
|
|
185
|
-
),
|
|
186
|
-
__QUICKTVUI_MODULE_AUTO_PROXY__: JSON.stringify(
|
|
187
|
-
path.join(projectRoot, 'src/web/core/autoProxy')
|
|
188
|
-
),
|
|
189
|
-
__QUICKTVUI_MODULE_SCENE_BUILDER__: JSON.stringify(
|
|
190
|
-
path.join(projectRoot, 'src/web/core/SceneBuilder')
|
|
191
|
-
),
|
|
192
|
-
__QUICKTVUI_MODULE_WEB_ENGINE__: JSON.stringify(path.join(projectRoot, 'src/web')),
|
|
193
|
-
__QUICKTVUI_MODULE_PATCHES__: JSON.stringify(path.join(projectRoot, 'src/web/core/patches')),
|
|
194
|
-
__QUICKTVUI_MODULE_TV_FOCUS_MANAGER__: JSON.stringify(
|
|
195
|
-
path.join(projectRoot, 'src/web/core/TVFocusManager')
|
|
196
|
-
),
|
|
197
|
-
__QUICKTVUI_MODULE_PAGE_LIFECYCLE__: JSON.stringify(
|
|
198
|
-
path.join(projectRoot, 'src/web/core/pageLifecycle')
|
|
199
|
-
),
|
|
200
222
|
process: JSON.stringify({
|
|
201
223
|
env: { NODE_ENV: 'development' },
|
|
202
224
|
browser: true,
|
|
@@ -218,7 +240,7 @@ module.exports = {
|
|
|
218
240
|
loader: 'vue-loader',
|
|
219
241
|
options: { compilerOptions: { whitespace: 'condense' } },
|
|
220
242
|
},
|
|
221
|
-
'scope-loader',
|
|
243
|
+
require.resolve('scope-loader', { paths: [path.resolve(__dirname, '..')] }),
|
|
222
244
|
],
|
|
223
245
|
},
|
|
224
246
|
// TypeScript 文件
|
|
@@ -254,9 +276,29 @@ module.exports = {
|
|
|
254
276
|
{ test: /\.css$/, use: [cssLoader] },
|
|
255
277
|
// Less
|
|
256
278
|
{ test: /\.less$/, use: [cssLoader, 'less-loader'] },
|
|
257
|
-
// Sass/SCSS
|
|
258
|
-
{
|
|
259
|
-
|
|
279
|
+
// Sass/SCSS - 自动注入全局变量文件
|
|
280
|
+
{
|
|
281
|
+
test: /\.scss$/,
|
|
282
|
+
use: [
|
|
283
|
+
cssLoader,
|
|
284
|
+
{
|
|
285
|
+
loader: 'sass-loader',
|
|
286
|
+
options: sassAdditionalData ? { additionalData: sassAdditionalData } : {},
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
test: /\.sass$/,
|
|
292
|
+
use: [
|
|
293
|
+
cssLoader,
|
|
294
|
+
{
|
|
295
|
+
loader: 'sass-loader',
|
|
296
|
+
options: sassAdditionalData
|
|
297
|
+
? { additionalData: sassAdditionalData, sassOptions: { indentedSyntax: true } }
|
|
298
|
+
: { sassOptions: { indentedSyntax: true } },
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
260
302
|
// 图片
|
|
261
303
|
{
|
|
262
304
|
test: /\.(png|jpe?g|gif|webp|svg)$/i,
|
|
@@ -298,9 +340,4 @@ module.exports = {
|
|
|
298
340
|
|
|
299
341
|
// 忽略 entry-local.js 中动态 require 的警告(这是预期行为)
|
|
300
342
|
ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],
|
|
301
|
-
|
|
302
|
-
cache: {
|
|
303
|
-
type: 'filesystem',
|
|
304
|
-
buildDependencies: { config: [__filename] },
|
|
305
|
-
},
|
|
306
343
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quicktvui/web-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "CLI tool for QuickTVUI web development - zero configuration",
|
|
5
5
|
"author": "QuickTVUI Team",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"bin": {
|
|
8
|
-
"qt-web-
|
|
8
|
+
"qt-web-cli": "./bin/qt-web-cli.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "lib/index.js",
|
|
11
11
|
"files": [
|
|
@@ -24,29 +24,39 @@
|
|
|
24
24
|
"minimist": "^1.2.8",
|
|
25
25
|
"shelljs": "^0.10.0",
|
|
26
26
|
"signale": "^1.4.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
27
|
+
"webpack-merge": "^6.0.0",
|
|
28
|
+
"@quicktvui/web-renderer": "^1.0.9",
|
|
29
|
+
"scope-loader": "^1.0.3",
|
|
30
|
+
"chokidar": "^3.5.3",
|
|
31
|
+
"rimraf": "^5.0.0"
|
|
29
32
|
},
|
|
30
33
|
"peerDependencies": {
|
|
31
|
-
"webpack": "^5.
|
|
32
|
-
"webpack-cli": "^5.
|
|
33
|
-
"webpack-dev-server": "^4.
|
|
34
|
+
"webpack": "^5.89.0",
|
|
35
|
+
"webpack-cli": "^5.1.0",
|
|
36
|
+
"webpack-dev-server": "^4.15.0",
|
|
34
37
|
"vue-loader": "^17.0.0",
|
|
35
|
-
"html-webpack-plugin": "^5.
|
|
36
|
-
"@babel/core": "^7.
|
|
37
|
-
"babel-loader": "^9.
|
|
38
|
+
"html-webpack-plugin": "^5.5.0",
|
|
39
|
+
"@babel/core": "^7.23.0",
|
|
40
|
+
"babel-loader": "^9.1.0",
|
|
38
41
|
"ts-loader": "^9.4.0",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"@extscreen/es3-vue": ">=3.0.0"
|
|
42
|
+
"vue": "^3.0.0",
|
|
43
|
+
"regenerator-runtime": "^0.14.0"
|
|
42
44
|
},
|
|
43
45
|
"peerDependenciesMeta": {
|
|
44
|
-
"less-loader": {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
46
|
+
"less-loader": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"sass-loader": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"sass": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"less": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
48
58
|
},
|
|
49
59
|
"engines": {
|
|
50
60
|
"node": ">=16.0.0"
|
|
51
61
|
}
|
|
52
|
-
}
|
|
62
|
+
}
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
height: 1080px !important;
|
|
51
51
|
transform-origin: top left;
|
|
52
52
|
background-color: #26292F;
|
|
53
|
+
visibility: hidden;
|
|
53
54
|
}
|
|
54
55
|
/* 返回按钮样式 */
|
|
55
56
|
#web-back-btn {
|
|
@@ -131,6 +132,9 @@
|
|
|
131
132
|
var offsetY = (_originalInnerHeight - TV_HEIGHT * scale) / 2;
|
|
132
133
|
app.style.marginLeft = offsetX + 'px';
|
|
133
134
|
app.style.marginTop = offsetY + 'px';
|
|
135
|
+
|
|
136
|
+
// Show the app after positioning
|
|
137
|
+
app.style.visibility = 'visible';
|
|
134
138
|
|
|
135
139
|
// 更新返回按钮位置,相对于 #app 左上角
|
|
136
140
|
var btn = document.getElementById('web-back-btn');
|
|
@@ -144,6 +148,13 @@
|
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
}
|
|
151
|
+
|
|
152
|
+
// Execute immediately when DOM is ready (before load event)
|
|
153
|
+
if (document.readyState === 'loading') {
|
|
154
|
+
document.addEventListener('DOMContentLoaded', scaleApp);
|
|
155
|
+
} else {
|
|
156
|
+
scaleApp();
|
|
157
|
+
}
|
|
147
158
|
window.addEventListener('load', scaleApp);
|
|
148
159
|
window.addEventListener('resize', scaleApp);
|
|
149
160
|
</script>
|
package/bin/qt-web-dev.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* QuickTVUI Web Dev CLI
|
|
5
|
-
* 启动 web 开发服务器,无需手动配置入口文件
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const path = require('path')
|
|
9
|
-
const fs = require('fs')
|
|
10
|
-
const signale = require('signale')
|
|
11
|
-
const minimist = require('minimist')
|
|
12
|
-
const { exec } = require('shelljs')
|
|
13
|
-
|
|
14
|
-
// 解析命令行参数
|
|
15
|
-
const args = minimist(process.argv.slice(2), {
|
|
16
|
-
string: ['port', 'config'],
|
|
17
|
-
boolean: ['help', 'open'],
|
|
18
|
-
alias: {
|
|
19
|
-
p: 'port',
|
|
20
|
-
c: 'config',
|
|
21
|
-
h: 'help',
|
|
22
|
-
o: 'open',
|
|
23
|
-
},
|
|
24
|
-
default: {
|
|
25
|
-
port: 39001,
|
|
26
|
-
open: false,
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
// 显示帮助信息
|
|
31
|
-
if (args.help) {
|
|
32
|
-
console.log(`
|
|
33
|
-
QuickTVUI Web Dev CLI
|
|
34
|
-
|
|
35
|
-
Usage:
|
|
36
|
-
qt-web-dev [options]
|
|
37
|
-
|
|
38
|
-
Options:
|
|
39
|
-
-p, --port <port> 开发服务器端口 (默认: 39001)
|
|
40
|
-
-c, --config <path> 自定义 webpack 配置文件路径
|
|
41
|
-
-o, --open 自动打开浏览器
|
|
42
|
-
-h, --help 显示帮助信息
|
|
43
|
-
|
|
44
|
-
Examples:
|
|
45
|
-
qt-web-dev
|
|
46
|
-
qt-web-dev --port 8080
|
|
47
|
-
qt-web-dev --config ./my.webpack.js
|
|
48
|
-
|
|
49
|
-
特点:
|
|
50
|
-
- 自动检测项目入口,优先使用 webMain
|
|
51
|
-
- 若无 webMain,自动使用 src/web 或 @quicktvui/web-renderer 包
|
|
52
|
-
- 零配置启动 web 开发服务器
|
|
53
|
-
`)
|
|
54
|
-
process.exit(0)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function main() {
|
|
58
|
-
signale.pending('正在启动 QuickTVUI Web 开发服务器...')
|
|
59
|
-
|
|
60
|
-
// 查找项目根目录
|
|
61
|
-
const projectRoot = findProjectRoot()
|
|
62
|
-
if (!projectRoot) {
|
|
63
|
-
signale.error('无法找到项目根目录')
|
|
64
|
-
process.exit(1)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
signale.info(`项目根目录: ${projectRoot}`)
|
|
68
|
-
|
|
69
|
-
// 读取 package.json
|
|
70
|
-
let pkg = {}
|
|
71
|
-
try {
|
|
72
|
-
pkg = require(path.join(projectRoot, 'package.json'))
|
|
73
|
-
} catch (e) {}
|
|
74
|
-
|
|
75
|
-
// 检测入口文件
|
|
76
|
-
let mainEntry
|
|
77
|
-
let entryType
|
|
78
|
-
|
|
79
|
-
if (pkg.webMain) {
|
|
80
|
-
// 项目已有 webMain,直接使用
|
|
81
|
-
mainEntry = pkg.webMain
|
|
82
|
-
entryType = 'webmain'
|
|
83
|
-
signale.info(`主入口: ${mainEntry} (webMain)`)
|
|
84
|
-
} else if (fs.existsSync(path.join(projectRoot, 'src/web/index.js'))) {
|
|
85
|
-
// 有 src/web 目录,使用 entry-local.js 包装 main
|
|
86
|
-
mainEntry = pkg.main || './src/main.ts'
|
|
87
|
-
entryType = 'local'
|
|
88
|
-
signale.info(`主入口: ${mainEntry}`)
|
|
89
|
-
signale.info(`Web 渲染器: src/web (本地)`)
|
|
90
|
-
} else {
|
|
91
|
-
// 使用 @quicktvui/web-renderer 包
|
|
92
|
-
mainEntry = pkg.main || './src/main.ts'
|
|
93
|
-
entryType = 'package'
|
|
94
|
-
signale.info(`主入口: ${mainEntry}`)
|
|
95
|
-
signale.info(`Web 渲染器: @quicktvui/web-renderer (包)`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const mainEntryPath = path.resolve(projectRoot, mainEntry)
|
|
99
|
-
|
|
100
|
-
// 获取 webpack 配置
|
|
101
|
-
let webpackConfigPath
|
|
102
|
-
if (args.config) {
|
|
103
|
-
webpackConfigPath = path.resolve(projectRoot, args.config)
|
|
104
|
-
} else {
|
|
105
|
-
webpackConfigPath = path.resolve(__dirname, '../lib/webpack.config.js')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 设置环境变量
|
|
109
|
-
process.env.NODE_ENV = 'development'
|
|
110
|
-
process.env.QUICKTVUI_PROJECT_ROOT = projectRoot
|
|
111
|
-
process.env.QUICKTVUI_MAIN_ENTRY = mainEntryPath
|
|
112
|
-
process.env.QUICKTVUI_ENTRY_TYPE = entryType
|
|
113
|
-
process.env.QUICKTVUI_PORT = String(args.port)
|
|
114
|
-
|
|
115
|
-
// 检测 OpenSSL 版本
|
|
116
|
-
const needLegacyProvider = checkOpenSSLVersion(process.versions.openssl)
|
|
117
|
-
let nodeOptions = ''
|
|
118
|
-
if (needLegacyProvider) {
|
|
119
|
-
nodeOptions = '--openssl-legacy-provider'
|
|
120
|
-
signale.warn(`检测到 OpenSSL ${process.versions.openssl},已添加 --openssl-legacy-provider`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 构建 webpack 命令
|
|
124
|
-
const webpackArgs = ['serve', `--config "${webpackConfigPath}"`, `--port ${args.port}`, '--color']
|
|
125
|
-
if (args.open) webpackArgs.push('--open')
|
|
126
|
-
|
|
127
|
-
const envPrefix = nodeOptions ? `NODE_OPTIONS="${nodeOptions}" ` : ''
|
|
128
|
-
const webpackCmd = `${envPrefix}npx webpack ${webpackArgs.join(' ')}`
|
|
129
|
-
|
|
130
|
-
signale.pending(`执行: ${webpackCmd}`)
|
|
131
|
-
|
|
132
|
-
const result = exec(webpackCmd, { stdio: 'inherit' })
|
|
133
|
-
if (result.code !== 0) {
|
|
134
|
-
signale.error('启动开发服务器失败')
|
|
135
|
-
process.exit(1)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function findProjectRoot(startDir = process.cwd()) {
|
|
140
|
-
let currentDir = startDir
|
|
141
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
142
|
-
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
143
|
-
return currentDir
|
|
144
|
-
}
|
|
145
|
-
currentDir = path.dirname(currentDir)
|
|
146
|
-
}
|
|
147
|
-
return null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function checkOpenSSLVersion(version) {
|
|
151
|
-
if (!version) return false
|
|
152
|
-
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
|
|
153
|
-
if (!match) return false
|
|
154
|
-
return parseInt(match[1], 10) >= 3
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
main().catch((err) => {
|
|
158
|
-
signale.error('启动失败:', err)
|
|
159
|
-
process.exit(1)
|
|
160
|
-
})
|