@zhin.js/console 1.0.40 → 1.0.42

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/lib/index.js CHANGED
@@ -1,40 +1,45 @@
1
1
  import { usePlugin } from '@zhin.js/core';
2
2
  import mime from 'mime';
3
- import * as fs2 from 'fs';
4
- import * as path2 from 'path';
3
+ import * as fs from 'fs';
4
+ import fs__default from 'fs';
5
+ import * as path3 from 'path';
6
+ import path3__default from 'path';
5
7
  import WebSocket from 'ws';
6
8
  import { transform } from 'esbuild';
7
9
 
8
10
  // src/index.ts
9
11
  var { root, logger } = usePlugin();
12
+ var ENV_WHITELIST = [".env", ".env.development", ".env.production"];
13
+ function resolveConfigKey(pluginName) {
14
+ const schemaService = root.inject("schema");
15
+ return schemaService?.resolveConfigKey(pluginName) ?? pluginName;
16
+ }
17
+ function getPluginKeys() {
18
+ const schemaService = root.inject("schema");
19
+ if (!schemaService) return [];
20
+ const keys = /* @__PURE__ */ new Set();
21
+ for (const [, configKey] of schemaService.getPluginKeyMap()) {
22
+ keys.add(configKey);
23
+ }
24
+ return Array.from(keys);
25
+ }
26
+ function getConfigFilePath() {
27
+ return path3__default.resolve(process.cwd(), "zhin.config.yml");
28
+ }
10
29
  function setupWebSocket(webServer) {
11
30
  webServer.ws.on("connection", (ws) => {
12
- ws.send(
13
- JSON.stringify({
14
- type: "sync",
15
- data: {
16
- key: "entries",
17
- value: Object.values(webServer.entries)
18
- }
19
- })
20
- );
21
- ws.send(
22
- JSON.stringify({
23
- type: "init-data",
24
- timestamp: Date.now()
25
- })
26
- );
31
+ ws.send(JSON.stringify({
32
+ type: "sync",
33
+ data: { key: "entries", value: Object.values(webServer.entries) }
34
+ }));
35
+ ws.send(JSON.stringify({ type: "init-data", timestamp: Date.now() }));
27
36
  ws.on("message", async (data) => {
28
37
  try {
29
38
  const message = JSON.parse(data.toString());
30
39
  await handleWebSocketMessage(ws, message, webServer);
31
40
  } catch (error) {
32
41
  console.error("WebSocket \u6D88\u606F\u5904\u7406\u9519\u8BEF:", error);
33
- ws.send(
34
- JSON.stringify({
35
- error: "Invalid message format"
36
- })
37
- );
42
+ ws.send(JSON.stringify({ error: "Invalid message format" }));
38
43
  }
39
44
  });
40
45
  ws.on("close", () => {
@@ -51,19 +56,50 @@ async function handleWebSocketMessage(ws, message, webServer) {
51
56
  ws.send(JSON.stringify({ type: "pong", requestId }));
52
57
  break;
53
58
  case "entries:get":
54
- ws.send(
55
- JSON.stringify({
56
- requestId,
57
- data: Object.values(webServer.entries)
58
- })
59
- );
59
+ ws.send(JSON.stringify({ requestId, data: Object.values(webServer.entries) }));
60
+ break;
61
+ // ================================================================
62
+ // 配置文件原始 YAML 读写(用于配置管理页面)
63
+ // ================================================================
64
+ case "config:get-yaml":
65
+ try {
66
+ const filePath = getConfigFilePath();
67
+ const yaml = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, "utf-8") : "";
68
+ ws.send(JSON.stringify({ requestId, data: { yaml, pluginKeys: getPluginKeys() } }));
69
+ } catch (error) {
70
+ ws.send(JSON.stringify({ requestId, error: `Failed to read config: ${error.message}` }));
71
+ }
60
72
  break;
73
+ case "config:save-yaml":
74
+ try {
75
+ const { yaml } = message;
76
+ if (typeof yaml !== "string") {
77
+ ws.send(JSON.stringify({ requestId, error: "yaml field is required" }));
78
+ break;
79
+ }
80
+ const filePath = getConfigFilePath();
81
+ fs__default.writeFileSync(filePath, yaml, "utf-8");
82
+ const configService2 = root.inject("config");
83
+ const loader = configService2.configs.get("zhin.config.yml");
84
+ if (loader) loader.load();
85
+ ws.send(JSON.stringify({ requestId, data: { success: true, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u751F\u6548" } }));
86
+ } catch (error) {
87
+ ws.send(JSON.stringify({ requestId, error: `Failed to save config: ${error.message}` }));
88
+ }
89
+ break;
90
+ // ================================================================
91
+ // 插件配置(供 PluginConfigForm 使用)
92
+ // ================================================================
61
93
  case "config:get":
62
94
  try {
63
95
  const configService2 = root.inject("config");
64
- const appConfig2 = configService2.get("zhin.config.yml");
65
- const config = pluginName ? appConfig2[pluginName] || {} : appConfig2;
66
- ws.send(JSON.stringify({ requestId, data: config }));
96
+ const rawConfig = configService2.getRaw("zhin.config.yml");
97
+ if (!pluginName) {
98
+ ws.send(JSON.stringify({ requestId, data: rawConfig }));
99
+ } else {
100
+ const configKey = resolveConfigKey(pluginName);
101
+ ws.send(JSON.stringify({ requestId, data: rawConfig[configKey] || {} }));
102
+ }
67
103
  } catch (error) {
68
104
  ws.send(JSON.stringify({ requestId, error: `Failed to get config: ${error.message}` }));
69
105
  }
@@ -71,8 +107,17 @@ async function handleWebSocketMessage(ws, message, webServer) {
71
107
  case "config:get-all":
72
108
  try {
73
109
  const configService2 = root.inject("config");
74
- const appConfig2 = configService2.get("zhin.config.yml");
75
- ws.send(JSON.stringify({ requestId, data: appConfig2 }));
110
+ const rawConfig = configService2.getRaw("zhin.config.yml");
111
+ const allConfigs = { ...rawConfig };
112
+ const schemaService = root.inject("schema");
113
+ if (schemaService) {
114
+ for (const [pName, configKey] of schemaService.getPluginKeyMap()) {
115
+ if (pName !== configKey) {
116
+ allConfigs[pName] = rawConfig[configKey] || {};
117
+ }
118
+ }
119
+ }
120
+ ws.send(JSON.stringify({ requestId, data: allConfigs }));
76
121
  } catch (error) {
77
122
  ws.send(JSON.stringify({ requestId, error: `Failed to get all configs: ${error.message}` }));
78
123
  }
@@ -84,28 +129,45 @@ async function handleWebSocketMessage(ws, message, webServer) {
84
129
  ws.send(JSON.stringify({ requestId, error: "Plugin name is required" }));
85
130
  break;
86
131
  }
132
+ const configKey = resolveConfigKey(pluginName);
87
133
  const configService2 = root.inject("config");
88
- const appConfig2 = configService2.get("zhin.config.yml");
89
- appConfig2[pluginName] = data;
90
- configService2.set("zhin.config.yml", appConfig2);
91
- ws.send(JSON.stringify({ requestId, success: true }));
92
- broadcastToAll(webServer, {
93
- type: "config:updated",
94
- data: { pluginName, config: data }
95
- });
134
+ const loader = configService2.configs.get("zhin.config.yml");
135
+ if (!loader) {
136
+ ws.send(JSON.stringify({ requestId, error: "Config file not loaded" }));
137
+ break;
138
+ }
139
+ loader.patchKey(configKey, data);
140
+ broadcastToAll(webServer, { type: "config:updated", data: { pluginName, config: data } });
141
+ const schemaService = root.inject("schema");
142
+ const reloadable = schemaService?.isReloadable?.(pluginName) ?? false;
143
+ if (reloadable) {
144
+ const target = findPluginByConfigKey(root, pluginName);
145
+ if (target) {
146
+ try {
147
+ await target.reload();
148
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: true } }));
149
+ } catch (reloadErr) {
150
+ logger.warn(`\u91CD\u8F7D\u63D2\u4EF6 ${pluginName} \u5931\u8D25: ${reloadErr.message}`);
151
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u91CD\u8F7D\u5931\u8D25" } }));
152
+ }
153
+ } else {
154
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58" } }));
155
+ }
156
+ } else {
157
+ ws.send(JSON.stringify({ requestId, data: { success: true, reloaded: false, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u8FDB\u7A0B\u624D\u80FD\u751F\u6548" } }));
158
+ }
96
159
  } catch (error) {
97
160
  ws.send(JSON.stringify({ requestId, error: `Failed to set config: ${error.message}` }));
98
161
  }
99
162
  break;
163
+ // ================================================================
164
+ // Schema(供 PluginConfigForm 使用)
165
+ // ================================================================
100
166
  case "schema:get":
101
167
  try {
102
168
  const schemaService = root.inject("schema");
103
169
  const schema = pluginName && schemaService ? schemaService.get(pluginName) : null;
104
- if (schema) {
105
- ws.send(JSON.stringify({ requestId, data: schema.toJSON() }));
106
- } else {
107
- ws.send(JSON.stringify({ requestId, data: null }));
108
- }
170
+ ws.send(JSON.stringify({ requestId, data: schema ? schema.toJSON() : null }));
109
171
  } catch (error) {
110
172
  ws.send(JSON.stringify({ requestId, error: `Failed to get schema: ${error.message}` }));
111
173
  }
@@ -115,8 +177,15 @@ async function handleWebSocketMessage(ws, message, webServer) {
115
177
  const schemaService = root.inject("schema");
116
178
  const schemas = {};
117
179
  if (schemaService) {
118
- for (const [name, schema] of schemaService.items.entries()) {
119
- schemas[name] = schema.toJSON();
180
+ for (const record of schemaService.items) {
181
+ if (record.key && record.schema) {
182
+ schemas[record.key] = record.schema.toJSON();
183
+ }
184
+ }
185
+ for (const [pName, configKey] of schemaService.getPluginKeyMap()) {
186
+ if (pName !== configKey && schemas[configKey]) {
187
+ schemas[pName] = schemas[configKey];
188
+ }
120
189
  }
121
190
  }
122
191
  ws.send(JSON.stringify({ requestId, data: schemas }));
@@ -124,15 +193,67 @@ async function handleWebSocketMessage(ws, message, webServer) {
124
193
  ws.send(JSON.stringify({ requestId, error: `Failed to get all schemas: ${error.message}` }));
125
194
  }
126
195
  break;
196
+ // ================================================================
197
+ // 环境变量文件管理
198
+ // ================================================================
199
+ case "env:list":
200
+ try {
201
+ const cwd = process.cwd();
202
+ const files = ENV_WHITELIST.map((name) => ({
203
+ name,
204
+ exists: fs__default.existsSync(path3__default.resolve(cwd, name))
205
+ }));
206
+ ws.send(JSON.stringify({ requestId, data: { files } }));
207
+ } catch (error) {
208
+ ws.send(JSON.stringify({ requestId, error: `Failed to list env files: ${error.message}` }));
209
+ }
210
+ break;
211
+ case "env:get":
212
+ try {
213
+ const { filename } = message;
214
+ if (!filename || !ENV_WHITELIST.includes(filename)) {
215
+ ws.send(JSON.stringify({ requestId, error: `Invalid env file: ${filename}` }));
216
+ break;
217
+ }
218
+ const envPath = path3__default.resolve(process.cwd(), filename);
219
+ const content = fs__default.existsSync(envPath) ? fs__default.readFileSync(envPath, "utf-8") : "";
220
+ ws.send(JSON.stringify({ requestId, data: { content } }));
221
+ } catch (error) {
222
+ ws.send(JSON.stringify({ requestId, error: `Failed to read env file: ${error.message}` }));
223
+ }
224
+ break;
225
+ case "env:save":
226
+ try {
227
+ const { filename, content } = message;
228
+ if (!filename || !ENV_WHITELIST.includes(filename)) {
229
+ ws.send(JSON.stringify({ requestId, error: `Invalid env file: ${filename}` }));
230
+ break;
231
+ }
232
+ if (typeof content !== "string") {
233
+ ws.send(JSON.stringify({ requestId, error: "content field is required" }));
234
+ break;
235
+ }
236
+ const envPath = path3__default.resolve(process.cwd(), filename);
237
+ fs__default.writeFileSync(envPath, content, "utf-8");
238
+ ws.send(JSON.stringify({ requestId, data: { success: true, message: "\u73AF\u5883\u53D8\u91CF\u5DF2\u4FDD\u5B58\uFF0C\u9700\u91CD\u542F\u751F\u6548" } }));
239
+ } catch (error) {
240
+ ws.send(JSON.stringify({ requestId, error: `Failed to save env file: ${error.message}` }));
241
+ }
242
+ break;
127
243
  default:
128
- ws.send(
129
- JSON.stringify({
130
- requestId,
131
- error: `Unknown message type: ${type}`
132
- })
133
- );
244
+ ws.send(JSON.stringify({ requestId, error: `Unknown message type: ${type}` }));
134
245
  }
135
246
  }
247
+ function findPluginByConfigKey(rootPlugin, configKey) {
248
+ for (const child of rootPlugin.children) {
249
+ if (child.name === configKey || child.name.endsWith(`-${configKey}`) || child.name.includes(configKey)) {
250
+ return child;
251
+ }
252
+ const found = findPluginByConfigKey(child, configKey);
253
+ if (found) return found;
254
+ }
255
+ return null;
256
+ }
136
257
  function broadcastToAll(webServer, message) {
137
258
  for (const ws of webServer.ws.clients || []) {
138
259
  if (ws.readyState === WebSocket.OPEN) {
@@ -141,16 +262,13 @@ function broadcastToAll(webServer, message) {
141
262
  }
142
263
  }
143
264
  function notifyDataUpdate(webServer) {
144
- broadcastToAll(webServer, {
145
- type: "data-update",
146
- timestamp: Date.now()
147
- });
265
+ broadcastToAll(webServer, { type: "data-update", timestamp: Date.now() });
148
266
  }
149
267
  var cache = /* @__PURE__ */ new Map();
150
268
  var TRANSFORMABLE_EXTS = [".ts", ".tsx", ".jsx"];
151
269
  var RESOLVE_EXTS = [".tsx", ".ts", ".jsx", ".js"];
152
270
  function isTransformable(filePath) {
153
- const ext = path2.extname(filePath).toLowerCase();
271
+ const ext = path3.extname(filePath).toLowerCase();
154
272
  return TRANSFORMABLE_EXTS.includes(ext);
155
273
  }
156
274
  var IMPORT_RE = /(?:import|export)\s+.*?\s+from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
@@ -159,22 +277,22 @@ function isRelative(specifier) {
159
277
  return specifier.startsWith("./") || specifier.startsWith("../");
160
278
  }
161
279
  function hasExtension(specifier) {
162
- const base = path2.basename(specifier);
280
+ const base = path3.basename(specifier);
163
281
  return base.includes(".") && !base.startsWith(".");
164
282
  }
165
283
  function resolveRelativeImport(specifier, fromFile) {
166
284
  if (!isRelative(specifier)) return specifier;
167
285
  if (hasExtension(specifier)) return specifier;
168
- const dir = path2.dirname(fromFile);
169
- const target = path2.resolve(dir, specifier);
286
+ const dir = path3.dirname(fromFile);
287
+ const target = path3.resolve(dir, specifier);
170
288
  for (const ext of RESOLVE_EXTS) {
171
- if (fs2.existsSync(target + ext)) {
289
+ if (fs.existsSync(target + ext)) {
172
290
  return specifier + ext;
173
291
  }
174
292
  }
175
- if (fs2.existsSync(target) && fs2.statSync(target).isDirectory()) {
293
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
176
294
  for (const ext of RESOLVE_EXTS) {
177
- if (fs2.existsSync(path2.join(target, `index${ext}`))) {
295
+ if (fs.existsSync(path3.join(target, `index${ext}`))) {
178
296
  return specifier + `/index${ext}`;
179
297
  }
180
298
  }
@@ -194,13 +312,13 @@ function rewriteImports(code, fromFile) {
194
312
  return code;
195
313
  }
196
314
  async function transformFile(filePath) {
197
- const stat = fs2.statSync(filePath);
315
+ const stat = fs.statSync(filePath);
198
316
  const cached = cache.get(filePath);
199
317
  if (cached && cached.mtime === stat.mtimeMs) {
200
318
  return cached.code;
201
319
  }
202
- const source = fs2.readFileSync(filePath, "utf-8");
203
- const ext = path2.extname(filePath).toLowerCase();
320
+ const source = fs.readFileSync(filePath, "utf-8");
321
+ const ext = path3.extname(filePath).toLowerCase();
204
322
  const loader = ext === ".tsx" ? "tsx" : ext === ".ts" ? "ts" : ext === ".jsx" ? "jsx" : "js";
205
323
  const result = await transform(source, {
206
324
  loader,
@@ -238,11 +356,11 @@ if (enabled) {
238
356
  const genToken = () => Math.random().toString(36).slice(2, 8);
239
357
  const watchedDirs = /* @__PURE__ */ new Set();
240
358
  const watchers = [];
241
- path2.join(import.meta.dirname, "../client");
242
- const distDir = path2.join(import.meta.dirname, "../dist");
359
+ path3.join(import.meta.dirname, "../client");
360
+ const distDir = path3.join(import.meta.dirname, "../dist");
243
361
  const resolveFile = (name) => {
244
- const distPath = path2.resolve(distDir, name);
245
- if (fs2.existsSync(distPath)) return distPath;
362
+ const distPath = path3.resolve(distDir, name);
363
+ if (fs.existsSync(distPath)) return distPath;
246
364
  return null;
247
365
  };
248
366
  const webServer = {
@@ -250,8 +368,8 @@ if (enabled) {
250
368
  addEntry(entry) {
251
369
  const hash = Date.now().toString(16) + Math.random().toString(16).slice(2, 8);
252
370
  const entryFile = typeof entry === "string" ? entry : entry.production;
253
- const dir = path2.dirname(entryFile);
254
- const filename = path2.basename(entryFile);
371
+ const dir = path3.dirname(entryFile);
372
+ const filename = path3.basename(entryFile);
255
373
  let token;
256
374
  for (const [t, d] of entryBases) {
257
375
  if (d === dir) {
@@ -285,10 +403,15 @@ if (enabled) {
285
403
  if (ctx.path.startsWith("/api/") || ctx.path === "/api" || ctx.path.startsWith("/pub/") || ctx.path === "/pub") {
286
404
  return next();
287
405
  }
406
+ const whiteList = router.whiteList || [];
407
+ const isRegistered = whiteList.some(
408
+ (p) => typeof p === "string" && !p.includes("*") && !p.startsWith("/vite") && ctx.path.startsWith(p)
409
+ );
410
+ if (isRegistered) return next();
288
411
  const name = ctx.path.slice(1);
289
412
  const sendFile = async (filename) => {
290
413
  try {
291
- const stat = fs2.statSync(filename);
414
+ const stat = fs.statSync(filename);
292
415
  if (!stat.isFile()) {
293
416
  ctx.status = 404;
294
417
  return;
@@ -310,9 +433,9 @@ if (enabled) {
310
433
  return;
311
434
  }
312
435
  }
313
- ctx.type = path2.extname(filename);
436
+ ctx.type = path3.extname(filename);
314
437
  ctx.type = mime.getType(filename) || ctx.type;
315
- return ctx.body = fs2.createReadStream(filename);
438
+ return ctx.body = fs.createReadStream(filename);
316
439
  };
317
440
  if (ctx.path.startsWith("/vite/@ext/")) {
318
441
  const rest = ctx.path.replace("/vite/@ext/", "");
@@ -328,13 +451,13 @@ if (enabled) {
328
451
  ctx.status = 404;
329
452
  return;
330
453
  }
331
- const fullPath = path2.resolve(baseDir, relPath);
332
- const safePfx = baseDir.endsWith(path2.sep) ? baseDir : baseDir + path2.sep;
454
+ const fullPath = path3.resolve(baseDir, relPath);
455
+ const safePfx = baseDir.endsWith(path3.sep) ? baseDir : baseDir + path3.sep;
333
456
  if (!fullPath.startsWith(safePfx) && fullPath !== baseDir) {
334
457
  ctx.status = 403;
335
458
  return;
336
459
  }
337
- if (fs2.existsSync(fullPath)) {
460
+ if (fs.existsSync(fullPath)) {
338
461
  return sendFile(fullPath);
339
462
  }
340
463
  ctx.status = 404;
@@ -343,7 +466,7 @@ if (enabled) {
343
466
  const resolved = resolveFile(name);
344
467
  if (resolved) {
345
468
  try {
346
- const fileState = fs2.statSync(resolved);
469
+ const fileState = fs.statSync(resolved);
347
470
  if (fileState.isFile() && !fileState.isSocket() && !fileState.isFIFO()) {
348
471
  return sendFile(resolved);
349
472
  }
@@ -2,17 +2,8 @@ import { WebServer } from './index.js';
2
2
  import '@zhin.js/http';
3
3
  import 'ws';
4
4
 
5
- /**
6
- * 设置 WebSocket 连接处理
7
- */
8
5
  declare function setupWebSocket(webServer: WebServer): void;
9
- /**
10
- * 广播消息给所有连接的客户端
11
- */
12
6
  declare function broadcastToAll(webServer: WebServer, message: any): void;
13
- /**
14
- * 通知数据更新
15
- */
16
7
  declare function notifyDataUpdate(webServer: WebServer): void;
17
8
 
18
9
  export { broadcastToAll, notifyDataUpdate, setupWebSocket };