@quicktvui/web-cli 1.0.0-beta.8 → 1.0.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.
@@ -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,316 @@
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
+ // 获取 webpack 配置
175
+ let webpackConfigPath
176
+ if (args.config) {
177
+ webpackConfigPath = path.resolve(projectRoot, args.config)
178
+ } else {
179
+ webpackConfigPath = path.resolve(__dirname, '../lib/webpack.config.js')
180
+ }
181
+
182
+ // 设置环境变量
183
+ process.env.NODE_ENV = 'development'
184
+ process.env.QUICKTVUI_PROJECT_ROOT = projectRoot
185
+ process.env.QUICKTVUI_MAIN_ENTRY = mainEntryPath
186
+ process.env.QUICKTVUI_ENTRY_TYPE = entryType
187
+ process.env.QUICKTVUI_PORT = String(args.port)
188
+
189
+ // 检测 OpenSSL 版本
190
+ const needLegacyProvider = checkOpenSSLVersion(process.versions.openssl)
191
+ if (needLegacyProvider) {
192
+ signale.warn(`检测到 OpenSSL ${process.versions.openssl},已添加 --openssl-legacy-provider`)
193
+ process.env.NODE_OPTIONS = '--openssl-legacy-provider'
194
+ }
195
+
196
+ signale.pending(`启动开发服务器: http://localhost:${args.port}`)
197
+
198
+ // 使用 webpack Node API 启动开发服务器
199
+ startDevServer(webpackConfigPath, args.port, args.open)
200
+ }
201
+
202
+ /**
203
+ * 使用 webpack Node API 启动开发服务器
204
+ */
205
+ function startDevServer(configPath, port, shouldOpen) {
206
+ const webpack = require('webpack')
207
+ const WebpackDevServer = require('webpack-dev-server')
208
+ const config = require(configPath)
209
+
210
+ config.devServer = config.devServer || {}
211
+ config.devServer.port = port
212
+ config.devServer.open = false
213
+
214
+ const compiler = webpack(config)
215
+ const server = new WebpackDevServer(config.devServer, compiler)
216
+
217
+ server.startCallback((err) => {
218
+ if (err) {
219
+ signale.error('启动开发服务器失败:', err.message)
220
+ process.exit(1)
221
+ }
222
+ signale.success(`开发服务器已启动: http://localhost:${port}`)
223
+
224
+ if (shouldOpen) {
225
+ const url = `http://localhost:${port}`
226
+ openBrowser(url)
227
+ }
228
+ })
229
+
230
+ process.on('SIGINT', () => {
231
+ server.stopCallback(() => {
232
+ signale.info('开发服务器已停止')
233
+ process.exit(0)
234
+ })
235
+ })
236
+ }
237
+
238
+ /**
239
+ * 等待服务器就绪
240
+ */
241
+ function waitForServer(url, timeout = 30000) {
242
+ const http = require('http')
243
+ const startTime = Date.now()
244
+
245
+ return new Promise((resolve, reject) => {
246
+ function check() {
247
+ if (Date.now() - startTime > timeout) {
248
+ reject(new Error('等待服务器超时'))
249
+ return
250
+ }
251
+
252
+ const req = http.get(url, (res) => {
253
+ if (res.statusCode === 200 || res.statusCode === 304) {
254
+ resolve()
255
+ } else {
256
+ setTimeout(check, 500)
257
+ }
258
+ })
259
+
260
+ req.on('error', () => {
261
+ setTimeout(check, 500)
262
+ })
263
+
264
+ req.setTimeout(1000, () => {
265
+ req.destroy()
266
+ setTimeout(check, 500)
267
+ })
268
+ }
269
+
270
+ check()
271
+ })
272
+ }
273
+
274
+ /**
275
+ * 打开浏览器
276
+ */
277
+ function openBrowser(url) {
278
+ const platform = process.platform
279
+ const command =
280
+ platform === 'darwin'
281
+ ? `open "${url}"`
282
+ : platform === 'win32'
283
+ ? `cmd /c start "" "${url}"`
284
+ : `xdg-open "${url}"`
285
+ const result = exec(command, { silent: true })
286
+
287
+ if (result.code === 0) {
288
+ signale.success('已打开浏览器')
289
+ return
290
+ }
291
+
292
+ signale.warn(`自动打开浏览器失败,请手动访问: ${url}`)
293
+ }
294
+
295
+ function findProjectRoot(startDir = process.cwd()) {
296
+ let currentDir = startDir
297
+ while (currentDir !== path.dirname(currentDir)) {
298
+ if (fs.existsSync(path.join(currentDir, 'package.json'))) {
299
+ return currentDir
300
+ }
301
+ currentDir = path.dirname(currentDir)
302
+ }
303
+ return null
304
+ }
305
+
306
+ function checkOpenSSLVersion(version) {
307
+ if (!version) return false
308
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)
309
+ if (!match) return false
310
+ return parseInt(match[1], 10) >= 3
311
+ }
312
+
313
+ main().catch((err) => {
314
+ signale.error('启动失败:', err)
315
+ process.exit(1)
316
+ })
@@ -1,24 +1,65 @@
1
- // @quicktvui/web-cli - web entry (package)
2
- // 此文件由 CLI 自动加载,使用 @quicktvui/web-renderer
3
- // 主入口路径通过 __QUICKTVUI_MAIN_ENTRY__ 注入
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 { initWebRenderer, startWebRenderer } from '@quicktvui/web-renderer'
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
- // Step 1: Initialize web renderer (includes setupSceneBuilder, createWebEngine, applyAllPatches, etc.)
12
- initWebRenderer()
19
+ // 初始化 async localStorage
20
+ initAsyncLocalStorage()
13
21
 
14
- // Step 2: Import main module (this will initialize Vue app and router)
15
- console.log('[Web Renderer] Importing main module...')
16
- import(__QUICKTVUI_MAIN_ENTRY__)
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
- // Step 3: Start the engine
19
- console.log('[Web Renderer] Starting engine...')
20
- startWebRenderer()
35
+ // 应用所有补丁
36
+ applyAllPatches(engine)
21
37
 
22
- setTimeout(() => {
23
- console.log('[Web Renderer] === Initialization complete ===')
24
- }, 1000)
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
+ })
@@ -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
- let entryFiles
137
+ // 获取 web-cli 目录下的 loader 路径
138
+ const cliDir = path.resolve(__dirname, '..')
139
+ const resolveLoader = (loaderName) => require.resolve(loaderName, { paths: [cliDir] })
92
140
 
93
- if (entryType === 'webmain') {
94
- // 直接使用 webMain 作为入口
95
- entryFiles = ['regenerator-runtime/runtime', mainEntry]
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: true,
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: 1000,
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
- { test: /\.scss$/, use: [cssLoader, 'sass-loader'] },
259
- { test: /\.sass$/, use: [cssLoader, 'sass-loader'] },
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.0-beta.8",
3
+ "version": "1.0.0",
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-dev": "./bin/qt-web-dev.js"
8
+ "qt-web-cli": "./bin/qt-web-cli.js"
9
9
  },
10
10
  "main": "lib/index.js",
11
11
  "files": [
@@ -24,29 +24,38 @@
24
24
  "minimist": "^1.2.8",
25
25
  "shelljs": "^0.10.0",
26
26
  "signale": "^1.4.0",
27
- "@quicktvui/web-renderer": "^1.0.3",
28
- "scope-loader": "^1.0.3"
27
+ "@quicktvui/web-renderer": "^1.0.9",
28
+ "scope-loader": "^1.0.3",
29
+ "chokidar": "^3.5.3",
30
+ "rimraf": "^5.0.0"
29
31
  },
30
32
  "peerDependencies": {
31
- "webpack": "^5.0.0",
32
- "webpack-cli": "^5.0.0",
33
- "webpack-dev-server": "^4.0.0",
33
+ "webpack": "^5.89.0",
34
+ "webpack-cli": "^5.1.0",
35
+ "webpack-dev-server": "^4.15.0",
34
36
  "vue-loader": "^17.0.0",
35
- "html-webpack-plugin": "^5.0.0",
36
- "@babel/core": "^7.0.0",
37
- "babel-loader": "^9.0.0",
37
+ "html-webpack-plugin": "^5.5.0",
38
+ "@babel/core": "^7.23.0",
39
+ "babel-loader": "^9.1.0",
38
40
  "ts-loader": "^9.4.0",
39
- "css-loader": "^6.0.0",
40
- "@extscreen/es3-router": ">=3.0.1",
41
- "@extscreen/es3-vue": ">=3.0.0"
41
+ "vue": "^3.0.0",
42
+ "regenerator-runtime": "^0.14.0"
42
43
  },
43
44
  "peerDependenciesMeta": {
44
- "less-loader": { "optional": true },
45
- "sass-loader": { "optional": true },
46
- "sass": { "optional": true },
47
- "less": { "optional": true }
45
+ "less-loader": {
46
+ "optional": true
47
+ },
48
+ "sass-loader": {
49
+ "optional": true
50
+ },
51
+ "sass": {
52
+ "optional": true
53
+ },
54
+ "less": {
55
+ "optional": true
56
+ }
48
57
  },
49
58
  "engines": {
50
59
  "node": ">=16.0.0"
51
60
  }
52
- }
61
+ }
@@ -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
- })