@quicktvui/web-cli 1.0.7 → 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.
@@ -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
- * 检测 web renderer 类型
26
+ * 检测项目的 dev 脚本
27
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'
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
- } catch (e) {}
39
- return 'package'
35
+ }
36
+ return null
40
37
  }
41
38
 
42
39
  /**
43
- * 生成 web 入口内容
40
+ * 检测 dist/dev 目录下的 bundle 入口
44
41
  */
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)
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
- 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
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
- 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
- `
70
+ return null
142
71
  }
143
72
 
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
- `
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.7",
4
- "description": "CLI tool for QuickTVUI web development - zero configuration",
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
- "webpack": "^5.89.0",
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
+ }