napcat-plugin-debug-cli 1.1.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/cli.mjs +21 -7
  2. package/package.json +21 -3
  3. package/vite.mjs +243 -0
package/cli.mjs CHANGED
@@ -5183,11 +5183,18 @@ function createWatcher(watchPath, onPluginChange) {
5183
5183
  }
5184
5184
  active = true;
5185
5185
  if (fs.existsSync(path.join(watchPath, "package.json"))) {
5186
- watchDir(path.basename(watchPath), watchPath);
5187
- logHmr(`监听插件: ${path.basename(watchPath)}`);
5186
+ const baseName = path.basename(watchPath);
5187
+ if (baseName === "napcat-plugin-debug") {
5188
+ logWarn("跳过 napcat-plugin-debug 自身,不能监听自我重载");
5189
+ } else {
5190
+ watchDir(baseName, watchPath);
5191
+ logHmr(`监听插件: ${baseName}`);
5192
+ }
5188
5193
  } else {
5189
5194
  for (const d of fs.readdirSync(watchPath, { withFileTypes: true })) {
5190
- if (d.isDirectory()) watchDir(d.name, path.join(watchPath, d.name));
5195
+ if (d.isDirectory() && d.name !== "napcat-plugin-debug") {
5196
+ watchDir(d.name, path.join(watchPath, d.name));
5197
+ }
5191
5198
  }
5192
5199
  logHmr(`监听 ${watchers.size} 个插件: ${watchPath}`);
5193
5200
  }
@@ -5215,6 +5222,14 @@ function copyDirRecursive(src, dest) {
5215
5222
  else fs.copyFileSync(srcPath, destPath);
5216
5223
  }
5217
5224
  }
5225
+ function countFiles(dir) {
5226
+ let count = 0;
5227
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
5228
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
5229
+ else count++;
5230
+ }
5231
+ return count;
5232
+ }
5218
5233
  async function deployPlugin(projectDir, remotePluginPath, rpc) {
5219
5234
  const distDir = path.resolve(projectDir, "dist");
5220
5235
  if (!fs.existsSync(distDir)) {
@@ -5246,7 +5261,7 @@ async function deployPlugin(projectDir, remotePluginPath, rpc) {
5246
5261
  fs.rmSync(destDir, { recursive: true, force: true });
5247
5262
  }
5248
5263
  copyDirRecursive(distDir, destDir);
5249
- logOk(`文件复制完成 (${fs.readdirSync(distDir, { recursive: true }).length} 个文件)`);
5264
+ logOk(`文件复制完成 (${countFiles(distDir)} 个文件)`);
5250
5265
  } catch (e) {
5251
5266
  logErr(`复制文件失败: ${e.message}`);
5252
5267
  return false;
@@ -5256,9 +5271,8 @@ async function deployPlugin(projectDir, remotePluginPath, rpc) {
5256
5271
  logOk(`${co(pluginName, C.green, C.bold)} 重载成功`);
5257
5272
  } catch {
5258
5273
  try {
5259
- logInfo("插件未注册,尝试扫描加载...");
5260
- await rpc.call("scanPlugins");
5261
- await rpc.call("loadPluginById", pluginName);
5274
+ logInfo("插件未注册,尝试从目录加载...");
5275
+ await rpc.call("loadDirectoryPlugin", destDir);
5262
5276
  logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
5263
5277
  } catch (e2) {
5264
5278
  logWarn(`自动加载失败: ${e2.message},请手动 load ${pluginName}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "napcat-plugin-debug-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "NapCat 插件调试 CLI — 连接调试服务实现热重载",
6
6
  "author": "NapNeko",
@@ -8,14 +8,32 @@
8
8
  "bin": {
9
9
  "napcat-debug": "./cli.mjs"
10
10
  },
11
+ "exports": {
12
+ ".": "./cli.mjs",
13
+ "./vite": "./vite.mjs"
14
+ },
11
15
  "files": [
12
- "cli.mjs"
16
+ "cli.mjs",
17
+ "vite.mjs"
13
18
  ],
19
+ "peerDependencies": {
20
+ "vite": ">=5.0.0",
21
+ "ws": ">=8.0.0"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "vite": {
25
+ "optional": true
26
+ },
27
+ "ws": {
28
+ "optional": false
29
+ }
30
+ },
14
31
  "keywords": [
15
32
  "napcat",
16
33
  "plugin",
17
34
  "debug",
18
35
  "hmr",
19
- "hot-reload"
36
+ "hot-reload",
37
+ "vite-plugin"
20
38
  ]
21
39
  }
package/vite.mjs ADDED
@@ -0,0 +1,243 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const C = {
5
+ reset: "\x1B[0m",
6
+ bold: "\x1B[1m",
7
+ dim: "\x1B[2m",
8
+ red: "\x1B[31m",
9
+ green: "\x1B[32m",
10
+ yellow: "\x1B[33m",
11
+ blue: "\x1B[34m",
12
+ magenta: "\x1B[35m",
13
+ cyan: "\x1B[36m",
14
+ gray: "\x1B[90m"
15
+ };
16
+ const co = (t, ...c) => c.join("") + t + C.reset;
17
+ const PREFIX = co("[napcat-hmr]", C.magenta, C.bold);
18
+ const log = (m) => console.log(`${PREFIX} ${m}`);
19
+ const logOk = (m) => console.log(`${PREFIX} ${co("(o'v'o)", C.green)} ${m}`);
20
+ const logErr = (m) => console.log(`${PREFIX} ${co("(;_;)", C.red)} ${m}`);
21
+ const logHmr = (m) => console.log(`${PREFIX} ${co("(><)", C.yellow)} ${co(m, C.magenta)}`);
22
+ class SimpleRpcClient {
23
+ ws;
24
+ nextId = 1;
25
+ pending = /* @__PURE__ */ new Map();
26
+ constructor(ws) {
27
+ this.ws = ws;
28
+ ws.on("message", (raw) => {
29
+ try {
30
+ const msg = JSON.parse(raw.toString());
31
+ if (msg.jsonrpc === "2.0" && msg.id != null) {
32
+ const p = this.pending.get(msg.id);
33
+ if (p) {
34
+ this.pending.delete(msg.id);
35
+ if (msg.error) p.reject(new Error(msg.error.message));
36
+ else p.resolve(msg.result);
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ });
42
+ }
43
+ call(method, ...params) {
44
+ return new Promise((resolve, reject) => {
45
+ const id = this.nextId++;
46
+ this.pending.set(id, { resolve, reject });
47
+ const req = { jsonrpc: "2.0", id, method, params };
48
+ this.ws.send(JSON.stringify(req));
49
+ setTimeout(() => {
50
+ if (this.pending.has(id)) {
51
+ this.pending.delete(id);
52
+ reject(new Error("RPC timeout"));
53
+ }
54
+ }, 1e4);
55
+ });
56
+ }
57
+ get connected() {
58
+ return this.ws?.readyState === 1;
59
+ }
60
+ close() {
61
+ try {
62
+ this.ws?.close(1e3);
63
+ } catch {
64
+ }
65
+ }
66
+ }
67
+ function copyDirRecursive(src, dest) {
68
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
69
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
70
+ const srcPath = path.join(src, entry.name);
71
+ const destPath = path.join(dest, entry.name);
72
+ if (entry.isDirectory()) copyDirRecursive(srcPath, destPath);
73
+ else fs.copyFileSync(srcPath, destPath);
74
+ }
75
+ }
76
+ function countFiles(dir) {
77
+ let count = 0;
78
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
+ if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
80
+ else count++;
81
+ }
82
+ return count;
83
+ }
84
+ function napcatHmrPlugin(options = {}) {
85
+ const {
86
+ wsUrl = "ws://127.0.0.1:8998",
87
+ token,
88
+ enabled = true,
89
+ autoConnect = true
90
+ } = options;
91
+ let rpc = null;
92
+ let remotePluginPath = null;
93
+ let connecting = false;
94
+ let config;
95
+ let isFirstBuild = true;
96
+ async function connect() {
97
+ if (rpc?.connected) return true;
98
+ if (connecting) return false;
99
+ connecting = true;
100
+ try {
101
+ process.env.WS_NO_BUFFER_UTIL = "1";
102
+ process.env.WS_NO_UTF_8_VALIDATE = "1";
103
+ const { default: WebSocket } = await import('ws');
104
+ let url = wsUrl;
105
+ if (token) {
106
+ const u = new URL(url);
107
+ u.searchParams.set("token", token);
108
+ url = u.toString();
109
+ }
110
+ return await new Promise((resolve) => {
111
+ const ws = new WebSocket(url);
112
+ const timeout = setTimeout(() => {
113
+ ws.close();
114
+ connecting = false;
115
+ resolve(false);
116
+ }, 5e3);
117
+ ws.on("open", () => {
118
+ clearTimeout(timeout);
119
+ });
120
+ ws.on("message", async (raw) => {
121
+ try {
122
+ const msg = JSON.parse(raw.toString());
123
+ if (msg.method === "welcome") {
124
+ rpc = new SimpleRpcClient(ws);
125
+ try {
126
+ const info = await rpc.call("getDebugInfo");
127
+ remotePluginPath = info.pluginPath;
128
+ logOk(`已连接调试服务 (${info.loadedCount}/${info.pluginCount} 插件)`);
129
+ log(`远程插件目录: ${co(info.pluginPath, C.dim)}`);
130
+ } catch {
131
+ }
132
+ connecting = false;
133
+ resolve(true);
134
+ }
135
+ } catch {
136
+ }
137
+ });
138
+ ws.on("error", () => {
139
+ clearTimeout(timeout);
140
+ connecting = false;
141
+ resolve(false);
142
+ });
143
+ ws.on("close", () => {
144
+ rpc = null;
145
+ remotePluginPath = null;
146
+ connecting = false;
147
+ });
148
+ });
149
+ } catch (e) {
150
+ connecting = false;
151
+ return false;
152
+ }
153
+ }
154
+ async function deployAndReload(distDir) {
155
+ if (!rpc?.connected || !remotePluginPath) {
156
+ logErr("未连接到调试服务,跳过部署");
157
+ return;
158
+ }
159
+ const pkgPath = path.join(distDir, "package.json");
160
+ if (!fs.existsSync(pkgPath)) {
161
+ logErr("dist/package.json 不存在,跳过部署");
162
+ return;
163
+ }
164
+ let pluginName;
165
+ try {
166
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
167
+ pluginName = pkg.name;
168
+ if (!pluginName) {
169
+ logErr("dist/package.json 中缺少 name 字段");
170
+ return;
171
+ }
172
+ } catch {
173
+ logErr("解析 dist/package.json 失败");
174
+ return;
175
+ }
176
+ const destDir = path.join(remotePluginPath, pluginName);
177
+ try {
178
+ if (fs.existsSync(destDir)) {
179
+ fs.rmSync(destDir, { recursive: true, force: true });
180
+ }
181
+ copyDirRecursive(distDir, destDir);
182
+ } catch (e) {
183
+ logErr(`复制文件失败: ${e.message}`);
184
+ return;
185
+ }
186
+ try {
187
+ const reloaded = await rpc.call("reloadPlugin", pluginName);
188
+ if (reloaded === false) {
189
+ throw new Error("not registered");
190
+ }
191
+ logHmr(`${co(pluginName, C.green, C.bold)} 已重载 (${countFiles(distDir)} 个文件)`);
192
+ } catch {
193
+ try {
194
+ await rpc.call("loadDirectoryPlugin", destDir);
195
+ try {
196
+ await rpc.call("setPluginStatus", pluginName, true);
197
+ await rpc.call("loadPluginById", pluginName);
198
+ } catch {
199
+ }
200
+ logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
201
+ } catch (e2) {
202
+ logErr(`加载失败: ${e2.message}`);
203
+ }
204
+ }
205
+ }
206
+ return {
207
+ name: "napcat-hmr",
208
+ apply: "build",
209
+ configResolved(resolvedConfig) {
210
+ config = resolvedConfig;
211
+ },
212
+ async buildStart() {
213
+ if (!enabled) return;
214
+ if (!autoConnect) return;
215
+ if (isFirstBuild) {
216
+ log(`连接 ${co(wsUrl, C.cyan)}...`);
217
+ const ok = await connect();
218
+ if (!ok) {
219
+ logErr(`无法连接调试服务 ${wsUrl}`);
220
+ log("请确认 napcat-plugin-debug 已启用");
221
+ log("仅构建模式,不自动部署");
222
+ }
223
+ }
224
+ },
225
+ async writeBundle() {
226
+ if (!enabled) return;
227
+ const distDir = path.resolve(config.build.outDir);
228
+ if (!rpc?.connected) {
229
+ const ok = await connect();
230
+ if (!ok) return;
231
+ }
232
+ await deployAndReload(distDir);
233
+ isFirstBuild = false;
234
+ },
235
+ closeBundle() {
236
+ if (config.build.watch) return;
237
+ rpc?.close();
238
+ rpc = null;
239
+ }
240
+ };
241
+ }
242
+
243
+ export { napcatHmrPlugin as default, napcatHmrPlugin };