@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 +185 -0
- package/bin/qt-web-dev.js +160 -0
- package/lib/entry-local.js +214 -0
- package/lib/entry-package.js +16 -0
- package/lib/index.js +166 -0
- package/lib/webpack.config.js +306 -0
- package/package.json +49 -0
- package/templates/web-renderer.html +169 -0
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>
|