@quicktvui/web-cli 1.0.0-beta.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 ADDED
@@ -0,0 +1,185 @@
1
+ # @quicktvui/web-cli
2
+
3
+ QuickTVUI Web 开发服务器 CLI 工具,零配置启动 Web 渲染开发环境。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @quicktvui/web-cli --save-dev
9
+ # 或
10
+ yarn add @quicktvui/web-cli --dev
11
+ # 或
12
+ pnpm add @quicktvui/web-cli -D
13
+ ```
14
+
15
+ ## 使用
16
+
17
+ ### 基本用法
18
+
19
+ ```bash
20
+ # 安装后直接运行
21
+ npx qt-web-dev
22
+
23
+ # 或在 package.json 中添加脚本
24
+ {
25
+ "scripts": {
26
+ "web:dev": "qt-web-dev"
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### 命令行选项
32
+
33
+ ```
34
+ 用法: qt-web-dev [options]
35
+
36
+ 选项:
37
+ -p, --port <port> 开发服务器端口 (默认: 39001)
38
+ -c, --config <path> 自定义 webpack 配置文件路径
39
+ -o, --open 自动打开浏览器
40
+ -h, --help 显示帮助信息
41
+
42
+ 示例:
43
+ qt-web-dev
44
+ qt-web-dev --port 8080
45
+ qt-web-dev --config ./my.webpack.js
46
+ ```
47
+
48
+ ## 入口文件检测规则
49
+
50
+ CLI 会自动检测项目入口,优先级如下:
51
+
52
+ ### 1. webMain 配置(推荐)
53
+
54
+ 如果 `package.json` 中配置了 `webMain`,直接使用该入口:
55
+
56
+ ```json
57
+ {
58
+ "main": "./src/main.ts",
59
+ "webMain": "./src/main-web.js"
60
+ }
61
+ ```
62
+
63
+ ### 2. 本地 src/web 目录
64
+
65
+ 如果没有 `webMain` 但存在 `src/web/index.js`,CLI 会:
66
+ - 使用 `src/web` 目录作为 Web 渲染器
67
+ - 自动包装 `main` 指定的入口文件
68
+
69
+ ### 3. @quicktvui/web-renderer 包
70
+
71
+ 如果以上都不满足,CLI 会:
72
+ - 使用 `@quicktvui/web-renderer` 包作为渲染器
73
+ - 自动包装 `main` 指定的入口文件
74
+
75
+ ## 项目结构示例
76
+
77
+ ### 方式一:独立 webMain 入口(推荐)
78
+
79
+ ```
80
+ project/
81
+ ├── package.json # 配置 webMain
82
+ ├── src/
83
+ │ ├── main.ts # 原生入口
84
+ │ ├── main-web.js # Web 入口(自定义初始化逻辑)
85
+ │ └── web/ # Web 渲染器
86
+ │ ├── index.js
87
+ │ ├── core/
88
+ │ └── components/
89
+ └── ...
90
+ ```
91
+
92
+ ```json
93
+ // package.json
94
+ {
95
+ "main": "./src/main.ts",
96
+ "webMain": "./src/main-web.js"
97
+ }
98
+ ```
99
+
100
+ ### 方式二:零配置模式
101
+
102
+ ```
103
+ project/
104
+ ├── package.json # 无需配置 webMain
105
+ ├── src/
106
+ │ ├── main.ts # 主入口
107
+ │ └── web/ # Web 渲染器(CLI 自动检测)
108
+ │ ├── index.js
109
+ │ └── core/
110
+ └── ...
111
+ ```
112
+
113
+ ### 方式三:使用 web-renderer 包
114
+
115
+ ```
116
+ project/
117
+ ├── package.json
118
+ ├── src/
119
+ │ └── main.ts # 主入口
120
+ └── ...
121
+ ```
122
+
123
+ ```json
124
+ // package.json
125
+ {
126
+ "dependencies": {
127
+ "@quicktvui/web-renderer": "^1.0.0"
128
+ }
129
+ }
130
+ ```
131
+
132
+ ## 内置功能
133
+
134
+ ### 自动代理
135
+
136
+ CLI 内置代理功能,解决开发环境跨域问题:
137
+
138
+ ```
139
+ /proxy/{protocol}/{host}{path}
140
+
141
+ 示例:
142
+ /proxy/https/api.example.com/users
143
+ -> https://api.example.com/users
144
+ ```
145
+
146
+ ### 自动检测路径别名
147
+
148
+ 自动读取 `jsconfig.json` 或 `tsconfig.json` 中的路径配置:
149
+
150
+ ```json
151
+ {
152
+ "compilerOptions": {
153
+ "baseUrl": ".",
154
+ "paths": {
155
+ "@/*": ["src/*"],
156
+ "@components/*": ["src/components/*"]
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### OpenSSL 兼容
163
+
164
+ 自动检测 OpenSSL 版本,对于 OpenSSL 3.x 自动添加 `--openssl-legacy-provider` 参数。
165
+
166
+ ## 自定义 Webpack 配置
167
+
168
+ 如需自定义配置,可通过 `--config` 指定:
169
+
170
+ ```bash
171
+ qt-web-dev --config ./webpack.config.js
172
+ ```
173
+
174
+ 自定义配置会完全替换内置配置。
175
+
176
+ ## 技术栈
177
+
178
+ - Vue 3 + TypeScript
179
+ - Webpack 5
180
+ - vue-loader
181
+ - @extscreen/es3-vue-css-loader
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,160 @@
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
+ })
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @quicktvui/web-cli - 本地入口文件
3
+ * 使用项目本地的 src/web 目录
4
+ *
5
+ * 路径变量通过 webpack DefinePlugin 注入(预计算完整路径)
6
+ */
7
+
8
+ console.log('[Web Renderer] === Starting initialization (local) ===')
9
+
10
+ // 路径变量已通过 webpack DefinePlugin 注入
11
+ const projectRoot = __QUICKTVUI_PROJECT_ROOT__
12
+ const mainEntry = __QUICKTVUI_MAIN_ENTRY__
13
+
14
+ // 1. 初始化 asyncLocalStorage
15
+ const { initAsyncLocalStorage } = require(__QUICKTVUI_MODULE_ASYNC_LOCAL_STORAGE__)
16
+ initAsyncLocalStorage()
17
+
18
+ // 2. 初始化自动代理
19
+ const { initAutoProxy } = require(__QUICKTVUI_MODULE_AUTO_PROXY__)
20
+ initAutoProxy()
21
+
22
+ // 3. 初始化全局组件注册
23
+ global.__WEB_COMPONENTS__ = global.__WEB_COMPONENTS__ || {}
24
+
25
+ // 4. 设置 SceneBuilder
26
+ const { setupSceneBuilder } = require(__QUICKTVUI_MODULE_SCENE_BUILDER__)
27
+ setupSceneBuilder()
28
+
29
+ // 5. 创建 Web 引擎
30
+ const { createWebEngine, startWebEngine, APP_NAME } = require(__QUICKTVUI_MODULE_WEB_ENGINE__)
31
+ const engine = createWebEngine()
32
+
33
+ // 6. 应用所有补丁
34
+ const { applyAllPatches } = require(__QUICKTVUI_MODULE_PATCHES__)
35
+ applyAllPatches(engine)
36
+
37
+ // 7. 初始化 TV 焦点管理器
38
+ const { TVFocusManager } = require(__QUICKTVUI_MODULE_TV_FOCUS_MANAGER__)
39
+ const focusManager = new TVFocusManager()
40
+ global.__TV_FOCUS_MANAGER__ = focusManager
41
+ console.log('[Web Renderer] TVFocusManager initialized')
42
+
43
+ // 8. 注入全局 CSS
44
+ const styleEl = document.createElement('style')
45
+ styleEl.id = 'web-platform-reset'
46
+ styleEl.textContent = `
47
+ /* Web-Android Layout Compatibility Reset */
48
+ #app, #app * { align-items: flex-start; }
49
+ [style*="align-items"] { align-items: var(--align-items, center) !important; }
50
+ `
51
+ document.head.appendChild(styleEl)
52
+ console.log('[Web Renderer] Global CSS reset injected')
53
+
54
+ // 9. 设置 appRegister 代理
55
+ setupAppRegisterProxy()
56
+
57
+ // 主入口文件已通过 webpack 多入口配置加载,无需在此处 require
58
+
59
+ // 10. 启动引擎
60
+ console.log('[Web Renderer] Starting engine...')
61
+ startWebEngine(engine, APP_NAME)
62
+ console.log('[Web Renderer] === Initialization complete ===')
63
+
64
+ // ============================================================================
65
+ // 辅助函数
66
+ // ============================================================================
67
+
68
+ let lifecycleSetupDone = false
69
+
70
+ function setupAppRegisterProxy() {
71
+ global.__GLOBAL__ = global.__GLOBAL__ || {}
72
+ const appRegister = {}
73
+
74
+ global.__GLOBAL__.appRegister = new Proxy(appRegister, {
75
+ set(target, appName, appEntry) {
76
+ console.log('[Web Renderer] App registering:', appName)
77
+
78
+ const originalRun = appEntry.run
79
+ if (typeof originalRun === 'function') {
80
+ appEntry.run = function (superProps) {
81
+ console.log('[Web Renderer] App starting:', appName)
82
+ const normalizedProps = normalizeSuperProps(superProps)
83
+ const result = originalRun.call(this, normalizedProps)
84
+ setupLifecycleAndGuards()
85
+ return result
86
+ }
87
+ }
88
+
89
+ target[appName] = appEntry
90
+ return true
91
+ },
92
+ get(target, prop) {
93
+ return target[prop]
94
+ },
95
+ })
96
+
97
+ console.log('[Web Renderer] AppRegister proxy installed')
98
+ }
99
+
100
+ function normalizeSuperProps(superProps) {
101
+ if (!superProps) return superProps
102
+ const result = { ...superProps }
103
+
104
+ if (result.location) {
105
+ const location = result.location
106
+ if (typeof location === 'string') {
107
+ if (location !== '/') {
108
+ result.path = location
109
+ }
110
+ delete result.location
111
+ } else if (typeof location === 'object') {
112
+ if (location.name) {
113
+ result.name = location.name
114
+ } else if (location.path) {
115
+ result.path = location.path
116
+ }
117
+ delete result.location
118
+ }
119
+ }
120
+
121
+ return result
122
+ }
123
+
124
+ function setupLifecycleAndGuards() {
125
+ if (lifecycleSetupDone) return
126
+
127
+ setTimeout(() => {
128
+ const app = findVueApp()
129
+ const router = findVueRouter(app)
130
+
131
+ if (app) {
132
+ const { setupPageLifecycle } = require(__QUICKTVUI_MODULE_PAGE_LIFECYCLE__)
133
+ setupPageLifecycle(app)
134
+ console.log('[Web Renderer] Page lifecycle setup complete')
135
+ }
136
+
137
+ if (router) {
138
+ global.__ES_ROUTER__ = router
139
+ addMainRouteRedirect(router)
140
+
141
+ const { setupRouterGuard, setupBrowserBackInterceptor } = require(
142
+ __QUICKTVUI_MODULE_PAGE_LIFECYCLE__
143
+ )
144
+ setupRouterGuard(router)
145
+ setupBrowserBackInterceptor()
146
+ console.log('[Web Renderer] Router guard setup complete')
147
+ }
148
+
149
+ lifecycleSetupDone = true
150
+ }, 150)
151
+ }
152
+
153
+ function findVueApp() {
154
+ const rootEl = document.getElementById('root')
155
+ if (rootEl?.__vue_app__) return rootEl.__vue_app__
156
+
157
+ const appEl = document.getElementById('app')
158
+ if (appEl?.__vue_app__) return appEl.__vue_app__
159
+
160
+ if (typeof __webpack_require__ !== 'undefined') {
161
+ try {
162
+ const cache = __webpack_require__.c
163
+ for (const moduleId in cache) {
164
+ const module = cache[moduleId]
165
+ if (!module) continue
166
+ const exports = module.exports || module
167
+ if (exports && typeof exports === 'object') {
168
+ if (exports.app && typeof exports.app.use === 'function') return exports.app
169
+ if (exports.default?.app && typeof exports.default.app.use === 'function')
170
+ return exports.default.app
171
+ }
172
+ }
173
+ } catch (e) {}
174
+ }
175
+
176
+ return null
177
+ }
178
+
179
+ function findVueRouter(app) {
180
+ if (!app) return null
181
+
182
+ const router = app.config?.globalProperties?.$router
183
+ if (router && typeof router.push === 'function') return router
184
+
185
+ const provides = app._context?.provides
186
+ if (provides) {
187
+ for (const key of Object.keys(provides)) {
188
+ const val = provides[key]
189
+ if (val && typeof val.push === 'function' && typeof val.beforeEach === 'function') {
190
+ return val
191
+ }
192
+ }
193
+ }
194
+
195
+ return null
196
+ }
197
+
198
+ function addMainRouteRedirect(router) {
199
+ if (!router) return
200
+
201
+ const mainRouteName = router.options?.main
202
+ if (!mainRouteName) return
203
+
204
+ const routes = router.options?.routes || []
205
+ const hasRootPath = routes.some((r) => r.path === '/')
206
+
207
+ if (!hasRootPath) {
208
+ router.addRoute({
209
+ path: '/',
210
+ redirect: { name: mainRouteName },
211
+ })
212
+ console.log('[Web Renderer] Added "/" redirect to main route:', mainRouteName)
213
+ }
214
+ }
@@ -0,0 +1,16 @@
1
+ // @quicktvui/web-cli - web entry (package)
2
+ // 此文件由 CLI 自动加载,使用 @quicktvui/web-renderer 包
3
+ // 主入口路径通过 __QUICKTVUI_MAIN_ENTRY__ 注入
4
+
5
+ console.log('[Web Renderer] === Starting initialization ===')
6
+
7
+ import { initWebRenderer, startWebRenderer } from '@quicktvui/web-renderer'
8
+
9
+ initWebRenderer()
10
+
11
+ console.log('[Web Renderer] Importing main module...')
12
+ import(__QUICKTVUI_MAIN_ENTRY__)
13
+
14
+ console.log('[Web Renderer] Starting engine...')
15
+ startWebRenderer()
16
+ console.log('[Web Renderer] === Initialization complete ===')
package/lib/index.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * QuickTVUI Web CLI
3
+ * 主入口 - 提供编程接口
4
+ */
5
+
6
+ const path = require('path')
7
+ const fs = require('fs')
8
+
9
+ const pkg = require('../package.json')
10
+
11
+ /**
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
+ /**
26
+ * 检测 web renderer 类型
27
+ */
28
+ function detectWebRendererType(projectRoot) {
29
+ if (fs.existsSync(path.join(projectRoot, 'src/web/index.js'))) {
30
+ return 'local'
31
+ }
32
+ try {
33
+ const projectPkg = require(path.join(projectRoot, 'package.json'))
34
+ const deps = { ...projectPkg?.dependencies, ...projectPkg?.devDependencies }
35
+ if (deps['@quicktvui/web-renderer']) {
36
+ return 'package'
37
+ }
38
+ } catch (e) {}
39
+ return 'package'
40
+ }
41
+
42
+ /**
43
+ * 生成 web 入口内容
44
+ */
45
+ function generateWebEntryContent(projectRoot, mainEntry) {
46
+ const mainPath =
47
+ './' + path.relative(projectRoot, path.resolve(projectRoot, mainEntry)).replace(/\\/g, '/')
48
+ const type = detectWebRendererType(projectRoot)
49
+
50
+ if (type === 'local') {
51
+ return getLocalEntryContent(mainPath)
52
+ }
53
+ return getPackageEntryContent(mainPath)
54
+ }
55
+
56
+ function getLocalEntryContent(mainPath) {
57
+ return `// @quicktvui/web-cli - auto-generated entry (local)
58
+ console.log('[Web Renderer] === Starting initialization ===')
59
+
60
+ import { initAsyncLocalStorage } from '../src/web/core/asyncLocalStorage'
61
+ initAsyncLocalStorage()
62
+
63
+ import { initAutoProxy } from '../src/web/core/autoProxy'
64
+ initAutoProxy()
65
+
66
+ global.__WEB_COMPONENTS__ = global.__WEB_COMPONENTS__ || {}
67
+
68
+ import { setupSceneBuilder } from '../src/web/core/SceneBuilder'
69
+ setupSceneBuilder()
70
+
71
+ import { createWebEngine, startWebEngine, APP_NAME } from '../src/web'
72
+ const engine = createWebEngine()
73
+
74
+ import { applyAllPatches } from '../src/web/core/patches'
75
+ applyAllPatches(engine)
76
+
77
+ import { TVFocusManager } from '../src/web/core/TVFocusManager'
78
+ const focusManager = new TVFocusManager()
79
+ global.__TV_FOCUS_MANAGER__ = focusManager
80
+
81
+ setupAppRegisterProxy()
82
+
83
+ console.log('[Web Renderer] Importing main module...')
84
+ import '${mainPath}'
85
+
86
+ console.log('[Web Renderer] Starting engine...')
87
+ startWebEngine(engine, APP_NAME)
88
+ console.log('[Web Renderer] === Initialization complete ===')
89
+
90
+ function setupAppRegisterProxy() {
91
+ global.__GLOBAL__ = global.__GLOBAL__ || {}
92
+ const appRegister = {}
93
+ let lifecycleDone = false
94
+ global.__GLOBAL__.appRegister = new Proxy(appRegister, {
95
+ set(t, name, entry) {
96
+ const run = entry.run
97
+ if (typeof run === 'function') {
98
+ entry.run = function(p) {
99
+ const r = run.call(this, normalize(p))
100
+ setupLifecycle()
101
+ return r
102
+ }
103
+ }
104
+ t[name] = entry
105
+ return true
106
+ },
107
+ get(t, p) { return t[p] }
108
+ })
109
+ }
110
+
111
+ function normalize(p) {
112
+ if (!p) return p
113
+ const r = { ...p }
114
+ if (r.location) {
115
+ if (typeof r.location === 'string') {
116
+ if (r.location !== '/') r.path = r.location
117
+ delete r.location
118
+ } else if (typeof r.location === 'object') {
119
+ if (r.location.name) r.name = r.location.name
120
+ else if (r.location.path) r.path = r.location.path
121
+ delete r.location
122
+ }
123
+ }
124
+ return r
125
+ }
126
+
127
+ function setupLifecycle() {
128
+ if (lifecycleDone) return
129
+ lifecycleDone = true
130
+ setTimeout(() => {
131
+ const app = document.getElementById('root')?.__vue_app__ || document.getElementById('app')?.__vue_app__
132
+ const router = app?.config?.globalProperties?.$router
133
+ if (router) {
134
+ global.__ES_ROUTER__ = router
135
+ if (router.options?.main && !router.options?.routes?.some(r => r.path === '/')) {
136
+ router.addRoute({ path: '/', redirect: { name: router.options.main } })
137
+ }
138
+ }
139
+ }, 150)
140
+ }
141
+ `
142
+ }
143
+
144
+ function getPackageEntryContent(mainPath) {
145
+ return `// @quicktvui/web-cli - auto-generated entry (package)
146
+ console.log('[Web Renderer] === Starting initialization ===')
147
+
148
+ import { initWebRenderer, startWebRenderer } from '@quicktvui/web-renderer'
149
+
150
+ initWebRenderer()
151
+
152
+ console.log('[Web Renderer] Importing main module...')
153
+ import '${mainPath}'
154
+
155
+ console.log('[Web Renderer] Starting engine...')
156
+ startWebRenderer()
157
+ console.log('[Web Renderer] === Initialization complete ===')
158
+ `
159
+ }
160
+
161
+ module.exports = {
162
+ version: pkg.version,
163
+ findProjectRoot,
164
+ detectWebRendererType,
165
+ generateWebEntryContent,
166
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * QuickTVUI Web CLI - 内置 Webpack 配置
3
+ * 使用固定入口文件,通过 DefinePlugin 注入主入口路径
4
+ */
5
+
6
+ const path = require('path')
7
+ const fs = require('fs')
8
+
9
+ // 从环境变量获取配置
10
+ const projectRoot = process.env.QUICKTVUI_PROJECT_ROOT || process.cwd()
11
+ const mainEntry = process.env.QUICKTVUI_MAIN_ENTRY || path.resolve(projectRoot, 'src/main.ts')
12
+ const entryType = process.env.QUICKTVUI_ENTRY_TYPE || 'package'
13
+ const port = parseInt(process.env.QUICKTVUI_PORT || '39001', 10)
14
+
15
+ // 尝试加载项目的 package.json
16
+ let pkg = {}
17
+ try {
18
+ pkg = require(path.join(projectRoot, 'package.json'))
19
+ } catch (e) {}
20
+
21
+ /**
22
+ * 自动检测项目路径别名
23
+ */
24
+ function detectAlias() {
25
+ const alias = {
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
+ }
31
+
32
+ const jsconfigPath = path.join(projectRoot, 'jsconfig.json')
33
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json')
34
+ const configPath = fs.existsSync(tsconfigPath) ? tsconfigPath : jsconfigPath
35
+
36
+ if (fs.existsSync(configPath)) {
37
+ try {
38
+ const config = require(configPath)
39
+ const paths = config.compilerOptions?.paths || {}
40
+ for (const [key, value] of Object.entries(paths)) {
41
+ if (Array.isArray(value) && value[0]) {
42
+ const aliasKey = key.replace('/*', '')
43
+ const aliasValue = path.resolve(
44
+ projectRoot,
45
+ config.compilerOptions?.baseUrl || '.',
46
+ value[0].replace('/*', '')
47
+ )
48
+ alias[aliasKey] = aliasValue
49
+ }
50
+ }
51
+ } catch (e) {}
52
+ }
53
+
54
+ return alias
55
+ }
56
+
57
+ /**
58
+ * 检测 resolve modules
59
+ */
60
+ function detectModules() {
61
+ const modules = []
62
+ const projectNodeModules = path.join(projectRoot, 'node_modules')
63
+ if (fs.existsSync(projectNodeModules)) {
64
+ modules.push(projectNodeModules)
65
+ }
66
+ const cwdNodeModules = path.join(process.cwd(), 'node_modules')
67
+ if (fs.existsSync(cwdNodeModules) && !modules.includes(cwdNodeModules)) {
68
+ modules.push(cwdNodeModules)
69
+ }
70
+ return modules
71
+ }
72
+
73
+ /**
74
+ * 检测 CSS loader 类型
75
+ */
76
+ function detectCSSLoader() {
77
+ try {
78
+ require.resolve('@extscreen/es3-vue-css-loader', { paths: [projectRoot] })
79
+ return '@extscreen/es3-vue-css-loader'
80
+ } catch (e) {}
81
+ try {
82
+ require.resolve('vue-style-loader', { paths: [projectRoot] })
83
+ return 'vue-style-loader'
84
+ } catch (e) {}
85
+ return 'style-loader'
86
+ }
87
+
88
+ const cssLoader = detectCSSLoader()
89
+
90
+ // 根据类型选择入口文件
91
+ let entryFiles
92
+
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
+ }
105
+
106
+ module.exports = {
107
+ mode: 'development',
108
+ bail: true,
109
+
110
+ devServer: {
111
+ port,
112
+ hot: true,
113
+ liveReload: true,
114
+ proxy: [
115
+ {
116
+ context: ['/proxy'],
117
+ target: 'http://placeholder',
118
+ changeOrigin: true,
119
+ secure: false,
120
+ router: (req) => {
121
+ const match = req.url.match(/^\/proxy\/(https?)\/([^/]+)(\/.*)?$/)
122
+ if (match) {
123
+ return `${match[1]}://${match[2]}`
124
+ }
125
+ return 'http://placeholder'
126
+ },
127
+ pathRewrite: (path, req) => {
128
+ const match = path.match(/^\/proxy\/(https?)\/[^/]+(\/.*)?$/)
129
+ if (match) {
130
+ return match[2] || '/'
131
+ }
132
+ return path
133
+ },
134
+ onProxyReq: (proxyReq, req, res) => {
135
+ console.log(
136
+ `[AutoProxy] ${req.method} ${req.url} -> ${proxyReq.getHeader('host')}${proxyReq.path}`
137
+ )
138
+ },
139
+ },
140
+ ],
141
+ static: {
142
+ directory: path.join(projectRoot, 'public'),
143
+ publicPath: '/',
144
+ },
145
+ client: {
146
+ overlay: { errors: true, warnings: false },
147
+ },
148
+ },
149
+
150
+ watchOptions: {
151
+ aggregateTimeout: 1500,
152
+ poll: 1000,
153
+ ignored: /node_modules/,
154
+ },
155
+
156
+ devtool: 'source-map',
157
+
158
+ // 多入口配置:先加载初始化代码,再加载主入口
159
+ entry: {
160
+ index: entryFiles,
161
+ },
162
+
163
+ output: {
164
+ filename: 'index.bundle.js',
165
+ path: path.resolve(projectRoot, './dist/web/'),
166
+ strictModuleExceptionHandling: true,
167
+ globalObject: '(0, eval)("this")',
168
+ },
169
+
170
+ plugins: [
171
+ new (require('vue-loader').VueLoaderPlugin)(),
172
+ new (require('html-webpack-plugin'))({
173
+ inject: true,
174
+ scriptLoading: 'blocking',
175
+ template: path.resolve(__dirname, '../templates/web-renderer.html'),
176
+ title: pkg.name || 'QuickTVUI Web',
177
+ }),
178
+ // 注入主入口路径和项目根目录,以及预计算的模块路径
179
+ new (require('webpack').DefinePlugin)({
180
+ __QUICKTVUI_PROJECT_ROOT__: JSON.stringify(projectRoot),
181
+ __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
+ process: JSON.stringify({
201
+ env: { NODE_ENV: 'development' },
202
+ browser: true,
203
+ version: '',
204
+ versions: {},
205
+ }),
206
+ __PLATFORM__: JSON.stringify('web'),
207
+ __DEV__: JSON.stringify(true),
208
+ }),
209
+ ],
210
+
211
+ module: {
212
+ rules: [
213
+ // Vue 文件
214
+ {
215
+ test: /\.vue$/,
216
+ use: [
217
+ {
218
+ loader: 'vue-loader',
219
+ options: { compilerOptions: { whitespace: 'condense' } },
220
+ },
221
+ 'scope-loader',
222
+ ],
223
+ },
224
+ // TypeScript 文件
225
+ {
226
+ test: /\.ts$/,
227
+ use: [
228
+ {
229
+ loader: 'ts-loader',
230
+ options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true },
231
+ },
232
+ ],
233
+ },
234
+ // JavaScript 文件
235
+ {
236
+ test: /\.js$/,
237
+ exclude: /node_modules/,
238
+ use: [
239
+ {
240
+ loader: 'babel-loader',
241
+ options: {
242
+ sourceType: 'unambiguous',
243
+ presets: [['@babel/preset-env', { targets: { chrome: 57, ios: 9 } }]],
244
+ plugins: [
245
+ ['@babel/plugin-proposal-class-properties'],
246
+ ['@babel/plugin-proposal-decorators', { legacy: true }],
247
+ ['@babel/plugin-transform-runtime', { regenerator: true }],
248
+ ],
249
+ },
250
+ },
251
+ ],
252
+ },
253
+ // CSS (直接使用 es3-vue-css-loader,不通过 css-loader)
254
+ { test: /\.css$/, use: [cssLoader] },
255
+ // Less
256
+ { test: /\.less$/, use: [cssLoader, 'less-loader'] },
257
+ // Sass/SCSS
258
+ { test: /\.scss$/, use: [cssLoader, 'sass-loader'] },
259
+ { test: /\.sass$/, use: [cssLoader, 'sass-loader'] },
260
+ // 图片
261
+ {
262
+ test: /\.(png|jpe?g|gif|webp|svg)$/i,
263
+ type: 'asset',
264
+ parser: { dataUrlCondition: { maxSize: 10000 } },
265
+ generator: { filename: 'assets/[name].[hash:8][ext]' },
266
+ },
267
+ // 字体
268
+ {
269
+ test: /\.(woff2?|eot|ttf|otf)$/i,
270
+ type: 'asset/resource',
271
+ generator: { filename: 'fonts/[name].[hash:8][ext]' },
272
+ },
273
+ // 音频
274
+ {
275
+ test: /\.(mp3|wav|ogg|m4a)$/i,
276
+ type: 'asset/resource',
277
+ generator: { filename: 'audio/[name].[hash:8][ext]' },
278
+ },
279
+ ],
280
+ },
281
+
282
+ resolve: {
283
+ extensions: ['.web.js', '.web.ts', '.web.vue', '.js', '.vue', '.json', '.ts'],
284
+ modules: detectModules(),
285
+ fallback: {
286
+ fs: false,
287
+ path: false,
288
+ crypto: false,
289
+ stream: false,
290
+ http: false,
291
+ https: false,
292
+ zlib: false,
293
+ },
294
+ alias: detectAlias(),
295
+ },
296
+
297
+ performance: { hints: false },
298
+
299
+ // 忽略 entry-local.js 中动态 require 的警告(这是预期行为)
300
+ ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],
301
+
302
+ cache: {
303
+ type: 'filesystem',
304
+ buildDependencies: { config: [__filename] },
305
+ },
306
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@quicktvui/web-cli",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "CLI tool for QuickTVUI web development - zero configuration",
5
+ "author": "QuickTVUI Team",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "qt-web-dev": "./bin/qt-web-dev.js"
9
+ },
10
+ "main": "lib/index.js",
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "templates"
15
+ ],
16
+ "keywords": [
17
+ "quicktvui",
18
+ "web",
19
+ "cli",
20
+ "tv",
21
+ "hippy"
22
+ ],
23
+ "dependencies": {
24
+ "minimist": "^1.2.8",
25
+ "shelljs": "^0.10.0",
26
+ "signale": "^1.4.0"
27
+ },
28
+ "peerDependencies": {
29
+ "webpack": "^5.0.0",
30
+ "webpack-cli": "^5.0.0",
31
+ "webpack-dev-server": "^4.0.0",
32
+ "vue-loader": "^17.0.0",
33
+ "html-webpack-plugin": "^5.0.0",
34
+ "@babel/core": "^7.0.0",
35
+ "babel-loader": "^9.0.0",
36
+ "ts-loader": "^9.4.0",
37
+ "css-loader": "^6.0.0",
38
+ "extscreen-router": ">=3.0.1"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "less-loader": { "optional": true },
42
+ "sass-loader": { "optional": true },
43
+ "sass": { "optional": true },
44
+ "less": { "optional": true }
45
+ },
46
+ "engines": {
47
+ "node": ">=16.0.0"
48
+ }
49
+ }
@@ -0,0 +1,169 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= htmlWebpackPlugin.options.title || 'QuickTVUI Web' %></title>
7
+ <script>
8
+ // TV dimensions - MUST be set before web-renderer loads
9
+ const TV_WIDTH = 1920;
10
+ const TV_HEIGHT = 1080;
11
+
12
+ // Store original values for scale calculation
13
+ const _originalInnerWidth = window.innerWidth;
14
+ const _originalInnerHeight = window.innerHeight;
15
+
16
+ // Override innerWidth/innerHeight getters
17
+ Object.defineProperty(window, 'innerWidth', {
18
+ get: function() { return TV_WIDTH; },
19
+ configurable: true
20
+ });
21
+ Object.defineProperty(window, 'innerHeight', {
22
+ get: function() { return TV_HEIGHT; },
23
+ configurable: true
24
+ });
25
+ // Override devicePixelRatio
26
+ Object.defineProperty(window, 'devicePixelRatio', {
27
+ get: function() { return 1; },
28
+ configurable: true
29
+ });
30
+
31
+ console.log('[Polyfill] Overridden window dimensions to', TV_WIDTH, 'x', TV_HEIGHT);
32
+
33
+ // Initialize focus styles storage
34
+ window.__TV_FOCUS_STYLES__ = {};
35
+ </script>
36
+ <style>
37
+ * {
38
+ margin: 0;
39
+ padding: 0;
40
+ box-sizing: border-box;
41
+ }
42
+ html, body {
43
+ width: 100%;
44
+ height: 100%;
45
+ overflow: hidden;
46
+ background-color: #1a1a1a;
47
+ }
48
+ #app {
49
+ width: 1920px !important;
50
+ height: 1080px !important;
51
+ transform-origin: top left;
52
+ background-color: #26292F;
53
+ }
54
+ /* 返回按钮样式 */
55
+ #web-back-btn {
56
+ position: fixed;
57
+ z-index: 99999;
58
+ width: 48px;
59
+ height: 48px;
60
+ background: rgba(255, 255, 255, 0.1);
61
+ border: 2px solid rgba(255, 255, 255, 0.3);
62
+ border-radius: 50%;
63
+ cursor: pointer;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ transition: all 0.2s ease;
68
+ backdrop-filter: blur(10px);
69
+ }
70
+ #web-back-btn:hover {
71
+ background: rgba(255, 255, 255, 0.2);
72
+ border-color: rgba(255, 255, 255, 0.5);
73
+ transform: scale(1.1);
74
+ }
75
+ #web-back-btn:active {
76
+ transform: scale(0.95);
77
+ }
78
+ #web-back-btn svg {
79
+ width: 24px;
80
+ height: 24px;
81
+ fill: none;
82
+ stroke: rgba(255, 255, 255, 0.8);
83
+ stroke-width: 2;
84
+ stroke-linecap: round;
85
+ stroke-linejoin: round;
86
+ }
87
+ /* Force all direct children of #app to be 1920x1080 */
88
+ #app > * {
89
+ width: 1920px !important;
90
+ height: 1080px !important;
91
+ position: relative !important;
92
+ overflow: hidden !important;
93
+ }
94
+ /* TV Focus styles */
95
+ [focusable="true"], [data-focusable="true"] {
96
+ cursor: pointer;
97
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
98
+ }
99
+ [focusable="true"].focused, [data-focusable="true"].focused {
100
+ transform: scale(1.05);
101
+ box-shadow: 0 0 20px rgba(76, 175, 80, 0.5);
102
+ z-index: 100;
103
+ }
104
+ </style>
105
+ <script>
106
+ // Fix #app dimensions immediately when DOM is ready
107
+ function fixAppDimensions() {
108
+ var app = document.getElementById('app');
109
+ if (app) {
110
+ app.style.setProperty('width', TV_WIDTH + 'px', 'important');
111
+ app.style.setProperty('height', TV_HEIGHT + 'px', 'important');
112
+ console.log('[Polyfill] Fixed #app dimensions to', TV_WIDTH, 'x', TV_HEIGHT);
113
+ }
114
+ }
115
+
116
+ document.addEventListener('DOMContentLoaded', fixAppDimensions);
117
+ window.addEventListener('load', fixAppDimensions);
118
+
119
+ // Scale the app to fit the viewport
120
+ function scaleApp() {
121
+ var app = document.getElementById('app');
122
+ if (app) {
123
+ app.style.setProperty('width', TV_WIDTH + 'px', 'important');
124
+ app.style.setProperty('height', TV_HEIGHT + 'px', 'important');
125
+
126
+ var scaleX = _originalInnerWidth / TV_WIDTH;
127
+ var scaleY = _originalInnerHeight / TV_HEIGHT;
128
+ var scale = Math.min(scaleX, scaleY);
129
+ app.style.transform = 'scale(' + scale + ')';
130
+ var offsetX = (_originalInnerWidth - TV_WIDTH * scale) / 2;
131
+ var offsetY = (_originalInnerHeight - TV_HEIGHT * scale) / 2;
132
+ app.style.marginLeft = offsetX + 'px';
133
+ app.style.marginTop = offsetY + 'px';
134
+
135
+ // 更新返回按钮位置,相对于 #app 左上角
136
+ var btn = document.getElementById('web-back-btn');
137
+ if (btn) {
138
+ var btnSize = 48 * scale;
139
+ var btnMargin = 20 * scale;
140
+ btn.style.width = btnSize + 'px';
141
+ btn.style.height = btnSize + 'px';
142
+ btn.style.left = (offsetX + btnMargin) + 'px';
143
+ btn.style.top = (offsetY + btnMargin) + 'px';
144
+ }
145
+ }
146
+ }
147
+ window.addEventListener('load', scaleApp);
148
+ window.addEventListener('resize', scaleApp);
149
+ </script>
150
+ </head>
151
+ <body>
152
+ <div id="app"></div>
153
+ <!-- Web 返回按钮 - 动态定位 -->
154
+ <button id="web-back-btn" title="返回">
155
+ <svg viewBox="0 0 24 24">
156
+ <polyline points="15 18 9 12 15 6"></polyline>
157
+ </svg>
158
+ </button>
159
+ <script>
160
+ // 返回按钮点击事件
161
+ document.getElementById('web-back-btn').addEventListener('click', function() {
162
+ var focusManager = window.__TV_FOCUS_MANAGER__;
163
+ if (focusManager && typeof focusManager.dispatchBackPressed === 'function') {
164
+ focusManager.dispatchBackPressed({ key: 'Escape', keyCode: 4 });
165
+ }
166
+ });
167
+ </script>
168
+ </body>
169
+ </html>