napcat-plugin-debug-cli 1.1.0 → 1.2.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.
- package/cli.mjs +11 -4
- package/package.json +21 -3
- package/vite.mjs +233 -0
package/cli.mjs
CHANGED
|
@@ -5215,6 +5215,14 @@ function copyDirRecursive(src, dest) {
|
|
|
5215
5215
|
else fs.copyFileSync(srcPath, destPath);
|
|
5216
5216
|
}
|
|
5217
5217
|
}
|
|
5218
|
+
function countFiles(dir) {
|
|
5219
|
+
let count = 0;
|
|
5220
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
5221
|
+
if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
|
|
5222
|
+
else count++;
|
|
5223
|
+
}
|
|
5224
|
+
return count;
|
|
5225
|
+
}
|
|
5218
5226
|
async function deployPlugin(projectDir, remotePluginPath, rpc) {
|
|
5219
5227
|
const distDir = path.resolve(projectDir, "dist");
|
|
5220
5228
|
if (!fs.existsSync(distDir)) {
|
|
@@ -5246,7 +5254,7 @@ async function deployPlugin(projectDir, remotePluginPath, rpc) {
|
|
|
5246
5254
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
5247
5255
|
}
|
|
5248
5256
|
copyDirRecursive(distDir, destDir);
|
|
5249
|
-
logOk(`文件复制完成 (${
|
|
5257
|
+
logOk(`文件复制完成 (${countFiles(distDir)} 个文件)`);
|
|
5250
5258
|
} catch (e) {
|
|
5251
5259
|
logErr(`复制文件失败: ${e.message}`);
|
|
5252
5260
|
return false;
|
|
@@ -5256,9 +5264,8 @@ async function deployPlugin(projectDir, remotePluginPath, rpc) {
|
|
|
5256
5264
|
logOk(`${co(pluginName, C.green, C.bold)} 重载成功`);
|
|
5257
5265
|
} catch {
|
|
5258
5266
|
try {
|
|
5259
|
-
logInfo("
|
|
5260
|
-
await rpc.call("
|
|
5261
|
-
await rpc.call("loadPluginById", pluginName);
|
|
5267
|
+
logInfo("插件未注册,尝试从目录加载...");
|
|
5268
|
+
await rpc.call("loadDirectoryPlugin", destDir);
|
|
5262
5269
|
logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
|
|
5263
5270
|
} catch (e2) {
|
|
5264
5271
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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,233 @@
|
|
|
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("✓", C.green)} ${m}`);
|
|
20
|
+
const logErr = (m) => console.log(`${PREFIX} ${co("✗", C.red)} ${m}`);
|
|
21
|
+
const logHmr = (m) => console.log(`${PREFIX} ${co("🔥", C.magenta)} ${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
|
+
const { default: WebSocket } = await import('ws');
|
|
102
|
+
let url = wsUrl;
|
|
103
|
+
if (token) {
|
|
104
|
+
const u = new URL(url);
|
|
105
|
+
u.searchParams.set("token", token);
|
|
106
|
+
url = u.toString();
|
|
107
|
+
}
|
|
108
|
+
return await new Promise((resolve) => {
|
|
109
|
+
const ws = new WebSocket(url);
|
|
110
|
+
const timeout = setTimeout(() => {
|
|
111
|
+
ws.close();
|
|
112
|
+
connecting = false;
|
|
113
|
+
resolve(false);
|
|
114
|
+
}, 5e3);
|
|
115
|
+
ws.on("open", () => {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
});
|
|
118
|
+
ws.on("message", async (raw) => {
|
|
119
|
+
try {
|
|
120
|
+
const msg = JSON.parse(raw.toString());
|
|
121
|
+
if (msg.method === "welcome") {
|
|
122
|
+
rpc = new SimpleRpcClient(ws);
|
|
123
|
+
try {
|
|
124
|
+
const info = await rpc.call("getDebugInfo");
|
|
125
|
+
remotePluginPath = info.pluginPath;
|
|
126
|
+
logOk(`已连接调试服务 (${info.loadedCount}/${info.pluginCount} 插件)`);
|
|
127
|
+
log(`远程插件目录: ${co(info.pluginPath, C.dim)}`);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
connecting = false;
|
|
131
|
+
resolve(true);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
ws.on("error", () => {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
connecting = false;
|
|
139
|
+
resolve(false);
|
|
140
|
+
});
|
|
141
|
+
ws.on("close", () => {
|
|
142
|
+
rpc = null;
|
|
143
|
+
remotePluginPath = null;
|
|
144
|
+
connecting = false;
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
} catch (e) {
|
|
148
|
+
connecting = false;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function deployAndReload(distDir) {
|
|
153
|
+
if (!rpc?.connected || !remotePluginPath) {
|
|
154
|
+
logErr("未连接到调试服务,跳过部署");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const pkgPath = path.join(distDir, "package.json");
|
|
158
|
+
if (!fs.existsSync(pkgPath)) {
|
|
159
|
+
logErr("dist/package.json 不存在,跳过部署");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let pluginName;
|
|
163
|
+
try {
|
|
164
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
165
|
+
pluginName = pkg.name;
|
|
166
|
+
if (!pluginName) {
|
|
167
|
+
logErr("dist/package.json 中缺少 name 字段");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
logErr("解析 dist/package.json 失败");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const destDir = path.join(remotePluginPath, pluginName);
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(destDir)) {
|
|
177
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
copyDirRecursive(distDir, destDir);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
logErr(`复制文件失败: ${e.message}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await rpc.call("reloadPlugin", pluginName);
|
|
186
|
+
logHmr(`${co(pluginName, C.green, C.bold)} 已重载 (${countFiles(distDir)} 个文件)`);
|
|
187
|
+
} catch {
|
|
188
|
+
try {
|
|
189
|
+
await rpc.call("loadDirectoryPlugin", destDir);
|
|
190
|
+
logOk(`${co(pluginName, C.green, C.bold)} 首次加载成功`);
|
|
191
|
+
} catch (e2) {
|
|
192
|
+
logErr(`加载失败: ${e2.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
name: "napcat-hmr",
|
|
198
|
+
apply: "build",
|
|
199
|
+
configResolved(resolvedConfig) {
|
|
200
|
+
config = resolvedConfig;
|
|
201
|
+
},
|
|
202
|
+
async buildStart() {
|
|
203
|
+
if (!enabled) return;
|
|
204
|
+
if (!autoConnect) return;
|
|
205
|
+
if (isFirstBuild) {
|
|
206
|
+
log(`连接 ${co(wsUrl, C.cyan)}...`);
|
|
207
|
+
const ok = await connect();
|
|
208
|
+
if (!ok) {
|
|
209
|
+
logErr(`无法连接调试服务 ${wsUrl}`);
|
|
210
|
+
log("请确认 napcat-plugin-debug 已启用");
|
|
211
|
+
log("仅构建模式,不自动部署");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
async writeBundle() {
|
|
216
|
+
if (!enabled) return;
|
|
217
|
+
const distDir = path.resolve(config.build.outDir);
|
|
218
|
+
if (!rpc?.connected) {
|
|
219
|
+
const ok = await connect();
|
|
220
|
+
if (!ok) return;
|
|
221
|
+
}
|
|
222
|
+
await deployAndReload(distDir);
|
|
223
|
+
isFirstBuild = false;
|
|
224
|
+
},
|
|
225
|
+
closeBundle() {
|
|
226
|
+
if (config.build.watch) return;
|
|
227
|
+
rpc?.close();
|
|
228
|
+
rpc = null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export { napcatHmrPlugin as default, napcatHmrPlugin };
|