@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 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 "execa";
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 pMap(Object.keys(result), async (name) => {
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
- }, { concurrency: 10 });
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
- const name = this.agent?.name ?? "npm";
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
- if (useJson) args.push("--json");
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
- data = stderr + data.toString();
209
- const lines = data.split("\n");
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
- data = stdout + data.toString();
216
- const lines = data.split("\n");
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.8",
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.2",
64
- "@koishi-ce/loader": "^1.0.3",
65
- "@koishi-ce/plugin-config": "^1.0.7",
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
- "vue": "^3.5.12"
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.4",
73
- "execa": "^5.1.1",
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
- * - 子进程(npm/yarn 安装)经 mock.module 拦截 execa,不落盘不联网安装;
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 execaMock = (name: string, args: string[]) => {
26
- spawnCalls.push([name, ...args]);
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("execa", () => ({ default: execaMock }));
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
- // 触发了包管理器安装(npm install --registry …)
287
+ // 触发了包管理器安装(bun install --registry …)
288
288
  expect(spawnCalls.length).toBe(1);
289
- expect(spawnCalls[0]?.[0]).toBe("npm");
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(),
@@ -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
- export interface YarnLog {
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
- export function loadManifest(name: string) {
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 pMap(
263
- Object.keys(result),
264
- async (name) => {
265
- const dep = result[name];
266
- if (!dep) return;
267
- try {
268
- // some dependencies may be left with no local installation
269
- const meta = loadManifest(name);
270
- dep.resolved = meta.version;
271
- dep.workspace = meta.$workspace;
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
- const versions = await this.getPackage(name);
280
- if (versions) dep.latest = Object.keys(versions)[0];
281
- },
282
- { concurrency: 10 },
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
- const name = this.agent?.name ?? "npm";
307
- const useJson = name === "yarn" && (this.agent?.version ?? "1") >= "2";
308
- if (name !== "yarn") args.unshift("install");
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
- if (useJson) args.push("--json");
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
- data = stderr + data.toString();
318
- const lines = data.split("\n");
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
- data = stdout + data.toString();
328
- const lines = data.split("\n");
300
+ stdout += data.toString();
301
+ const lines = stdout.split("\n");
329
302
  stdout = lines.pop() ?? "";
330
303
  for (const line of lines) {
331
- if (!useJson || line[0] !== "{") {
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
  });
@@ -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
+ }