@koishi-ce/plugin-market 1.0.8 → 1.0.10
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/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/lib/index.d.ts +8 -2
- package/lib/index.mjs +36 -37
- package/package.json +9 -10
- package/src/__tests__/index.test.ts +584 -286
- package/src/browser/index.ts +6 -1
- package/src/browser/market.ts +6 -2
- package/src/node/deps.ts +7 -2
- package/src/node/index.ts +74 -22
- package/src/node/installer.ts +114 -84
- package/src/node/market.ts +42 -21
- package/src/node/proc.ts +31 -0
- package/src/shared/index.ts +27 -12
package/src/node/market.ts
CHANGED
|
@@ -41,7 +41,8 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
41
41
|
constructor(ctx: Context, config: MarketProvider.Config) {
|
|
42
42
|
super(ctx);
|
|
43
43
|
this.config = config;
|
|
44
|
-
if (config.endpoint)
|
|
44
|
+
if (config.endpoint)
|
|
45
|
+
this.http = ctx.http.extend(config);
|
|
45
46
|
this.flushData = ctx.throttle(() => {
|
|
46
47
|
ctx.console.broadcast("market/patch", {
|
|
47
48
|
data: this.tempCache,
|
|
@@ -88,7 +89,11 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
88
89
|
// 可调用的 HTTP 实例本身),须包一层保持绑定
|
|
89
90
|
return await http.get<T>(url, config);
|
|
90
91
|
} catch (error) {
|
|
91
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
attempt >= maxRetries ||
|
|
94
|
+
!this.isRetryable(error)
|
|
95
|
+
)
|
|
96
|
+
throw error;
|
|
92
97
|
const retryAfter = Number(
|
|
93
98
|
error.response?.headers.get("retry-after") ?? "",
|
|
94
99
|
);
|
|
@@ -113,16 +118,25 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
113
118
|
const registry = this.ctx.installer.http;
|
|
114
119
|
|
|
115
120
|
this.failed = [];
|
|
116
|
-
this.scanner = new Scanner(
|
|
117
|
-
|
|
121
|
+
this.scanner = new Scanner(
|
|
122
|
+
<T>(url: string, config?: RequestConfig) =>
|
|
123
|
+
this.request<T>(registry, url, config),
|
|
118
124
|
);
|
|
119
125
|
if (this.http) {
|
|
120
|
-
const result = await this.request<SearchResult>(
|
|
121
|
-
|
|
126
|
+
const result = await this.request<SearchResult>(
|
|
127
|
+
this.http,
|
|
128
|
+
"",
|
|
129
|
+
);
|
|
130
|
+
this.scanner.objects = result.objects.filter(
|
|
131
|
+
(object) => !object.ignored,
|
|
132
|
+
);
|
|
122
133
|
this.scanner.total = this.scanner.objects.length;
|
|
123
|
-
if (result.version !== undefined)
|
|
134
|
+
if (result.version !== undefined)
|
|
135
|
+
this.scanner.version = result.version;
|
|
124
136
|
} else {
|
|
125
|
-
await this.scanner.collect(
|
|
137
|
+
await this.scanner.collect(
|
|
138
|
+
timeout !== undefined ? { timeout } : {},
|
|
139
|
+
);
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
if (!this.scanner.version) {
|
|
@@ -150,16 +164,18 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
150
164
|
}
|
|
151
165
|
},
|
|
152
166
|
onRegistry: (registry, versions) => {
|
|
153
|
-
this.ctx.installer.setPackage(
|
|
167
|
+
this.ctx.installer.setPackage(
|
|
168
|
+
registry.name,
|
|
169
|
+
versions,
|
|
170
|
+
);
|
|
154
171
|
},
|
|
155
172
|
onSuccess: (object, _versions) => {
|
|
156
173
|
// npmmirror lacks `links` field
|
|
157
174
|
object.package.links ||= {
|
|
158
175
|
npm: `${registry.config.endpoint?.replace("registry.", "www.") ?? ""}/package/${object.package.name}`,
|
|
159
176
|
};
|
|
160
|
-
this.fullCache[object.package.name] =
|
|
161
|
-
object.package.name
|
|
162
|
-
] = object;
|
|
177
|
+
this.fullCache[object.package.name] =
|
|
178
|
+
this.tempCache[object.package.name] = object;
|
|
163
179
|
},
|
|
164
180
|
after: () => this.flushData(),
|
|
165
181
|
});
|
|
@@ -170,13 +186,17 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
170
186
|
|
|
171
187
|
override async get() {
|
|
172
188
|
await this.prepare();
|
|
173
|
-
if (this._error)
|
|
189
|
+
if (this._error)
|
|
190
|
+
return { data: {}, failed: 0, total: 0, progress: 0 };
|
|
174
191
|
const gravatar = process.env["GRAVATAR_MIRROR"];
|
|
175
192
|
return this.scanner.version
|
|
176
193
|
? {
|
|
177
194
|
registry: this.ctx.installer.endpoint,
|
|
178
195
|
data: Object.fromEntries(
|
|
179
|
-
this.scanner.objects.map((item) => [
|
|
196
|
+
this.scanner.objects.map((item) => [
|
|
197
|
+
item.package.name,
|
|
198
|
+
item,
|
|
199
|
+
]),
|
|
180
200
|
),
|
|
181
201
|
failed: 0,
|
|
182
202
|
total: this.scanner.total,
|
|
@@ -195,13 +215,14 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
195
215
|
|
|
196
216
|
// erasableSyntaxOnly 禁止含运行时值的 namespace,
|
|
197
217
|
// 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
|
|
198
|
-
static Config: Schema<MarketProvider.Config> =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
static Config: Schema<MarketProvider.Config> =
|
|
219
|
+
Schema.object({
|
|
220
|
+
endpoint: Schema.string().role("link"),
|
|
221
|
+
timeout: Schema.number()
|
|
222
|
+
.role("time")
|
|
223
|
+
.default(Time.second * 30),
|
|
224
|
+
proxyAgent: Schema.string().role("link"),
|
|
225
|
+
});
|
|
205
226
|
}
|
|
206
227
|
|
|
207
228
|
declare namespace MarketProvider {
|
package/src/node/proc.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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 {
|
|
21
|
+
type ChildProcess,
|
|
22
|
+
spawn,
|
|
23
|
+
} from "node:child_process";
|
|
24
|
+
|
|
25
|
+
/** 在指定工作目录启动 bun 子进程(args 已含子命令与参数),stdio 三路 pipe。 */
|
|
26
|
+
export function spawnBun(
|
|
27
|
+
args: string[],
|
|
28
|
+
cwd: string,
|
|
29
|
+
): ChildProcess {
|
|
30
|
+
return spawn(process.execPath, args, { cwd });
|
|
31
|
+
}
|
package/src/shared/index.ts
CHANGED
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
Logger,
|
|
11
11
|
Time,
|
|
12
12
|
} from "@koishi-ce/koishi";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
SearchObject,
|
|
15
|
+
SearchResult,
|
|
16
|
+
} from "@koishi-ce/registry";
|
|
14
17
|
|
|
15
18
|
declare module "@koishi-ce/console" {
|
|
16
19
|
interface Events {
|
|
@@ -27,21 +30,31 @@ declare module "@koishi-ce/console" {
|
|
|
27
30
|
const logger = new Logger("market");
|
|
28
31
|
|
|
29
32
|
export abstract class MarketProvider extends DataService<MarketProvider.Payload> {
|
|
30
|
-
private _task: Promise<SearchResult | undefined> | null =
|
|
33
|
+
private _task: Promise<SearchResult | undefined> | null =
|
|
34
|
+
null;
|
|
31
35
|
private _timestamp = 0;
|
|
32
36
|
protected _error: unknown;
|
|
33
37
|
|
|
34
38
|
constructor(ctx: Context) {
|
|
35
39
|
super(ctx, "market", { authority: 4 });
|
|
36
40
|
|
|
37
|
-
ctx.console.addListener(
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
ctx.console.addListener(
|
|
42
|
+
"market/refresh",
|
|
43
|
+
() => this.start(true),
|
|
44
|
+
{
|
|
45
|
+
authority: 4,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
40
48
|
|
|
41
49
|
ctx.on("console/connection", async (client) => {
|
|
42
50
|
if (!ctx.console.clients[client.id]) return;
|
|
43
|
-
if (Date.now() - this._timestamp <= Time.hour * 12)
|
|
44
|
-
|
|
51
|
+
if (Date.now() - this._timestamp <= Time.hour * 12)
|
|
52
|
+
return;
|
|
53
|
+
if (
|
|
54
|
+
await this.ctx.serial("console/intercept", client, {
|
|
55
|
+
authority: 4,
|
|
56
|
+
})
|
|
57
|
+
)
|
|
45
58
|
return;
|
|
46
59
|
this.start();
|
|
47
60
|
});
|
|
@@ -57,11 +70,13 @@ export abstract class MarketProvider extends DataService<MarketProvider.Payload>
|
|
|
57
70
|
abstract collect(): Promise<SearchResult | undefined>;
|
|
58
71
|
|
|
59
72
|
async prepare(): Promise<SearchResult | undefined> {
|
|
60
|
-
return (this._task ||= this.collect().catch(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
return (this._task ||= this.collect().catch(
|
|
74
|
+
(error: unknown) => {
|
|
75
|
+
logger.warn(error);
|
|
76
|
+
this._error = error;
|
|
77
|
+
return undefined;
|
|
78
|
+
},
|
|
79
|
+
));
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|