@quicktvui/web-cli 1.0.8 → 2.1.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.
- package/README.md +101 -113
- package/bin/qt-web-cli-watch.js +0 -0
- package/bin/qt-web-cli.js +401 -100
- package/lib/BundleWatcher.js +192 -0
- package/lib/DevBuildManager.js +295 -0
- package/lib/DevServer.js +586 -0
- package/lib/HotReloader.js +142 -0
- package/lib/index.js +52 -122
- package/package.json +42 -19
- package/templates/dev-renderer.html +357 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotReloader - 基于 SSE 的热更新管理
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 管理 SSE 客户端连接
|
|
6
|
+
* 2. 接收文件变化事件,推送更新通知
|
|
7
|
+
* 3. 支持 full-reload 和 bundle-update 两种更新模式
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const signale = require('signale')
|
|
11
|
+
|
|
12
|
+
class HotReloader {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.clients = new Set()
|
|
15
|
+
this.lastEventType = null
|
|
16
|
+
this.lastEventData = null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 处理 SSE 连接请求
|
|
21
|
+
*/
|
|
22
|
+
handleSSE(req, res) {
|
|
23
|
+
res.writeHead(200, {
|
|
24
|
+
'Content-Type': 'text/event-stream',
|
|
25
|
+
'Cache-Control': 'no-cache',
|
|
26
|
+
Connection: 'keep-alive',
|
|
27
|
+
'Access-Control-Allow-Origin': '*',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// 发送初始连接成功消息
|
|
31
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`)
|
|
32
|
+
|
|
33
|
+
// 如果有最近的事件,立即发送
|
|
34
|
+
if (this.lastEventType) {
|
|
35
|
+
res.write(`data: ${JSON.stringify({ type: this.lastEventType, ...this.lastEventData })}\n\n`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.clients.add(res)
|
|
39
|
+
|
|
40
|
+
// 心跳保活
|
|
41
|
+
const heartbeat = setInterval(() => {
|
|
42
|
+
res.write(`:heartbeat\n\n`)
|
|
43
|
+
}, 15000)
|
|
44
|
+
|
|
45
|
+
req.on('close', () => {
|
|
46
|
+
clearInterval(heartbeat)
|
|
47
|
+
this.clients.delete(res)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 通知所有客户端 bundle 更新
|
|
53
|
+
*/
|
|
54
|
+
notifyBundleUpdate(data = {}) {
|
|
55
|
+
const event = {
|
|
56
|
+
type: 'bundle-update',
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
...data,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.lastEventType = 'bundle-update'
|
|
62
|
+
this.lastEventData = data
|
|
63
|
+
|
|
64
|
+
this._broadcast(event)
|
|
65
|
+
signale.info(`[HotReloader] 通知 ${this.clients.size} 个客户端: bundle-update`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 通知所有客户端整页刷新
|
|
70
|
+
*/
|
|
71
|
+
notifyFullReload(reason = 'file changed') {
|
|
72
|
+
const event = {
|
|
73
|
+
type: 'full-reload',
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
reason,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.lastEventType = 'full-reload'
|
|
79
|
+
this.lastEventData = { reason }
|
|
80
|
+
|
|
81
|
+
this._broadcast(event)
|
|
82
|
+
signale.info(`[HotReloader] 通知 ${this.clients.size} 个客户端: full-reload`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 通知构建状态变化
|
|
87
|
+
*/
|
|
88
|
+
notifyBuildStatus(status, message = '') {
|
|
89
|
+
const event = {
|
|
90
|
+
type: 'build-status',
|
|
91
|
+
status,
|
|
92
|
+
message,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this._broadcast(event)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 广播事件到所有客户端
|
|
101
|
+
*/
|
|
102
|
+
_broadcast(event) {
|
|
103
|
+
const data = `data: ${JSON.stringify(event)}\n\n`
|
|
104
|
+
const disconnected = []
|
|
105
|
+
|
|
106
|
+
for (const client of this.clients) {
|
|
107
|
+
try {
|
|
108
|
+
client.write(data)
|
|
109
|
+
} catch (e) {
|
|
110
|
+
disconnected.push(client)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 清理断开的连接
|
|
115
|
+
for (const client of disconnected) {
|
|
116
|
+
this.clients.delete(client)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 获取当前连接数
|
|
122
|
+
*/
|
|
123
|
+
getClientCount() {
|
|
124
|
+
return this.clients.size
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 关闭所有连接
|
|
129
|
+
*/
|
|
130
|
+
close() {
|
|
131
|
+
for (const client of this.clients) {
|
|
132
|
+
try {
|
|
133
|
+
client.end()
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// 忽略关闭错误
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.clients.clear()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = HotReloader
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QuickTVUI Web CLI
|
|
2
|
+
* QuickTVUI Web CLI v2
|
|
3
3
|
* 主入口 - 提供编程接口
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -23,144 +23,74 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* 检测项目的 dev 脚本
|
|
27
27
|
*/
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const deps = { ...projectPkg?.dependencies, ...projectPkg?.devDependencies }
|
|
35
|
-
if (deps['@quicktvui/web-renderer']) {
|
|
36
|
-
return 'package'
|
|
28
|
+
function detectDevScript(pkg, scriptName) {
|
|
29
|
+
const scripts = pkg.scripts || {}
|
|
30
|
+
const candidates = [scriptName, 'dev', 'dev:android', 'build:dev']
|
|
31
|
+
for (const name of candidates) {
|
|
32
|
+
if (name && scripts[name]) {
|
|
33
|
+
return { name, script: scripts[name] }
|
|
37
34
|
}
|
|
38
|
-
}
|
|
39
|
-
return
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
|
-
*
|
|
40
|
+
* 检测 dist/dev 目录下的 bundle 入口
|
|
44
41
|
*/
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
function detectDevBundle(projectRoot) {
|
|
43
|
+
const distDir = path.join(projectRoot, 'dist', 'dev')
|
|
44
|
+
if (!fs.existsSync(distDir)) return null
|
|
45
|
+
|
|
46
|
+
// 优先查找 index.bundle (v2 dev 模式)
|
|
47
|
+
const indexPath = path.join(distDir, 'index.bundle')
|
|
48
|
+
if (fs.existsSync(indexPath)) {
|
|
49
|
+
const stat = fs.statSync(indexPath)
|
|
50
|
+
return {
|
|
51
|
+
entry: 'index.bundle',
|
|
52
|
+
path: indexPath,
|
|
53
|
+
size: stat.size,
|
|
54
|
+
urlPath: '/dist/dev/index.bundle',
|
|
55
|
+
}
|
|
52
56
|
}
|
|
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
57
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
58
|
+
// 兼容 index.android.js (v1 模式)
|
|
59
|
+
const androidPath = path.join(distDir, 'index.android.js')
|
|
60
|
+
if (fs.existsSync(androidPath)) {
|
|
61
|
+
const stat = fs.statSync(androidPath)
|
|
62
|
+
return {
|
|
63
|
+
entry: 'index.android.js',
|
|
64
|
+
path: androidPath,
|
|
65
|
+
size: stat.size,
|
|
66
|
+
urlPath: '/dist/dev/index.android.js',
|
|
122
67
|
}
|
|
123
68
|
}
|
|
124
|
-
return r
|
|
125
|
-
}
|
|
126
69
|
|
|
127
|
-
|
|
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
|
-
`
|
|
70
|
+
return null
|
|
142
71
|
}
|
|
143
72
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
73
|
+
/**
|
|
74
|
+
* 检测 web renderer 类型(v1 兼容)
|
|
75
|
+
*/
|
|
76
|
+
function detectWebRendererType(projectRoot) {
|
|
77
|
+
if (fs.existsSync(path.join(projectRoot, 'src/web/index.js'))) {
|
|
78
|
+
return 'local'
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const projectPkg = require(path.join(projectRoot, 'package.json'))
|
|
82
|
+
const deps = { ...projectPkg?.dependencies, ...projectPkg?.devDependencies }
|
|
83
|
+
if (deps['@quicktvui/web-renderer']) {
|
|
84
|
+
return 'package'
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {}
|
|
87
|
+
return 'package'
|
|
159
88
|
}
|
|
160
89
|
|
|
161
90
|
module.exports = {
|
|
162
91
|
version: pkg.version,
|
|
163
92
|
findProjectRoot,
|
|
93
|
+
detectDevScript,
|
|
94
|
+
detectDevBundle,
|
|
164
95
|
detectWebRendererType,
|
|
165
|
-
generateWebEntryContent,
|
|
166
96
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quicktvui/web-cli",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "CLI tool for QuickTVUI web development -
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "CLI tool for QuickTVUI web development v2 - delegate build & bundle loading",
|
|
5
5
|
"author": "QuickTVUI Team",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"bin": {
|
|
@@ -21,28 +21,51 @@
|
|
|
21
21
|
"hippy"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"chokidar": "^3.5.3",
|
|
24
25
|
"minimist": "^1.2.8",
|
|
25
26
|
"shelljs": "^0.10.0",
|
|
26
|
-
"signale": "^1.4.0"
|
|
27
|
-
"webpack-merge": "^6.0.0",
|
|
28
|
-
"@quicktvui/web-renderer": "^1.0.26",
|
|
29
|
-
"scope-loader": "^1.0.3",
|
|
30
|
-
"chokidar": "^3.5.3",
|
|
31
|
-
"rimraf": "^5.0.0"
|
|
27
|
+
"signale": "^1.4.0"
|
|
32
28
|
},
|
|
33
29
|
"peerDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"webpack-cli": "^5.1.0",
|
|
36
|
-
"webpack-dev-server": "^4.15.0",
|
|
37
|
-
"vue-loader": "^17.0.0",
|
|
38
|
-
"html-webpack-plugin": "^5.5.0",
|
|
39
|
-
"@babel/core": "^7.23.0",
|
|
40
|
-
"babel-loader": "^9.1.0",
|
|
41
|
-
"ts-loader": "^9.4.0",
|
|
42
|
-
"vue": "^3.0.0",
|
|
43
|
-
"regenerator-runtime": "^0.14.0"
|
|
30
|
+
"vue": "^3.0.0"
|
|
44
31
|
},
|
|
45
32
|
"peerDependenciesMeta": {
|
|
33
|
+
"webpack": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"webpack-cli": {
|
|
37
|
+
"optional": true
|
|
38
|
+
},
|
|
39
|
+
"webpack-dev-server": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"vue-loader": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"html-webpack-plugin": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@babel/core": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"babel-loader": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"ts-loader": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"regenerator-runtime": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"webpack-merge": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@quicktvui/web-renderer": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"scope-loader": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
46
69
|
"less-loader": {
|
|
47
70
|
"optional": true
|
|
48
71
|
},
|
|
@@ -59,4 +82,4 @@
|
|
|
59
82
|
"engines": {
|
|
60
83
|
"node": ">=16.0.0"
|
|
61
84
|
}
|
|
62
|
-
}
|
|
85
|
+
}
|