@koishi-ce/plugin-market 1.0.8 → 1.0.9
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.d.ts +0 -1
- package/lib/index.mjs +32 -36
- package/package.json +9 -10
- package/src/__tests__/index.test.ts +7 -8
- package/src/node/installer.ts +30 -69
- package/src/node/proc.ts +25 -0
package/lib/index.d.ts
CHANGED
|
@@ -26,7 +26,6 @@ declare class Installer extends Service {
|
|
|
26
26
|
fullCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>;
|
|
27
27
|
tempCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>;
|
|
28
28
|
private pkgTasks;
|
|
29
|
-
private agent;
|
|
30
29
|
private manifest;
|
|
31
30
|
private depTask;
|
|
32
31
|
private flushData;
|
package/lib/index.mjs
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { join, resolve } from "node:path";
|
|
3
2
|
import { Logger, Schema, Service, Time, defineProperty, pick, sleep, valueMap } from "@koishi-ce/koishi";
|
|
4
3
|
import { compare, gt, satisfies, valid } from "semver";
|
|
5
4
|
import { DataService } from "@koishi-ce/console";
|
|
6
5
|
import { readFileSync } from "node:fs";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
|
-
import Scanner, { isResidentInCache, resolvePackageJson } from "@koishi-ce/registry";
|
|
9
|
-
import spawn from "
|
|
10
|
-
import pMap from "p-map";
|
|
7
|
+
import Scanner, { isResidentInCache, mapLimit, resolvePackageJson } from "@koishi-ce/registry";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
11
9
|
import messageZhCN from "./assets/message.zh-CN-D1DbsO3I.yml";
|
|
12
10
|
import schemaZhCN from "./assets/schema.zh-CN-Bj0_jaVr.yml";
|
|
13
11
|
//#region src/node/deps.ts
|
|
@@ -28,6 +26,27 @@ var RegistryProvider = class extends DataService {
|
|
|
28
26
|
}
|
|
29
27
|
};
|
|
30
28
|
//#endregion
|
|
29
|
+
//#region src/node/proc.ts
|
|
30
|
+
/**
|
|
31
|
+
* 安装子进程的创建封装(原依赖 execa)。
|
|
32
|
+
*
|
|
33
|
+
* execa 本质是 node:child_process 的事件流封装,本仓只用到其
|
|
34
|
+
* spawn + exit/error 事件 + stdout/stderr 流读取的子集,直接用
|
|
35
|
+
* node:child_process 等价实现即可,Bun 运行时完全兼容。刻意不用
|
|
36
|
+
* Bun.spawn 的捕获管道:win32 下其读端存在 EOF 竞态(见
|
|
37
|
+
* tooling/release/proc.ts 注释),而单进程安装场景 child_process
|
|
38
|
+
* 无此问题。子进程以 process.execPath 启动——宿主必为 Bun 运行时,
|
|
39
|
+
* 直接复用当前可执行文件,不依赖 PATH 中的 bun。
|
|
40
|
+
*
|
|
41
|
+
* 独立成模块是为了让测试能以 mock.module 按相对路径精确拦截,
|
|
42
|
+
* 不真正拉起安装进程(此前 mock execa 包名,全局替换同样可行,
|
|
43
|
+
* 但本地模块拦截范围更小、不受其它包使用 child_process 的干扰)。
|
|
44
|
+
*/
|
|
45
|
+
/** 在指定工作目录启动 bun 子进程(args 已含子命令与参数),stdio 三路 pipe。 */
|
|
46
|
+
function spawnBun(args, cwd) {
|
|
47
|
+
return spawn(process.execPath, args, { cwd });
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
31
50
|
//#region src/node/installer.ts
|
|
32
51
|
const logger$2 = new Logger("market");
|
|
33
52
|
/**
|
|
@@ -70,12 +89,6 @@ function getLocalRegistry(cwd, userHome = homedir()) {
|
|
|
70
89
|
for (const candidate of candidates) if (candidate?.startsWith("https://") || candidate?.startsWith("http://")) return candidate;
|
|
71
90
|
return "https://registry.npmjs.org/";
|
|
72
91
|
}
|
|
73
|
-
const whichPMRuns = createRequire(import.meta.url)("which-pm-runs");
|
|
74
|
-
const levelMap = {
|
|
75
|
-
info: "info",
|
|
76
|
-
warning: "debug",
|
|
77
|
-
error: "warn"
|
|
78
|
-
};
|
|
79
92
|
function loadManifest(name) {
|
|
80
93
|
const filename = resolvePackageJson(name);
|
|
81
94
|
const meta = JSON.parse(readFileSync(filename, "utf8"));
|
|
@@ -94,7 +107,6 @@ var Installer = class extends Service {
|
|
|
94
107
|
fullCache = {};
|
|
95
108
|
tempCache = {};
|
|
96
109
|
pkgTasks = {};
|
|
97
|
-
agent = whichPMRuns();
|
|
98
110
|
manifest;
|
|
99
111
|
flushData;
|
|
100
112
|
config;
|
|
@@ -164,7 +176,7 @@ var Installer = class extends Service {
|
|
|
164
176
|
const result = valueMap(this.manifest.dependencies, (request) => {
|
|
165
177
|
return { request: request.replace(/^[~^]/, "") };
|
|
166
178
|
});
|
|
167
|
-
await
|
|
179
|
+
await mapLimit(Object.keys(result), 10, async (name) => {
|
|
168
180
|
const dep = result[name];
|
|
169
181
|
if (!dep) return;
|
|
170
182
|
try {
|
|
@@ -176,7 +188,7 @@ var Installer = class extends Service {
|
|
|
176
188
|
if (!valid(dep.request)) dep.invalid = true;
|
|
177
189
|
const versions = await this.getPackage(name);
|
|
178
190
|
if (versions) dep.latest = Object.keys(versions)[0];
|
|
179
|
-
}
|
|
191
|
+
});
|
|
180
192
|
return result;
|
|
181
193
|
}
|
|
182
194
|
getDeps() {
|
|
@@ -195,40 +207,24 @@ var Installer = class extends Service {
|
|
|
195
207
|
this.refreshData();
|
|
196
208
|
}
|
|
197
209
|
async exec(args) {
|
|
198
|
-
|
|
199
|
-
const useJson = name === "yarn" && (this.agent?.version ?? "1") >= "2";
|
|
200
|
-
if (name !== "yarn") args.unshift("install");
|
|
210
|
+
args.unshift("install");
|
|
201
211
|
return new Promise((resolve) => {
|
|
202
|
-
|
|
203
|
-
const child = spawn(name, args, { cwd: this.cwd });
|
|
212
|
+
const child = spawnBun(args, this.cwd);
|
|
204
213
|
child.on("exit", (code) => resolve(code ?? -1));
|
|
205
214
|
child.on("error", () => resolve(-1));
|
|
206
215
|
let stderr = "";
|
|
207
216
|
child.stderr?.on("data", (data) => {
|
|
208
|
-
|
|
209
|
-
const lines =
|
|
217
|
+
stderr += data.toString();
|
|
218
|
+
const lines = stderr.split("\n");
|
|
210
219
|
stderr = lines.pop() ?? "";
|
|
211
220
|
for (const line of lines) logger$2.warn(line);
|
|
212
221
|
});
|
|
213
222
|
let stdout = "";
|
|
214
223
|
child.stdout?.on("data", (data) => {
|
|
215
|
-
|
|
216
|
-
const lines =
|
|
224
|
+
stdout += data.toString();
|
|
225
|
+
const lines = stdout.split("\n");
|
|
217
226
|
stdout = lines.pop() ?? "";
|
|
218
|
-
for (const line of lines)
|
|
219
|
-
if (!useJson || line[0] !== "{") {
|
|
220
|
-
logger$2.info(line);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
const { type, data } = JSON.parse(line);
|
|
225
|
-
const level = type in levelMap ? levelMap[type] : null;
|
|
226
|
-
(level ? logger$2[level] : logger$2.info)(data);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
logger$2.warn(line);
|
|
229
|
-
logger$2.warn(error);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
227
|
+
for (const line of lines) logger$2.info(line);
|
|
232
228
|
});
|
|
233
229
|
});
|
|
234
230
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishi-ce/plugin-market",
|
|
3
3
|
"description": "Manage your bots and plugins with console",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -60,20 +60,19 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@koishi-ce/client": "^1.0.
|
|
64
|
-
"@koishi-ce/loader": "^1.0.
|
|
65
|
-
"@koishi-ce/plugin-config": "^1.0.
|
|
63
|
+
"@koishi-ce/client": "^1.0.3",
|
|
64
|
+
"@koishi-ce/loader": "^1.0.4",
|
|
65
|
+
"@koishi-ce/plugin-config": "^1.0.8",
|
|
66
66
|
"@koishijs/market": "^4.2.10",
|
|
67
67
|
"@types/semver": "^7.5.8",
|
|
68
|
-
"
|
|
68
|
+
"@vueuse/core": "^14.4.0",
|
|
69
|
+
"vue": "^3.5.12",
|
|
70
|
+
"vue-router": "^5.2.0"
|
|
69
71
|
},
|
|
70
72
|
"dependencies": {
|
|
71
73
|
"@koishi-ce/console": "^1.0.0",
|
|
72
|
-
"@koishi-ce/registry": "^1.0.
|
|
73
|
-
"
|
|
74
|
-
"p-map": "^4.0.0",
|
|
75
|
-
"semver": "^7.6.3",
|
|
76
|
-
"which-pm-runs": "^1.1.0"
|
|
74
|
+
"@koishi-ce/registry": "^1.0.5",
|
|
75
|
+
"semver": "^7.6.3"
|
|
77
76
|
},
|
|
78
77
|
"exports": {
|
|
79
78
|
".": {
|
|
@@ -10,20 +10,20 @@ import memory from "@koishijs/plugin-database-memory";
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* market 插件测试:
|
|
13
|
-
* - 子进程(
|
|
13
|
+
* - 子进程(bun 安装)经 mock.module 拦截本地 proc.ts(spawnBun 封装),不落盘不联网安装;
|
|
14
14
|
* - registry 网络请求由进程内 Bun.serve 提供(registry 协议的最小 JSON);
|
|
15
15
|
* - 宿主环境(loader / cwd)使用 FakeLoader 与临时目录 + chdir,
|
|
16
16
|
* Installer 的 override 写盘只会作用于临时 package.json。
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** 已发生的子进程调用(参数列表)。 */
|
|
20
20
|
const spawnCalls: string[][] = [];
|
|
21
21
|
/** 控制下一次子进程的退出码与触发事件。 */
|
|
22
22
|
let nextExitCode = 0;
|
|
23
23
|
let nextSpawnError = false;
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
spawnCalls.push(
|
|
25
|
+
const spawnBunMock = (_args: string[], _cwd: string) => {
|
|
26
|
+
spawnCalls.push(_args);
|
|
27
27
|
return {
|
|
28
28
|
on(event: string, cb: (code?: number) => void) {
|
|
29
29
|
if (nextSpawnError) {
|
|
@@ -52,7 +52,7 @@ const execaMock = (name: string, args: string[]) => {
|
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
mock.module("
|
|
55
|
+
mock.module("../node/proc.ts", () => ({ spawnBun: spawnBunMock }));
|
|
56
56
|
|
|
57
57
|
import type { Entry } from "@koishi-ce/console";
|
|
58
58
|
// 均为 type-only 导入:编译期擦除,不干扰 mock.module 先于插件加载的时序
|
|
@@ -284,10 +284,9 @@ describe("Installer 安装链路", () => {
|
|
|
284
284
|
"koishi-plugin-demo": "^1.0.0",
|
|
285
285
|
});
|
|
286
286
|
expect(code).toBe(0);
|
|
287
|
-
// 触发了包管理器安装(
|
|
287
|
+
// 触发了包管理器安装(bun install --registry …)
|
|
288
288
|
expect(spawnCalls.length).toBe(1);
|
|
289
|
-
expect(spawnCalls[0]?.[0]).toBe("
|
|
290
|
-
expect(spawnCalls[0]?.[1]).toBe("install");
|
|
289
|
+
expect(spawnCalls[0]?.[0]).toBe("install");
|
|
291
290
|
// 重新读取临时 package.json:护栏项保持原样,新依赖加入
|
|
292
291
|
const manifest = JSON.parse(
|
|
293
292
|
await Bun.file(join(tmp, "package.json")).text(),
|
package/src/node/installer.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
|
-
import { createRequire } from "node:module";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
7
|
import { join, resolve } from "node:path";
|
|
9
8
|
import type {} from "@koishi-ce/console";
|
|
@@ -24,14 +23,14 @@ import type {} from "@koishi-ce/plugin-market";
|
|
|
24
23
|
import Scanner, {
|
|
25
24
|
type DependencyMetaKey,
|
|
26
25
|
isResidentInCache,
|
|
26
|
+
mapLimit,
|
|
27
27
|
type PackageJson,
|
|
28
28
|
type Registry,
|
|
29
29
|
type RemotePackage,
|
|
30
30
|
resolvePackageJson,
|
|
31
31
|
} from "@koishi-ce/registry";
|
|
32
|
-
import spawn from "execa";
|
|
33
|
-
import pMap from "p-map";
|
|
34
32
|
import { compare, satisfies, valid } from "semver";
|
|
33
|
+
import { spawnBun } from "./proc.ts";
|
|
35
34
|
|
|
36
35
|
const logger = new Logger("market");
|
|
37
36
|
|
|
@@ -87,12 +86,6 @@ function getLocalRegistry(cwd: string, userHome: string = homedir()): string {
|
|
|
87
86
|
return "https://registry.npmjs.org/";
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
// 经 createRequire 加载 CJS 包并就地断言签名:不走 ESM 导入互操作,
|
|
91
|
-
// 规避多包合并类型检查(大一统 tsconfig)下 export = 交织失效问题
|
|
92
|
-
const whichPMRuns = createRequire(import.meta.url)("which-pm-runs") as () =>
|
|
93
|
-
| undefined
|
|
94
|
-
| { name: string; version: string };
|
|
95
|
-
|
|
96
89
|
export interface Dependency {
|
|
97
90
|
/**
|
|
98
91
|
* requested semver range
|
|
@@ -112,28 +105,14 @@ export interface Dependency {
|
|
|
112
105
|
latest?: string | undefined;
|
|
113
106
|
}
|
|
114
107
|
|
|
115
|
-
|
|
116
|
-
type: "warning" | "info" | "error" | string;
|
|
117
|
-
name: number | null;
|
|
118
|
-
displayName: string;
|
|
119
|
-
indent?: string;
|
|
120
|
-
data: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const levelMap = {
|
|
124
|
-
info: "info",
|
|
125
|
-
warning: "debug",
|
|
126
|
-
error: "warn",
|
|
127
|
-
} as const;
|
|
128
|
-
|
|
129
|
-
export interface LocalPackage extends PackageJson {
|
|
108
|
+
interface LocalPackage extends PackageJson {
|
|
130
109
|
private?: boolean;
|
|
131
110
|
$workspace?: boolean;
|
|
132
111
|
/** loadManifest 归一化保证 dependencies 必有 */
|
|
133
112
|
dependencies: Record<string, string>;
|
|
134
113
|
}
|
|
135
114
|
|
|
136
|
-
|
|
115
|
+
function loadManifest(name: string) {
|
|
137
116
|
// resolvePackageJson 以纯 fs 探测为主路径:市场安装流程在包落盘前的
|
|
138
117
|
// 探测不能触碰解析 API,否则触发 Bun 的父目录快照缓存(装完即失败)
|
|
139
118
|
const filename = resolvePackageJson(name);
|
|
@@ -170,7 +149,6 @@ class Installer extends Service {
|
|
|
170
149
|
private pkgTasks: Dict<
|
|
171
150
|
Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
172
151
|
> = {};
|
|
173
|
-
private agent = whichPMRuns();
|
|
174
152
|
private manifest: LocalPackage;
|
|
175
153
|
private declare depTask: Promise<Dict<Dependency>>;
|
|
176
154
|
private flushData: () => void;
|
|
@@ -259,28 +237,24 @@ class Installer extends Service {
|
|
|
259
237
|
const result = valueMap(this.manifest.dependencies, (request) => {
|
|
260
238
|
return { request: request.replace(/^[~^]/, "") } as Dependency;
|
|
261
239
|
});
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (meta.$workspace) return;
|
|
273
|
-
} catch {}
|
|
274
|
-
|
|
275
|
-
if (!valid(dep.request)) {
|
|
276
|
-
dep.invalid = true;
|
|
277
|
-
}
|
|
240
|
+
await mapLimit(Object.keys(result), 10, async (name) => {
|
|
241
|
+
const dep = result[name];
|
|
242
|
+
if (!dep) return;
|
|
243
|
+
try {
|
|
244
|
+
// some dependencies may be left with no local installation
|
|
245
|
+
const meta = loadManifest(name);
|
|
246
|
+
dep.resolved = meta.version;
|
|
247
|
+
dep.workspace = meta.$workspace;
|
|
248
|
+
if (meta.$workspace) return;
|
|
249
|
+
} catch {}
|
|
278
250
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
251
|
+
if (!valid(dep.request)) {
|
|
252
|
+
dep.invalid = true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const versions = await this.getPackage(name);
|
|
256
|
+
if (versions) dep.latest = Object.keys(versions)[0];
|
|
257
|
+
});
|
|
284
258
|
return result;
|
|
285
259
|
}
|
|
286
260
|
|
|
@@ -303,19 +277,18 @@ class Installer extends Service {
|
|
|
303
277
|
}
|
|
304
278
|
|
|
305
279
|
async exec(args: string[]) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
280
|
+
// Bun-first:CE 生态只存在 bun 这一种包管理器,直接驱动 bun 执行
|
|
281
|
+
// 安装(上游经 which-pm-runs 探测 npm/yarn/bun,此处固定为 bun)
|
|
282
|
+
args.unshift("install");
|
|
309
283
|
return new Promise<number>((resolve) => {
|
|
310
|
-
|
|
311
|
-
const child = spawn(name, args, { cwd: this.cwd });
|
|
284
|
+
const child = spawnBun(args, this.cwd);
|
|
312
285
|
child.on("exit", (code) => resolve(code ?? -1));
|
|
313
286
|
child.on("error", () => resolve(-1));
|
|
314
287
|
|
|
315
288
|
let stderr = "";
|
|
316
289
|
child.stderr?.on("data", (data) => {
|
|
317
|
-
|
|
318
|
-
const lines =
|
|
290
|
+
stderr += data.toString();
|
|
291
|
+
const lines = stderr.split("\n");
|
|
319
292
|
stderr = lines.pop() ?? "";
|
|
320
293
|
for (const line of lines) {
|
|
321
294
|
logger.warn(line);
|
|
@@ -324,23 +297,11 @@ class Installer extends Service {
|
|
|
324
297
|
|
|
325
298
|
let stdout = "";
|
|
326
299
|
child.stdout?.on("data", (data) => {
|
|
327
|
-
|
|
328
|
-
const lines =
|
|
300
|
+
stdout += data.toString();
|
|
301
|
+
const lines = stdout.split("\n");
|
|
329
302
|
stdout = lines.pop() ?? "";
|
|
330
303
|
for (const line of lines) {
|
|
331
|
-
|
|
332
|
-
logger.info(line);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
try {
|
|
336
|
-
const { type, data } = JSON.parse(line) as YarnLog;
|
|
337
|
-
const level =
|
|
338
|
-
type in levelMap ? levelMap[type as keyof typeof levelMap] : null;
|
|
339
|
-
(level ? logger[level] : logger.info)(data);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
logger.warn(line);
|
|
342
|
-
logger.warn(error);
|
|
343
|
-
}
|
|
304
|
+
logger.info(line);
|
|
344
305
|
}
|
|
345
306
|
});
|
|
346
307
|
});
|
package/src/node/proc.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 安装子进程的创建封装(原依赖 execa)。
|
|
7
|
+
*
|
|
8
|
+
* execa 本质是 node:child_process 的事件流封装,本仓只用到其
|
|
9
|
+
* spawn + exit/error 事件 + stdout/stderr 流读取的子集,直接用
|
|
10
|
+
* node:child_process 等价实现即可,Bun 运行时完全兼容。刻意不用
|
|
11
|
+
* Bun.spawn 的捕获管道:win32 下其读端存在 EOF 竞态(见
|
|
12
|
+
* tooling/release/proc.ts 注释),而单进程安装场景 child_process
|
|
13
|
+
* 无此问题。子进程以 process.execPath 启动——宿主必为 Bun 运行时,
|
|
14
|
+
* 直接复用当前可执行文件,不依赖 PATH 中的 bun。
|
|
15
|
+
*
|
|
16
|
+
* 独立成模块是为了让测试能以 mock.module 按相对路径精确拦截,
|
|
17
|
+
* 不真正拉起安装进程(此前 mock execa 包名,全局替换同样可行,
|
|
18
|
+
* 但本地模块拦截范围更小、不受其它包使用 child_process 的干扰)。
|
|
19
|
+
*/
|
|
20
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
21
|
+
|
|
22
|
+
/** 在指定工作目录启动 bun 子进程(args 已含子命令与参数),stdio 三路 pipe。 */
|
|
23
|
+
export function spawnBun(args: string[], cwd: string): ChildProcess {
|
|
24
|
+
return spawn(process.execPath, args, { cwd });
|
|
25
|
+
}
|