@zhin.js/console 1.0.6 → 1.0.8
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 +86 -21
- package/app/bin.ts +52 -0
- package/app/build.ts +211 -0
- package/app/dev.ts +83 -0
- package/app/index.ts +193 -0
- package/app/websocket.ts +109 -0
- package/client/index.html +13 -0
- package/client/postcss.config.js +5 -0
- package/client/public/vendor/react-dom.production.min.js +1 -0
- package/client/public/vendor/react.production.min.js +1 -0
- package/client/src/components/PluginConfigForm/BasicFieldRenderers.tsx +253 -0
- package/client/src/components/PluginConfigForm/CollectionFieldRenderers.tsx +261 -0
- package/client/src/components/PluginConfigForm/CompositeFieldRenderers.tsx +105 -0
- package/client/src/components/PluginConfigForm/FieldRenderer.tsx +110 -0
- package/client/src/components/PluginConfigForm/NestedFieldRenderer.tsx +95 -0
- package/client/src/components/PluginConfigForm/index.tsx +237 -0
- package/client/src/components/PluginConfigForm/types.ts +46 -0
- package/client/src/components/ThemeToggle.tsx +21 -0
- package/client/src/hooks/useTheme.ts +17 -0
- package/client/src/layouts/dashboard.tsx +259 -0
- package/client/src/main.tsx +121 -0
- package/client/src/pages/dashboard-bots.tsx +198 -0
- package/client/src/pages/dashboard-home.tsx +301 -0
- package/client/src/pages/dashboard-logs.tsx +298 -0
- package/client/src/pages/dashboard-plugin-detail.tsx +360 -0
- package/client/src/pages/dashboard-plugins.tsx +166 -0
- package/client/src/style.css +1105 -0
- package/client/src/theme/index.ts +92 -0
- package/client/tailwind.config.js +85 -0
- package/client/tsconfig.json +17 -0
- package/lib/bin.d.ts +3 -0
- package/lib/bin.d.ts.map +1 -0
- package/lib/bin.js +45 -0
- package/lib/bin.js.map +1 -0
- package/lib/build.d.ts +33 -0
- package/lib/build.d.ts.map +1 -0
- package/lib/build.js +168 -0
- package/lib/build.js.map +1 -0
- package/lib/dev.d.ts +16 -0
- package/lib/dev.d.ts.map +1 -0
- package/lib/dev.js +70 -0
- package/lib/dev.js.map +1 -0
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +107 -269
- package/lib/index.js.map +1 -1
- package/lib/websocket.d.ts +14 -0
- package/lib/websocket.d.ts.map +1 -0
- package/lib/websocket.js +85 -0
- package/lib/websocket.js.map +1 -0
- package/package.json +28 -10
- package/src/index.ts +0 -370
package/README.md
CHANGED
|
@@ -15,11 +15,12 @@ Zhin 机器人框架的 Web 控制台插件,提供开发环境下的可视化
|
|
|
15
15
|
## 技术架构
|
|
16
16
|
|
|
17
17
|
- **构建工具**: Vite 7.x
|
|
18
|
-
- **前端框架**:
|
|
18
|
+
- **前端框架**: React 18 + React Router 7 + TypeScript
|
|
19
|
+
- **UI 组件库**: Radix UI + Tailwind CSS
|
|
20
|
+
- **状态管理**: Redux Toolkit + Redux Persist
|
|
19
21
|
- **开发服务器**: 集成到 Koa 路由
|
|
20
22
|
- **WebSocket**: 实时数据同步
|
|
21
|
-
-
|
|
22
|
-
- **UI组件**: PrimeVue 自动导入
|
|
23
|
+
- **构建优化**: Vendor Chunks 分割,支持插件复用公共依赖
|
|
23
24
|
|
|
24
25
|
## 安装
|
|
25
26
|
|
|
@@ -55,13 +56,30 @@ http://localhost:8086/vite/
|
|
|
55
56
|
|
|
56
57
|
```typescript
|
|
57
58
|
interface WebServer {
|
|
58
|
-
vite
|
|
59
|
+
vite?: ViteDevServer // Vite开发服务器
|
|
59
60
|
addEntry(entry: string): () => void // 添加入口文件
|
|
60
61
|
entries: Record<string, string> // 入口文件映射
|
|
61
62
|
ws: WebSocketServer // WebSocket服务器
|
|
62
63
|
}
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
### 构建优化
|
|
67
|
+
|
|
68
|
+
Console 插件采用智能的构建优化策略,显著减少重复打包:
|
|
69
|
+
|
|
70
|
+
- **Vendor Chunks 分割**: 将公共依赖分割成独立的 JS 文件
|
|
71
|
+
- `vendor-react.js` - React 核心库 (~190KB)
|
|
72
|
+
- `vendor-ui.js` - UI 组件库 (~250KB)
|
|
73
|
+
- `vendor-redux.js` - 状态管理 (~23KB)
|
|
74
|
+
- 其他分组...
|
|
75
|
+
|
|
76
|
+
- **插件依赖复用**: 其他插件构建时自动外部化公共依赖
|
|
77
|
+
- 插件体积减少 ~90% (从 650KB → 30KB)
|
|
78
|
+
- 浏览器缓存复用,提升加载速度
|
|
79
|
+
- 开发和生产环境统一体验
|
|
80
|
+
|
|
81
|
+
详见 [BUILD_OPTIMIZATION.md](./BUILD_OPTIMIZATION.md)
|
|
82
|
+
|
|
65
83
|
### 实时数据同步
|
|
66
84
|
|
|
67
85
|
- 📡 WebSocket 连接管理
|
|
@@ -82,20 +100,45 @@ interface WebServer {
|
|
|
82
100
|
|
|
83
101
|
```javascript
|
|
84
102
|
{
|
|
85
|
-
root: '
|
|
103
|
+
root: 'plugins/console/client',
|
|
86
104
|
base: '/vite/',
|
|
87
105
|
plugins: [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
resolvers: [PrimeVueResolver()]
|
|
91
|
-
})
|
|
106
|
+
react(),
|
|
107
|
+
tailwindcss()
|
|
92
108
|
],
|
|
93
109
|
server: {
|
|
94
110
|
middlewareMode: true
|
|
111
|
+
},
|
|
112
|
+
build: {
|
|
113
|
+
rollupOptions: {
|
|
114
|
+
output: {
|
|
115
|
+
manualChunks: {
|
|
116
|
+
// 自动分割 vendor chunks
|
|
117
|
+
'vendor-react': ['react', 'react-dom'],
|
|
118
|
+
'vendor-ui': ['@radix-ui/themes', 'lucide-react'],
|
|
119
|
+
'vendor-redux': ['@reduxjs/toolkit', 'redux-persist'],
|
|
120
|
+
// ...
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
```
|
|
98
127
|
|
|
128
|
+
### 插件客户端构建
|
|
129
|
+
|
|
130
|
+
使用 `zhin-client` 工具构建插件客户端代码:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 在插件目录下
|
|
134
|
+
npx zhin-client build
|
|
135
|
+
|
|
136
|
+
# 或使用相对路径
|
|
137
|
+
node ../../plugins/console/lib/bin.js build
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
配置会自动外部化公共依赖,生成轻量级的插件代码。
|
|
141
|
+
|
|
99
142
|
### 路由配置
|
|
100
143
|
|
|
101
144
|
- 支持所有路由通过 Vite 处理
|
|
@@ -108,18 +151,38 @@ interface WebServer {
|
|
|
108
151
|
### 项目结构
|
|
109
152
|
|
|
110
153
|
```
|
|
111
|
-
|
|
112
|
-
├──
|
|
113
|
-
|
|
154
|
+
console/
|
|
155
|
+
├── app/ # 构建工具
|
|
156
|
+
│ ├── index.ts # Console 插件主入口
|
|
157
|
+
│ ├── build.ts # 构建逻辑 (buildConsoleClient, buildPluginClient)
|
|
158
|
+
│ ├── dev.ts # Vite 开发服务器
|
|
159
|
+
│ ├── websocket.ts # WebSocket 管理
|
|
160
|
+
│ └── bin.ts # CLI 工具
|
|
161
|
+
├── client/ # 前端应用
|
|
162
|
+
│ ├── src/ # React 应用源码
|
|
163
|
+
│ ├── index.html # SPA 入口
|
|
164
|
+
│ └── ...
|
|
165
|
+
├── dist/ # 构建产物
|
|
166
|
+
│ ├── assets/
|
|
167
|
+
│ │ ├── vendor-react-*.js # React vendor chunk
|
|
168
|
+
│ │ ├── vendor-ui-*.js # UI vendor chunk
|
|
169
|
+
│ │ └── ...
|
|
170
|
+
│ └── index.html
|
|
171
|
+
└── lib/ # TypeScript 编译产物
|
|
114
172
|
```
|
|
115
173
|
|
|
116
174
|
### 构建
|
|
117
175
|
|
|
118
176
|
```bash
|
|
119
|
-
npm run build
|
|
120
|
-
npm run
|
|
177
|
+
npm run build # 构建插件 (TypeScript)
|
|
178
|
+
npm run build:client # 构建客户端 (React SPA)
|
|
179
|
+
npm run clean # 清理构建文件
|
|
121
180
|
```
|
|
122
181
|
|
|
182
|
+
构建产物说明:
|
|
183
|
+
- `lib/` - Node.js 运行的插件代码
|
|
184
|
+
- `dist/` - 浏览器加载的客户端代码,包含分割的 vendor chunks
|
|
185
|
+
|
|
123
186
|
## WebSocket API
|
|
124
187
|
|
|
125
188
|
### 消息类型
|
|
@@ -138,16 +201,18 @@ npm run clean # 清理构建文件
|
|
|
138
201
|
## 依赖项
|
|
139
202
|
|
|
140
203
|
### 核心依赖
|
|
141
|
-
- `@vitejs/plugin-
|
|
142
|
-
- `
|
|
143
|
-
- `
|
|
204
|
+
- `@vitejs/plugin-react` - React 插件支持
|
|
205
|
+
- `@tailwindcss/vite` - Tailwind CSS 集成
|
|
206
|
+
- `koa-connect` - Koa 中间件集成
|
|
207
|
+
- `react` / `react-dom` - React 框架
|
|
208
|
+
- `react-router` - 路由管理
|
|
209
|
+
- `@reduxjs/toolkit` - 状态管理
|
|
210
|
+
- `@radix-ui/themes` - UI 组件库
|
|
144
211
|
- `vite` - 构建工具
|
|
145
212
|
|
|
146
213
|
### 对等依赖
|
|
147
|
-
- `@zhin.js/client` -
|
|
148
|
-
- `@zhin.js/http` - HTTP服务器
|
|
149
|
-
- `unplugin-vue-components` - 组件自动导入
|
|
150
|
-
- `@primevue/auto-import-resolver` - PrimeVue组件解析
|
|
214
|
+
- `@zhin.js/client` - 客户端基础库
|
|
215
|
+
- `@zhin.js/http` - HTTP 服务器
|
|
151
216
|
|
|
152
217
|
## 使用场景
|
|
153
218
|
|
package/app/bin.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { buildCurrentPlugin, buildConsoleClient } from "./build.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
try {
|
|
12
|
+
switch (command) {
|
|
13
|
+
case "build":
|
|
14
|
+
// 构建当前目录的插件客户端代码
|
|
15
|
+
console.log("🔨 Building plugin client...");
|
|
16
|
+
await buildCurrentPlugin();
|
|
17
|
+
break;
|
|
18
|
+
|
|
19
|
+
case "build:console":
|
|
20
|
+
// 构建 console 插件的客户端代码
|
|
21
|
+
console.log("🔨 Building console client...");
|
|
22
|
+
const consoleRoot = path.resolve(
|
|
23
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
24
|
+
".."
|
|
25
|
+
);
|
|
26
|
+
await buildConsoleClient({ consoleRoot });
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
default:
|
|
30
|
+
console.log(`
|
|
31
|
+
Zhin.js Client Builder
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
zhin-client build Build current plugin's client code
|
|
35
|
+
zhin-client build:console Build console plugin's client code (SPA mode)
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
# Build a plugin (single file mode)
|
|
39
|
+
cd my-plugin && zhin-client build
|
|
40
|
+
|
|
41
|
+
# Build console (SPA mode)
|
|
42
|
+
zhin-client build:console
|
|
43
|
+
`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("❌ Build failed:", error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main();
|
package/app/build.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { build, searchForWorkspaceRoot } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
export interface BuildOptions {
|
|
8
|
+
/** 插件根目录 */
|
|
9
|
+
pluginRoot: string;
|
|
10
|
+
/** 输出目录,默认为 pluginRoot/dist */
|
|
11
|
+
outDir?: string;
|
|
12
|
+
/** 是否启用 tailwindcss,默认 true */
|
|
13
|
+
enableTailwind?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConsoleBuildOptions {
|
|
17
|
+
/** Console 插件根目录 */
|
|
18
|
+
consoleRoot: string;
|
|
19
|
+
/** 输出目录,默认为 consoleRoot/dist */
|
|
20
|
+
outDir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 查找插件的客户端入口文件
|
|
25
|
+
* @param pluginRoot 插件根目录
|
|
26
|
+
* @returns 入口文件路径,如果不存在则返回 null
|
|
27
|
+
*/
|
|
28
|
+
function findClientEntry(pluginRoot: string): string | null {
|
|
29
|
+
const possibleEntries = [
|
|
30
|
+
path.join(pluginRoot, "client/index.tsx"),
|
|
31
|
+
path.join(pluginRoot, "client/index.ts"),
|
|
32
|
+
path.join(pluginRoot, "src/main.tsx"),
|
|
33
|
+
path.join(pluginRoot, "src/main.ts"),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const entry of possibleEntries) {
|
|
37
|
+
if (fs.existsSync(entry)) {
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 验证构建环境
|
|
47
|
+
* @param pluginRoot 插件根目录
|
|
48
|
+
* @throws 如果环境不满足构建要求
|
|
49
|
+
*/
|
|
50
|
+
function validateBuildEnvironment(pluginRoot: string): void {
|
|
51
|
+
if (!fs.existsSync(pluginRoot)) {
|
|
52
|
+
throw new Error(`Plugin root directory does not exist: ${pluginRoot}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entry = findClientEntry(pluginRoot);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`No client entry file found in ${pluginRoot}. Looking for: client/index.tsx, client/index.ts, src/main.tsx, or src/main.ts`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 构建插件的客户端代码(单文件模式)
|
|
65
|
+
* 用于构建普通插件的 client/index.tsx 文件
|
|
66
|
+
*
|
|
67
|
+
* 策略:将公共依赖配置为 external,运行时从 console 加载的 vendor chunks 中复用
|
|
68
|
+
* @param options 构建选项
|
|
69
|
+
*/
|
|
70
|
+
export async function buildPluginClient(options: BuildOptions): Promise<void> {
|
|
71
|
+
const {
|
|
72
|
+
pluginRoot,
|
|
73
|
+
outDir = path.join(pluginRoot, "dist"),
|
|
74
|
+
enableTailwind = true,
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
// 验证构建环境
|
|
78
|
+
validateBuildEnvironment(pluginRoot);
|
|
79
|
+
|
|
80
|
+
const entry = findClientEntry(pluginRoot);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
throw new Error(`No client entry file found in ${pluginRoot}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const plugins = [react()];
|
|
86
|
+
if (enableTailwind) {
|
|
87
|
+
plugins.push(tailwindcss());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 构建配置 - 库模式
|
|
91
|
+
const clientRoot = path.dirname(entry);
|
|
92
|
+
|
|
93
|
+
await build({
|
|
94
|
+
root: clientRoot,
|
|
95
|
+
plugins,
|
|
96
|
+
build: {
|
|
97
|
+
outDir,
|
|
98
|
+
emptyOutDir: true,
|
|
99
|
+
lib: {
|
|
100
|
+
entry,
|
|
101
|
+
formats: ["es"],
|
|
102
|
+
fileName: "index",
|
|
103
|
+
},
|
|
104
|
+
rollupOptions: {
|
|
105
|
+
makeAbsoluteExternalsRelative: true,
|
|
106
|
+
external:[
|
|
107
|
+
'react',
|
|
108
|
+
'react-dom',
|
|
109
|
+
'react/jsx-runtime',
|
|
110
|
+
'clsx',
|
|
111
|
+
'tailwind-merge',
|
|
112
|
+
'lucide-react',
|
|
113
|
+
'@radix-ui/themes'
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
resolve:{
|
|
118
|
+
dedupe: [
|
|
119
|
+
"react",
|
|
120
|
+
"react-dom",
|
|
121
|
+
"clsx",
|
|
122
|
+
"tailwind-merge",
|
|
123
|
+
],
|
|
124
|
+
alias: {
|
|
125
|
+
"@": path.resolve(pluginRoot, "client/src"),
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log(`✅ Plugin client code built successfully: ${outDir}`);
|
|
131
|
+
console.log(`📦 External dependencies will be loaded from console vendor chunks`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 构建 Console 插件的客户端代码(SPA 应用模式)
|
|
136
|
+
* Console 有完整的 index.html 和 src 目录结构
|
|
137
|
+
* @param options Console 构建选项
|
|
138
|
+
*/
|
|
139
|
+
export async function buildConsoleClient(
|
|
140
|
+
options: ConsoleBuildOptions
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const { consoleRoot, outDir = path.join(consoleRoot, "dist") } = options;
|
|
143
|
+
|
|
144
|
+
const clientRoot = path.join(consoleRoot, "client");
|
|
145
|
+
|
|
146
|
+
// 检查 client 目录是否存在
|
|
147
|
+
if (!fs.existsSync(clientRoot)) {
|
|
148
|
+
throw new Error(`Console client directory does not exist: ${clientRoot}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 检查 index.html 是否存在
|
|
152
|
+
const indexHtml = path.join(clientRoot, "index.html");
|
|
153
|
+
if (!fs.existsSync(indexHtml)) {
|
|
154
|
+
throw new Error(`index.html not found in: ${clientRoot}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const workspaceRoot = searchForWorkspaceRoot(consoleRoot);
|
|
158
|
+
const consoleClientRoot=path.resolve(workspaceRoot, "plugins/client/client")
|
|
159
|
+
const plugins = [react(), tailwindcss()];
|
|
160
|
+
|
|
161
|
+
await build({
|
|
162
|
+
root: clientRoot,
|
|
163
|
+
plugins,
|
|
164
|
+
build: {
|
|
165
|
+
outDir,
|
|
166
|
+
emptyOutDir: true,
|
|
167
|
+
// 设置最小 chunk 大小,避免过度分割
|
|
168
|
+
chunkSizeWarningLimit: 1000,
|
|
169
|
+
// SPA 应用模式,不是库模式
|
|
170
|
+
rollupOptions: {
|
|
171
|
+
input: indexHtml,
|
|
172
|
+
// 保留导出签名
|
|
173
|
+
preserveEntrySignatures: 'strict',
|
|
174
|
+
output: {
|
|
175
|
+
// 确保文件名稳定,不使用哈希,方便插件引用
|
|
176
|
+
chunkFileNames: '[name].js',
|
|
177
|
+
entryFileNames: '[name].js',
|
|
178
|
+
assetFileNames: '[name].[ext]',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
resolve: {
|
|
183
|
+
dedupe: [
|
|
184
|
+
"react",
|
|
185
|
+
"react-dom",
|
|
186
|
+
"clsx",
|
|
187
|
+
"tailwind-merge",
|
|
188
|
+
"@reduxjs/toolkit",
|
|
189
|
+
"react-router",
|
|
190
|
+
"react-redux",
|
|
191
|
+
"redux-persist",
|
|
192
|
+
],
|
|
193
|
+
alias: {
|
|
194
|
+
"@zhin.js/client": consoleClientRoot,
|
|
195
|
+
"@": path.resolve(clientRoot, "src"),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log(`✅ Console client built successfully: ${outDir}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 构建当前目录的插件客户端代码
|
|
205
|
+
*/
|
|
206
|
+
export async function buildCurrentPlugin(): Promise<void> {
|
|
207
|
+
const currentDir = process.cwd();
|
|
208
|
+
await buildPluginClient({
|
|
209
|
+
pluginRoot: currentDir,
|
|
210
|
+
});
|
|
211
|
+
}
|
package/app/dev.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ViteDevServer, createServer, searchForWorkspaceRoot } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export interface DevServerOptions {
|
|
7
|
+
/** 客户端代码根目录 */
|
|
8
|
+
root: string;
|
|
9
|
+
/** 基础路径,默认 /vite/ */
|
|
10
|
+
base?: string;
|
|
11
|
+
/** 是否启用 tailwindcss,默认 true */
|
|
12
|
+
enableTailwind?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 创建 Vite 开发服务器
|
|
17
|
+
* @param options 开发服务器选项
|
|
18
|
+
* @returns Vite 开发服务器实例
|
|
19
|
+
*/
|
|
20
|
+
export async function createViteDevServer(
|
|
21
|
+
options: DevServerOptions
|
|
22
|
+
): Promise<ViteDevServer> {
|
|
23
|
+
const { root, base = "/vite/", enableTailwind = true } = options;
|
|
24
|
+
|
|
25
|
+
const plugins = [react()];
|
|
26
|
+
if (enableTailwind) {
|
|
27
|
+
plugins.push(tailwindcss());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return await await createServer({
|
|
31
|
+
root,
|
|
32
|
+
base,
|
|
33
|
+
plugins: [react(), tailwindcss()],
|
|
34
|
+
server: {
|
|
35
|
+
middlewareMode: true,
|
|
36
|
+
fs: {
|
|
37
|
+
strict: false,
|
|
38
|
+
// 添加文件访问过滤,避免访问特殊文件
|
|
39
|
+
allow: [
|
|
40
|
+
// 允许访问的目录
|
|
41
|
+
root,
|
|
42
|
+
searchForWorkspaceRoot(root),
|
|
43
|
+
path.resolve(process.cwd(), 'node_modules'),
|
|
44
|
+
path.resolve(process.cwd(), 'client'),
|
|
45
|
+
path.resolve(process.cwd(), 'src'),
|
|
46
|
+
],
|
|
47
|
+
// 拒绝访问某些文件模式
|
|
48
|
+
deny: [
|
|
49
|
+
'**/.git/**',
|
|
50
|
+
'**/node_modules/.cache/**',
|
|
51
|
+
'**/*.socket',
|
|
52
|
+
'**/*.pipe',
|
|
53
|
+
'**/Dockerfile*',
|
|
54
|
+
'**/.env*',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
resolve: {
|
|
59
|
+
dedupe: [
|
|
60
|
+
"react",
|
|
61
|
+
"react-dom",
|
|
62
|
+
"clsx",
|
|
63
|
+
"tailwind-merge",
|
|
64
|
+
"@reduxjs/toolkit",
|
|
65
|
+
"react-router",
|
|
66
|
+
"react-redux",
|
|
67
|
+
"redux-persist",
|
|
68
|
+
],
|
|
69
|
+
alias: {
|
|
70
|
+
"@zhin.js/client": path.resolve(root, "../../client/client"),
|
|
71
|
+
"@": path.resolve(root, "../client/src"),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
optimizeDeps: {
|
|
75
|
+
include: ["react", "react-dom"],
|
|
76
|
+
},
|
|
77
|
+
build: {
|
|
78
|
+
rollupOptions: {
|
|
79
|
+
input: root + "/index.html",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
package/app/index.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { register, useContext,useLogger } from "@zhin.js/core";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import { ViteDevServer } from "vite";
|
|
4
|
+
import mime from "mime";
|
|
5
|
+
import connect from "koa-connect";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { createViteDevServer } from "./dev.js";
|
|
9
|
+
import { setupWebSocket,notifyDataUpdate } from "./websocket.js";
|
|
10
|
+
|
|
11
|
+
declare module "@zhin.js/types" {
|
|
12
|
+
interface GlobalContext {
|
|
13
|
+
web: WebServer;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export type WebEntry =
|
|
17
|
+
| string
|
|
18
|
+
| {
|
|
19
|
+
production: string;
|
|
20
|
+
development: string;
|
|
21
|
+
};
|
|
22
|
+
export type WebServer = {
|
|
23
|
+
vite?: ViteDevServer;
|
|
24
|
+
addEntry(entry: WebEntry): () => void;
|
|
25
|
+
entries: Record<string, string>;
|
|
26
|
+
ws: WebSocketServer;
|
|
27
|
+
};
|
|
28
|
+
const logger=useLogger()
|
|
29
|
+
const createSyncMsg = (key: string, value: any) => {
|
|
30
|
+
return {
|
|
31
|
+
type: "sync",
|
|
32
|
+
data: {
|
|
33
|
+
key,
|
|
34
|
+
value,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const createAddMsg = (key: string, value: any) => {
|
|
39
|
+
return {
|
|
40
|
+
type: "add",
|
|
41
|
+
data: {
|
|
42
|
+
key,
|
|
43
|
+
value,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const createDeleteMsg = (key: string, value: any) => {
|
|
48
|
+
return {
|
|
49
|
+
type: "delete",
|
|
50
|
+
data: {
|
|
51
|
+
key,
|
|
52
|
+
value,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
useContext("router", async (router) => {
|
|
57
|
+
const base = "/vite/";
|
|
58
|
+
|
|
59
|
+
const webServer: WebServer = {
|
|
60
|
+
entries: {},
|
|
61
|
+
addEntry(entry) {
|
|
62
|
+
const hash =
|
|
63
|
+
Date.now().toString(16) + Math.random().toString(16).slice(2, 8);
|
|
64
|
+
const entryFile =
|
|
65
|
+
typeof entry === "string"
|
|
66
|
+
? entry
|
|
67
|
+
: entry[
|
|
68
|
+
(process.env.NODE_ENV as "development" | "production") ||
|
|
69
|
+
"development"
|
|
70
|
+
];
|
|
71
|
+
this.entries[hash] = `/vite/@fs/${entryFile}`
|
|
72
|
+
for (const ws of this.ws.clients || []) {
|
|
73
|
+
ws.send(JSON.stringify(createAddMsg("entries", this.entries[hash])));
|
|
74
|
+
}
|
|
75
|
+
return () => {
|
|
76
|
+
for (const ws of this.ws.clients || []) {
|
|
77
|
+
ws.send(
|
|
78
|
+
JSON.stringify(createDeleteMsg("entries", this.entries[hash]))
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
delete this.entries[hash];
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
ws: router.ws("/server"),
|
|
85
|
+
};
|
|
86
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
87
|
+
const root = isDev
|
|
88
|
+
? path.join(import.meta.dirname, "../client")
|
|
89
|
+
: path.join(import.meta.dirname, "../dist");
|
|
90
|
+
logger.info({isDev,root})
|
|
91
|
+
if (isDev) {
|
|
92
|
+
webServer.vite = await await createViteDevServer({
|
|
93
|
+
root,
|
|
94
|
+
base,
|
|
95
|
+
enableTailwind: true,
|
|
96
|
+
});
|
|
97
|
+
// Vite 中间件 - 必须在其他路由之前
|
|
98
|
+
router.use((ctx, next) => {
|
|
99
|
+
if (ctx.request.originalUrl.startsWith("/api")) return next();
|
|
100
|
+
return connect(webServer.vite!.middlewares)(ctx, next);
|
|
101
|
+
});
|
|
102
|
+
}else{
|
|
103
|
+
router.use((ctx, next) => {
|
|
104
|
+
if (ctx.request.originalUrl.startsWith("/api")) return next();
|
|
105
|
+
if(!ctx.path.startsWith('/vite/@fs/')) return next();
|
|
106
|
+
const filename=ctx.path.replace(`/vite/@fs/`,'')
|
|
107
|
+
if(!fs.existsSync(filename)) return next();
|
|
108
|
+
ctx.type = mime.getType(filename) || path.extname(filename);
|
|
109
|
+
ctx.body = fs.createReadStream(filename);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// SPA 回退路由 - 处理所有未匹配的路由
|
|
114
|
+
router.all("*all", async (ctx, next) => {
|
|
115
|
+
const url = ctx.request.originalUrl.replace(base, "");
|
|
116
|
+
const name = isDev ? ctx.path.slice(1) : ctx.path.slice(1);
|
|
117
|
+
const sendFile = (filename: string) => {
|
|
118
|
+
console.log(`发送文件: ${filename}`);
|
|
119
|
+
// 安全检查:确保是常规文件
|
|
120
|
+
try {
|
|
121
|
+
const stat = fs.statSync(filename);
|
|
122
|
+
if (!stat.isFile()) {
|
|
123
|
+
ctx.status = 404;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
ctx.status = 404;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ctx.type = path.extname(filename);
|
|
132
|
+
ctx.type = mime.getType(filename) || ctx.type;
|
|
133
|
+
return (ctx.body = fs.createReadStream(filename));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 1. 检查是否是动态入口
|
|
137
|
+
if (Object.keys(webServer.entries).includes(name)) {
|
|
138
|
+
return sendFile(path.resolve(process.cwd(), webServer.entries[name]));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. 检查是否是静态文件
|
|
142
|
+
const filename = path.resolve(root, name);
|
|
143
|
+
if (filename.startsWith(root) || filename.includes("node_modules")) {
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(filename)) {
|
|
146
|
+
const fileState = fs.statSync(filename);
|
|
147
|
+
// 只处理常规文件,忽略目录、socket、符号链接等
|
|
148
|
+
if (
|
|
149
|
+
fileState.isFile() &&
|
|
150
|
+
!fileState.isSocket() &&
|
|
151
|
+
!fileState.isFIFO()
|
|
152
|
+
) {
|
|
153
|
+
return sendFile(filename);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// 忽略文件系统错误,继续处理
|
|
158
|
+
console.warn(`文件访问错误: ${filename}`, (error as Error).message);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// 安全检查:路径不在允许范围内
|
|
162
|
+
return (ctx.status = 403);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 3. 所有其他路径(包括 SPA 路由)都返回 index.html
|
|
166
|
+
// 这样前端路由可以正确处理
|
|
167
|
+
const indexFile = path.resolve(root, "index.html");
|
|
168
|
+
if(!isDev) return sendFile(indexFile);
|
|
169
|
+
const template = fs.readFileSync(indexFile, "utf8");
|
|
170
|
+
ctx.type = "html";
|
|
171
|
+
ctx.body = await webServer.vite!.transformIndexHtml(url, template);
|
|
172
|
+
});
|
|
173
|
+
// 定时通知客户端更新数据
|
|
174
|
+
const dataUpdateInterval = setInterval(() => {
|
|
175
|
+
notifyDataUpdate(webServer);
|
|
176
|
+
}, 5000); // 每5秒通知一次更新
|
|
177
|
+
setupWebSocket(webServer);
|
|
178
|
+
// 插件卸载时清理定时器
|
|
179
|
+
process.on("exit", () => {
|
|
180
|
+
clearInterval(dataUpdateInterval);
|
|
181
|
+
});
|
|
182
|
+
register({
|
|
183
|
+
name: "web",
|
|
184
|
+
description: "web服务",
|
|
185
|
+
async mounted() {
|
|
186
|
+
return webServer;
|
|
187
|
+
},
|
|
188
|
+
async dispose(server) {
|
|
189
|
+
await server.vite?.close();
|
|
190
|
+
server.ws.close();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
});
|