Skip to content

Commit 86180e7

Browse files
committed
feat(cli): 添加本地web服务器功能用于UI展示
添加startWebServer函数实现本地web服务器,支持静态文件服务和SPA路由 修改配置文件和构建输出路径以支持本地开发模式 添加ui命令用于启动web服务器并支持自定义端口
1 parent 0a36af4 commit 86180e7

File tree

6 files changed

+160
-9
lines changed

6 files changed

+160
-9
lines changed

app/main.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UsbDevice, UsbIdsData, UsbVendor, VersionInfo } from '../src/types'
2+
import { UI_LOCAL_BASE_URL } from '../src/config'
23
import './styles.css'
34

45
// 类型定义
@@ -15,6 +16,8 @@ interface SearchOptions {
1516

1617
// 全局变量
1718
const version = import.meta.env.VERSION || 'latest'
19+
const useLocalData = import.meta.env.BASE_URL === UI_LOCAL_BASE_URL
20+
1821
let currentData: UsbIdsData = {}
1922
let currentResults: DeviceResult[] = []
2023
let currentPage = 1
@@ -267,7 +270,7 @@ function updateStats(): void {
267270
}
268271
}
269272

270-
// 版本信息相关函数
273+
// 远程数据获取
271274
async function loadDataFromNpm<T>(version: string, file: string): Promise<T> {
272275
try {
273276
// 从npm CDN获取指定版本的file
@@ -298,9 +301,25 @@ async function loadDataFromNpm<T>(version: string, file: string): Promise<T> {
298301
}
299302
}
300303

304+
async function loadDataFromLocal<T>(file: string): Promise<T> {
305+
try {
306+
const response = await fetch(file)
307+
if (response.ok) {
308+
return await response.json() as T
309+
}
310+
else {
311+
throw new Error(`Failed to fetch from local: ${response.status}`)
312+
}
313+
}
314+
catch (error) {
315+
console.warn('Failed to load local data:', error)
316+
return {} as T
317+
}
318+
}
319+
301320
async function loadVersionInfo(): Promise<void> {
302321
try {
303-
versionInfo = await loadDataFromNpm<VersionInfo>(version, 'usb.ids.version.json')
322+
versionInfo = useLocalData ? await loadDataFromLocal<VersionInfo>('usb.ids.version.json') : await loadDataFromNpm<VersionInfo>(version, 'usb.ids.version.json')
304323
updateVersionDisplay()
305324
startCountdown()
306325
}
@@ -505,8 +524,8 @@ async function initializeApp(): Promise<void> {
505524
countdown: document.getElementById('countdown') as HTMLElement,
506525
}
507526

508-
// 异步加载USB IDs数据 - 从最新的npm包获取
509-
const usbIdsData = await loadDataFromNpm<UsbIdsData>(version, 'usb.ids.json')
527+
// 异步加载USB IDs数据
528+
const usbIdsData = useLocalData ? await loadDataFromLocal<UsbIdsData>('usb.ids.json') : await loadDataFromNpm<UsbIdsData>(version, 'usb.ids.json')
510529
console.log('USB IDs Data loaded:', usbIdsData)
511530

512531
// 设置数据

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"module": "./dist/index.js",
2828
"types": "./dist/index.d.ts",
2929
"bin": {
30-
"usb-ids": "./bin/cli.cjs"
30+
"usb-ids": "./bin/cli.js"
3131
},
3232
"files": [
3333
"bin",
@@ -42,14 +42,15 @@
4242
"scripts": {
4343
"preinstall": "npx only-allow pnpm",
4444
"dev:app": "vite",
45-
"build:app": "vite build",
45+
"build:app": "vite build --outDir dist/ui",
4646
"dev:lib": "tsdown --watch",
4747
"build:lib": "tsdown",
4848
"lint": "eslint",
4949
"lint:fix": "eslint --fix",
5050
"fetch-usb-ids": "node ./bin/cli.cjs fetch",
5151
"version-info": "node ./bin/cli.cjs version",
5252
"check-update": "node ./bin/cli.cjs check",
53+
"ui": "tsx src/cli.ts ui",
5354
"release": "bumpp && pnpm publish",
5455
"start": "tsx src/index.ts",
5556
"test": "vitest run",

src/cli.ts

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
*/
77

88
import * as fs from 'node:fs'
9+
import { createServer } from 'node:http'
910
import * as path from 'node:path'
1011
import * as process from 'node:process'
11-
import { USB_IDS_SOURCE } from './config'
12+
import { UI_LOCAL_BASE_URL, USB_IDS_SOURCE } from './config'
1213
import { fetchUsbIdsData, loadVersionInfo, saveUsbIdsToFile, saveVersionInfo } from './core'
1314
import { shouldUpdate } from './parser'
1415
import { logger } from './utils'
@@ -135,6 +136,112 @@ export function checkUpdate(): void {
135136
}
136137
}
137138

139+
/**
140+
* 启动静态web服务器
141+
*/
142+
export async function startWebServer(port = 3000): Promise<void> {
143+
try {
144+
const root = process.cwd()
145+
const distDir = path.join(root, 'dist', 'ui')
146+
147+
// 检查dist/ui目录是否存在
148+
if (!fs.existsSync(distDir)) {
149+
logger.error('dist/ui目录不存在,请先运行构建命令: pnpm run build:app')
150+
process.exit(1)
151+
}
152+
153+
// 创建HTTP服务器
154+
const server = createServer((req, res) => {
155+
console.log('req url', req.url)
156+
// 重定向根路径到UI_LOCAL_BASE_URL
157+
if (req.url === '/') {
158+
res.writeHead(302, {
159+
Location: UI_LOCAL_BASE_URL,
160+
})
161+
res.end()
162+
return
163+
}
164+
165+
let filePath = path.join(distDir, req.url === UI_LOCAL_BASE_URL
166+
? 'index.html'
167+
: req.url?.replace(UI_LOCAL_BASE_URL, '') || '')
168+
169+
console.log('file path', filePath)
170+
171+
// 安全检查,防止路径遍历攻击
172+
if (!filePath.startsWith(distDir)) {
173+
res.writeHead(403)
174+
res.end('Forbidden')
175+
return
176+
}
177+
178+
// 处理usb.ids.json和usb.ids.version.json
179+
if (filePath.includes('usb.ids.json') || filePath.includes('usb.ids.version.json')) {
180+
// usb.ids.json和usb.ids.version.json与在dist同级目录
181+
filePath = path.join(root, req.url!.replace(UI_LOCAL_BASE_URL, ''))
182+
console.log('json file path', filePath)
183+
}
184+
185+
// 如果文件不存在,返回index.html(用于SPA路由)
186+
if (!fs.existsSync(filePath)) {
187+
filePath = path.join(distDir, 'index.html')
188+
}
189+
190+
// 读取文件
191+
fs.readFile(filePath, (err, data) => {
192+
if (err) {
193+
res.writeHead(404)
194+
res.end('Not Found')
195+
return
196+
}
197+
198+
// 设置Content-Type
199+
const ext = path.extname(filePath)
200+
const contentType = {
201+
'.html': 'text/html',
202+
'.js': 'application/javascript',
203+
'.css': 'text/css',
204+
'.json': 'application/json',
205+
'.png': 'image/png',
206+
'.jpg': 'image/jpeg',
207+
'.gif': 'image/gif',
208+
'.svg': 'image/svg+xml',
209+
'.ico': 'image/x-icon',
210+
}[ext] || 'text/plain'
211+
212+
res.writeHead(200, { 'Content-Type': contentType })
213+
res.end(data)
214+
})
215+
})
216+
217+
// 启动服务器
218+
server.listen(port, () => {
219+
logger.success(`usb.ids Web UI服务器已启动!`)
220+
logger.info(`访问地址: http://localhost:${port}${UI_LOCAL_BASE_URL}`)
221+
logger.info('按 Control+C 停止服务器')
222+
})
223+
224+
// 保持服务器运行,直到手动停止
225+
return new Promise<void>((resolve, reject) => {
226+
// 监听服务器错误
227+
server.on('error', (error) => {
228+
logger.error(`服务器错误: ${error.message}`)
229+
reject(error)
230+
})
231+
232+
// 服务器关闭时resolve Promise
233+
server.on('close', () => {
234+
logger.success('服务器已停止')
235+
resolve()
236+
})
237+
})
238+
}
239+
catch (error) {
240+
logger.error(`启动web服务器失败: ${(error as Error).message}`)
241+
process.exit(1)
242+
}
243+
}
244+
138245
/**
139246
* 显示帮助信息
140247
*/
@@ -150,16 +257,20 @@ USB设备数据管理工具
150257
update, fetch 更新USB设备数据
151258
version, info 显示当前版本信息
152259
check 检查是否需要更新
260+
ui 启动web界面服务器
153261
help 显示此帮助信息
154262
155263
选项:
156264
--force 强制更新(忽略时间检查)
265+
--port <port> 指定web服务器端口(默认3000)
157266
158267
示例:
159268
usb-ids update
160269
usb-ids update --force
161270
usb-ids version
162271
usb-ids check
272+
usb-ids ui
273+
usb-ids ui --port 8080
163274
`)
164275
}
165276

@@ -185,6 +296,23 @@ export async function runCli(): Promise<void> {
185296
checkUpdate()
186297
break
187298

299+
case 'ui': {
300+
// 解析端口参数
301+
const portIndex = args.indexOf('--port')
302+
let port = 3000
303+
if (portIndex !== -1 && args[portIndex + 1]) {
304+
const parsedPort = Number.parseInt(args[portIndex + 1], 10)
305+
if (!Number.isNaN(parsedPort) && parsedPort > 0 && parsedPort < 65536) {
306+
port = parsedPort
307+
}
308+
else {
309+
logger.error('无效的端口号,使用默认端口3000')
310+
}
311+
}
312+
await startWebServer(port)
313+
break
314+
}
315+
188316
case 'help':
189317
case '--help':
190318
case '-h':

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export const USB_IDS_SOURCE = [
22
'http://www.linux-usb.org/usb.ids',
33
'https://raw.githubusercontent.com/systemd/systemd/main/hwdb.d/usb.ids',
44
]
5+
6+
export const UI_LOCAL_BASE_URL = '/__usb_ids__/'

tsdown.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default defineConfig((_) => {
2828
platform: 'node',
2929
entry: ['src/cli.ts'],
3030
outDir: 'bin',
31-
format: 'cjs',
31+
format: 'esm',
3232
dts: false,
3333
},
3434
]

vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import process from 'node:process'
22
import { defineConfig, loadEnv } from 'vite'
33
import pkg from './package.json'
4+
import { UI_LOCAL_BASE_URL } from './src/config'
45

56
export default defineConfig(({ command }) => {
67
const env = loadEnv(command, process.cwd())
78

8-
const base = env.VITE_BASE_PATH || '/'
9+
const base = env.VITE_BASE_PATH || UI_LOCAL_BASE_URL
910
console.log('vite base ', base)
1011

1112
const version = pkg.version

0 commit comments

Comments
 (0)