@koishi-ce/plugin-market 1.0.9 → 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 -1
- package/lib/index.mjs +4 -1
- package/package.json +3 -3
- package/src/__tests__/index.test.ts +580 -281
- 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 +120 -51
- package/src/node/market.ts +42 -21
- package/src/node/proc.ts +8 -2
- package/src/shared/index.ts +27 -12
package/src/browser/index.ts
CHANGED
|
@@ -25,6 +25,11 @@ export function apply(ctx: Context, _config: Config) {
|
|
|
25
25
|
ctx.console.addEntry(
|
|
26
26
|
base
|
|
27
27
|
? [`${base}/dist/index.js`, `${base}/dist/style.css`]
|
|
28
|
-
: [
|
|
28
|
+
: [
|
|
29
|
+
import.meta.url.replace(
|
|
30
|
+
/\/src\/[^/]+\/[^/]+$/,
|
|
31
|
+
"/client/index.ts",
|
|
32
|
+
),
|
|
33
|
+
],
|
|
29
34
|
);
|
|
30
35
|
}
|
package/src/browser/market.ts
CHANGED
|
@@ -12,10 +12,14 @@ export default class MarketProvider extends BaseMarketProvider {
|
|
|
12
12
|
|
|
13
13
|
override async get() {
|
|
14
14
|
const market = await this.prepare();
|
|
15
|
-
if (!market)
|
|
15
|
+
if (!market)
|
|
16
|
+
return { data: {}, failed: 0, total: 0, progress: 0 };
|
|
16
17
|
return {
|
|
17
18
|
data: Object.fromEntries(
|
|
18
|
-
market.objects.map((item) => [
|
|
19
|
+
market.objects.map((item) => [
|
|
20
|
+
item.package.name,
|
|
21
|
+
item,
|
|
22
|
+
]),
|
|
19
23
|
),
|
|
20
24
|
failed: 0,
|
|
21
25
|
total: market.objects.length,
|
package/src/node/deps.ts
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { DataService } from "@koishi-ce/console";
|
|
6
6
|
import type { Context, Dict } from "@koishi-ce/koishi";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
DependencyMetaKey,
|
|
9
|
+
RemotePackage,
|
|
10
|
+
} from "@koishi-ce/registry";
|
|
8
11
|
import type { Dependency } from "./installer.ts";
|
|
9
12
|
|
|
10
|
-
class DependencyProvider extends DataService<
|
|
13
|
+
class DependencyProvider extends DataService<
|
|
14
|
+
Dict<Dependency>
|
|
15
|
+
> {
|
|
11
16
|
constructor(ctx: Context) {
|
|
12
17
|
super(ctx, "dependencies", { authority: 4 });
|
|
13
18
|
}
|
package/src/node/index.ts
CHANGED
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type Context,
|
|
8
|
+
type Dict,
|
|
9
|
+
pick,
|
|
10
|
+
Schema,
|
|
11
|
+
} from "@koishi-ce/koishi";
|
|
12
|
+
import type {
|
|
13
|
+
DependencyMetaKey,
|
|
14
|
+
RemotePackage,
|
|
15
|
+
} from "@koishi-ce/registry";
|
|
8
16
|
import { gt } from "semver";
|
|
9
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DependencyProvider,
|
|
19
|
+
RegistryProvider,
|
|
20
|
+
} from "./deps.ts";
|
|
10
21
|
import Installer from "./installer.ts";
|
|
11
22
|
import messageZhCN from "./locales/message.zh-CN.yml";
|
|
12
23
|
import schemaZhCN from "./locales/schema.zh-CN.yml";
|
|
@@ -31,15 +42,25 @@ declare module "@koishi-ce/console" {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
interface Events {
|
|
34
|
-
"market/install"(
|
|
45
|
+
"market/install"(
|
|
46
|
+
deps: Dict<string>,
|
|
47
|
+
forced?: boolean,
|
|
48
|
+
): Promise<number>;
|
|
35
49
|
"market/registry"(
|
|
36
50
|
names: string[],
|
|
37
|
-
): Promise<
|
|
51
|
+
): Promise<
|
|
52
|
+
Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
53
|
+
>;
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
export const name = "market";
|
|
42
|
-
|
|
58
|
+
// loader 由宿主以 builtin 服务提供,生产恒存在;测试等裸 App 环境可缺席,
|
|
59
|
+
// 声明为非必需注入以免 cordis 报「property loader is not registered」
|
|
60
|
+
export const inject = {
|
|
61
|
+
http: { required: true },
|
|
62
|
+
loader: { required: false },
|
|
63
|
+
};
|
|
43
64
|
|
|
44
65
|
export const usage = `
|
|
45
66
|
如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。
|
|
@@ -101,12 +122,18 @@ export function apply(ctx: Context, config: Config) {
|
|
|
101
122
|
return session.text(".already-installed");
|
|
102
123
|
|
|
103
124
|
// find proper version
|
|
104
|
-
const result =
|
|
125
|
+
const result =
|
|
126
|
+
await ctx.installer.findVersion(names);
|
|
105
127
|
if (!result) return session.text(".not-found");
|
|
106
128
|
|
|
107
129
|
// set restart message
|
|
108
130
|
ctx.loader.envData.message = {
|
|
109
|
-
...pick(session, [
|
|
131
|
+
...pick(session, [
|
|
132
|
+
"sid",
|
|
133
|
+
"channelId",
|
|
134
|
+
"guildId",
|
|
135
|
+
"isDirect",
|
|
136
|
+
]),
|
|
110
137
|
content: session.text(".success"),
|
|
111
138
|
};
|
|
112
139
|
await ctx.installer.install(result);
|
|
@@ -125,7 +152,8 @@ export function apply(ctx: Context, config: Config) {
|
|
|
125
152
|
const names = ctx.installer.resolveName(name);
|
|
126
153
|
const deps = await ctx.installer.getDeps();
|
|
127
154
|
const installed = names.find((name) => deps[name]);
|
|
128
|
-
if (!installed)
|
|
155
|
+
if (!installed)
|
|
156
|
+
return session.text(".not-installed");
|
|
129
157
|
|
|
130
158
|
await ctx.installer.install({ [installed]: null });
|
|
131
159
|
return session.text(".success");
|
|
@@ -145,7 +173,9 @@ export function apply(ctx: Context, config: Config) {
|
|
|
145
173
|
const names = ctx.installer.resolveName(name);
|
|
146
174
|
return names.find((name) => deps[name]);
|
|
147
175
|
})
|
|
148
|
-
.filter(
|
|
176
|
+
.filter(
|
|
177
|
+
(name): name is string => name !== undefined,
|
|
178
|
+
);
|
|
149
179
|
if (options?.self) resolved.push("koishi");
|
|
150
180
|
return resolved;
|
|
151
181
|
}
|
|
@@ -154,17 +184,25 @@ export function apply(ctx: Context, config: Config) {
|
|
|
154
184
|
ctx.installer.refresh(true);
|
|
155
185
|
const deps = await ctx.installer.getDeps();
|
|
156
186
|
names = (
|
|
157
|
-
await getPackages(
|
|
187
|
+
await getPackages(
|
|
188
|
+
names.filter((name): name is string => !!name),
|
|
189
|
+
)
|
|
158
190
|
).filter((name) => {
|
|
159
|
-
const { latest, resolved, invalid } =
|
|
160
|
-
|
|
191
|
+
const { latest, resolved, invalid } =
|
|
192
|
+
deps[name] ?? {};
|
|
193
|
+
if (
|
|
194
|
+
latest === undefined ||
|
|
195
|
+
resolved === undefined
|
|
196
|
+
)
|
|
197
|
+
return false;
|
|
161
198
|
try {
|
|
162
199
|
return !invalid && gt(latest, resolved);
|
|
163
200
|
} catch {
|
|
164
201
|
return false;
|
|
165
202
|
}
|
|
166
203
|
});
|
|
167
|
-
if (!names.length)
|
|
204
|
+
if (!names.length)
|
|
205
|
+
return session.text(".all-updated");
|
|
168
206
|
|
|
169
207
|
const output = names.map((name) => {
|
|
170
208
|
const { latest, resolved } = deps[name] ?? {};
|
|
@@ -180,15 +218,24 @@ export function apply(ctx: Context, config: Config) {
|
|
|
180
218
|
}
|
|
181
219
|
|
|
182
220
|
ctx.loader.envData.message = {
|
|
183
|
-
...pick(session, [
|
|
221
|
+
...pick(session, [
|
|
222
|
+
"sid",
|
|
223
|
+
"channelId",
|
|
224
|
+
"guildId",
|
|
225
|
+
"isDirect",
|
|
226
|
+
]),
|
|
184
227
|
content: session.text(".success"),
|
|
185
228
|
};
|
|
186
229
|
await ctx.installer.install(
|
|
187
|
-
names.reduce<Dict<string | null>>(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
names.reduce<Dict<string | null>>(
|
|
231
|
+
(result, name) => {
|
|
232
|
+
const latest = deps[name]?.latest;
|
|
233
|
+
if (latest !== undefined)
|
|
234
|
+
result[name] = latest;
|
|
235
|
+
return result;
|
|
236
|
+
},
|
|
237
|
+
{},
|
|
238
|
+
),
|
|
192
239
|
);
|
|
193
240
|
ctx.loader.envData.message = null;
|
|
194
241
|
return session.text(".success");
|
|
@@ -210,7 +257,10 @@ export function apply(ctx: Context, config: Config) {
|
|
|
210
257
|
ctx.console.addListener(
|
|
211
258
|
"market/install",
|
|
212
259
|
async (deps, forced) => {
|
|
213
|
-
const code = await ctx.installer.install(
|
|
260
|
+
const code = await ctx.installer.install(
|
|
261
|
+
deps,
|
|
262
|
+
forced,
|
|
263
|
+
);
|
|
214
264
|
ctx.get("console")?.refresh("dependencies");
|
|
215
265
|
ctx.get("console")?.refresh("registry");
|
|
216
266
|
ctx.get("console")?.refresh("packages");
|
|
@@ -223,7 +273,9 @@ export function apply(ctx: Context, config: Config) {
|
|
|
223
273
|
"market/registry",
|
|
224
274
|
async (names) => {
|
|
225
275
|
const meta = await Promise.all(
|
|
226
|
-
names.map((name) =>
|
|
276
|
+
names.map((name) =>
|
|
277
|
+
ctx.installer.getPackage(name),
|
|
278
|
+
),
|
|
227
279
|
);
|
|
228
280
|
return Object.fromEntries(
|
|
229
281
|
meta.map((meta, index) => [names[index], meta]),
|
package/src/node/installer.ts
CHANGED
|
@@ -44,7 +44,9 @@ const logger = new Logger("market");
|
|
|
44
44
|
* 两者被覆盖或删除都会让 peer 解析失去归属,重新拉下 npm 官方包形成
|
|
45
45
|
* 第二份框架副本。
|
|
46
46
|
*/
|
|
47
|
-
function isGuardedRequest(
|
|
47
|
+
function isGuardedRequest(
|
|
48
|
+
request: string | undefined,
|
|
49
|
+
): boolean {
|
|
48
50
|
return (
|
|
49
51
|
request?.startsWith("workspace:") === true ||
|
|
50
52
|
request?.startsWith("npm:@koishi-ce") === true
|
|
@@ -52,10 +54,16 @@ function isGuardedRequest(request: string | undefined): boolean {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** 从单个 .npmrc 文件提取 registry 配置项;文件不存在或读取出错一律视为未配置 */
|
|
55
|
-
function readNpmrcRegistry(
|
|
57
|
+
function readNpmrcRegistry(
|
|
58
|
+
file: string,
|
|
59
|
+
): string | undefined {
|
|
56
60
|
try {
|
|
57
|
-
for (const line of readFileSync(file, "utf8").split(
|
|
58
|
-
|
|
61
|
+
for (const line of readFileSync(file, "utf8").split(
|
|
62
|
+
/\r?\n/,
|
|
63
|
+
)) {
|
|
64
|
+
const matched = /^\s*registry\s*=\s*(\S+)\s*$/.exec(
|
|
65
|
+
line,
|
|
66
|
+
);
|
|
59
67
|
const value = matched?.[1];
|
|
60
68
|
if (value) return value;
|
|
61
69
|
}
|
|
@@ -72,14 +80,20 @@ function readNpmrcRegistry(file: string): string | undefined {
|
|
|
72
80
|
* 源(对齐被移除的 get-registry 的默认值),保证 http 恒有 endpoint
|
|
73
81
|
* 可拼接相对 URL——缺省时相对请求会在 resolveURL 抛 Invalid URL。
|
|
74
82
|
*/
|
|
75
|
-
function getLocalRegistry(
|
|
83
|
+
function getLocalRegistry(
|
|
84
|
+
cwd: string,
|
|
85
|
+
userHome: string = homedir(),
|
|
86
|
+
): string {
|
|
76
87
|
const candidates = [
|
|
77
88
|
process.env["npm_config_registry"],
|
|
78
89
|
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
79
90
|
readNpmrcRegistry(join(userHome, ".npmrc")),
|
|
80
91
|
];
|
|
81
92
|
for (const candidate of candidates) {
|
|
82
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
candidate?.startsWith("https://") ||
|
|
95
|
+
candidate?.startsWith("http://")
|
|
96
|
+
) {
|
|
83
97
|
return candidate;
|
|
84
98
|
}
|
|
85
99
|
}
|
|
@@ -116,9 +130,15 @@ function loadManifest(name: string) {
|
|
|
116
130
|
// resolvePackageJson 以纯 fs 探测为主路径:市场安装流程在包落盘前的
|
|
117
131
|
// 探测不能触碰解析 API,否则触发 Bun 的父目录快照缓存(装完即失败)
|
|
118
132
|
const filename = resolvePackageJson(name);
|
|
119
|
-
const meta: LocalPackage = JSON.parse(
|
|
133
|
+
const meta: LocalPackage = JSON.parse(
|
|
134
|
+
readFileSync(filename, "utf8"),
|
|
135
|
+
);
|
|
120
136
|
meta.dependencies ||= {};
|
|
121
|
-
defineProperty(
|
|
137
|
+
defineProperty(
|
|
138
|
+
meta,
|
|
139
|
+
"$workspace",
|
|
140
|
+
!filename.includes("node_modules"),
|
|
141
|
+
);
|
|
122
142
|
return meta;
|
|
123
143
|
}
|
|
124
144
|
|
|
@@ -143,8 +163,12 @@ function getVersions(versions: RemotePackage[]) {
|
|
|
143
163
|
class Installer extends Service {
|
|
144
164
|
declare http: HTTP;
|
|
145
165
|
declare endpoint: string | undefined;
|
|
146
|
-
public fullCache: Dict<
|
|
147
|
-
|
|
166
|
+
public fullCache: Dict<
|
|
167
|
+
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
168
|
+
> = {};
|
|
169
|
+
public tempCache: Dict<
|
|
170
|
+
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
171
|
+
> = {};
|
|
148
172
|
|
|
149
173
|
private pkgTasks: Dict<
|
|
150
174
|
Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
@@ -160,7 +184,9 @@ class Installer extends Service {
|
|
|
160
184
|
this.config = config;
|
|
161
185
|
this.manifest = loadManifest(this.cwd);
|
|
162
186
|
this.flushData = ctx.throttle(() => {
|
|
163
|
-
ctx
|
|
187
|
+
ctx
|
|
188
|
+
.get("console")
|
|
189
|
+
?.broadcast("market/registry", this.tempCache);
|
|
164
190
|
this.tempCache = {};
|
|
165
191
|
}, 500);
|
|
166
192
|
}
|
|
@@ -185,7 +211,10 @@ class Installer extends Service {
|
|
|
185
211
|
const [left, right] = name.split("/");
|
|
186
212
|
return [`${left}/koishi-plugin-${right}`];
|
|
187
213
|
} else {
|
|
188
|
-
return [
|
|
214
|
+
return [
|
|
215
|
+
`@koishijs/plugin-${name}`,
|
|
216
|
+
`koishi-plugin-${name}`,
|
|
217
|
+
];
|
|
189
218
|
}
|
|
190
219
|
}
|
|
191
220
|
|
|
@@ -193,7 +222,9 @@ class Installer extends Service {
|
|
|
193
222
|
const entries = await Promise.all(
|
|
194
223
|
names.map(async (name) => {
|
|
195
224
|
try {
|
|
196
|
-
const versions = Object.entries(
|
|
225
|
+
const versions = Object.entries(
|
|
226
|
+
await this.getPackage(name),
|
|
227
|
+
);
|
|
197
228
|
const [latest] = versions;
|
|
198
229
|
if (!latest) return undefined;
|
|
199
230
|
return { [name]: latest[0] };
|
|
@@ -202,19 +233,30 @@ class Installer extends Service {
|
|
|
202
233
|
}
|
|
203
234
|
}),
|
|
204
235
|
);
|
|
205
|
-
return entries.find(
|
|
236
|
+
return entries.find(
|
|
237
|
+
(entry): entry is Dict<string> => entry !== undefined,
|
|
238
|
+
);
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
private async _getPackage(name: string) {
|
|
209
242
|
try {
|
|
210
|
-
const registry = await this.http.get<Registry>(
|
|
243
|
+
const registry = await this.http.get<Registry>(
|
|
244
|
+
`/${name}`,
|
|
245
|
+
);
|
|
211
246
|
const versions = getVersions(
|
|
212
|
-
Object.values(registry.versions).filter(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
Object.values(registry.versions).filter(
|
|
248
|
+
(remote) => {
|
|
249
|
+
if (name === "koishi")
|
|
250
|
+
return satisfies(remote.version, "4");
|
|
251
|
+
return (
|
|
252
|
+
!Scanner.isPlugin(name) ||
|
|
253
|
+
Scanner.isCompatible("4", remote)
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
),
|
|
216
257
|
);
|
|
217
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
258
|
+
this.fullCache[name] = this.tempCache[name] =
|
|
259
|
+
versions;
|
|
218
260
|
this.flushData();
|
|
219
261
|
return versions;
|
|
220
262
|
} catch (error) {
|
|
@@ -224,9 +266,12 @@ class Installer extends Service {
|
|
|
224
266
|
}
|
|
225
267
|
|
|
226
268
|
setPackage(name: string, versions: RemotePackage[]) {
|
|
227
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
269
|
+
this.fullCache[name] = this.tempCache[name] =
|
|
270
|
+
getVersions(versions);
|
|
228
271
|
this.flushData();
|
|
229
|
-
this.pkgTasks[name] = Promise.resolve(
|
|
272
|
+
this.pkgTasks[name] = Promise.resolve(
|
|
273
|
+
this.fullCache[name],
|
|
274
|
+
);
|
|
230
275
|
}
|
|
231
276
|
|
|
232
277
|
getPackage(name: string) {
|
|
@@ -234,27 +279,36 @@ class Installer extends Service {
|
|
|
234
279
|
}
|
|
235
280
|
|
|
236
281
|
private async _getDeps() {
|
|
237
|
-
const result = valueMap(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
const result = valueMap(
|
|
283
|
+
this.manifest.dependencies,
|
|
284
|
+
(request) => {
|
|
285
|
+
return {
|
|
286
|
+
request: request.replace(/^[~^]/, ""),
|
|
287
|
+
} as Dependency;
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
await mapLimit(
|
|
291
|
+
Object.keys(result),
|
|
292
|
+
10,
|
|
293
|
+
async (name) => {
|
|
294
|
+
const dep = result[name];
|
|
295
|
+
if (!dep) return;
|
|
296
|
+
try {
|
|
297
|
+
// some dependencies may be left with no local installation
|
|
298
|
+
const meta = loadManifest(name);
|
|
299
|
+
dep.resolved = meta.version;
|
|
300
|
+
dep.workspace = meta.$workspace;
|
|
301
|
+
if (meta.$workspace) return;
|
|
302
|
+
} catch {}
|
|
303
|
+
|
|
304
|
+
if (!valid(dep.request)) {
|
|
305
|
+
dep.invalid = true;
|
|
306
|
+
}
|
|
254
307
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
308
|
+
const versions = await this.getPackage(name);
|
|
309
|
+
if (versions) dep.latest = Object.keys(versions)[0];
|
|
310
|
+
},
|
|
311
|
+
);
|
|
258
312
|
return result;
|
|
259
313
|
}
|
|
260
314
|
|
|
@@ -311,26 +365,35 @@ class Installer extends Service {
|
|
|
311
365
|
const filename = resolve(this.cwd, "package.json");
|
|
312
366
|
// 现读现写:this.manifest 原为构造期的启动快照,运行期间根
|
|
313
367
|
// package.json 可能已被外部更新,基于快照整体重写会抹掉变更
|
|
314
|
-
this.manifest = JSON.parse(
|
|
368
|
+
this.manifest = JSON.parse(
|
|
369
|
+
readFileSync(filename, "utf8"),
|
|
370
|
+
) as LocalPackage;
|
|
315
371
|
this.manifest.dependencies ||= {};
|
|
316
372
|
for (const key in deps) {
|
|
317
373
|
if (deps[key]) {
|
|
318
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
isGuardedRequest(this.manifest.dependencies[key])
|
|
376
|
+
) {
|
|
319
377
|
continue;
|
|
320
378
|
}
|
|
321
379
|
this.manifest.dependencies[key] = deps[key];
|
|
322
|
-
} else if (
|
|
380
|
+
} else if (
|
|
381
|
+
!isGuardedRequest(this.manifest.dependencies[key])
|
|
382
|
+
) {
|
|
323
383
|
delete this.manifest.dependencies[key];
|
|
324
384
|
}
|
|
325
385
|
}
|
|
326
386
|
this.manifest.dependencies = Object.fromEntries(
|
|
327
|
-
Object.entries(this.manifest.dependencies).sort(
|
|
328
|
-
a[0].localeCompare(b[0]),
|
|
387
|
+
Object.entries(this.manifest.dependencies).sort(
|
|
388
|
+
(a, b) => a[0].localeCompare(b[0]),
|
|
329
389
|
),
|
|
330
390
|
);
|
|
331
391
|
// 仓库格式权威是 biome(tab 缩进),按 tab 写出避免装插件后
|
|
332
392
|
// package.json 被重排成空格、lint 报格式漂移
|
|
333
|
-
await Bun.write(
|
|
393
|
+
await Bun.write(
|
|
394
|
+
filename,
|
|
395
|
+
`${JSON.stringify(this.manifest, null, "\t")}\n`,
|
|
396
|
+
);
|
|
334
397
|
}
|
|
335
398
|
|
|
336
399
|
private _install() {
|
|
@@ -353,7 +416,10 @@ class Installer extends Service {
|
|
|
353
416
|
});
|
|
354
417
|
}
|
|
355
418
|
|
|
356
|
-
async install(
|
|
419
|
+
async install(
|
|
420
|
+
deps: Dict<string | null>,
|
|
421
|
+
forced?: boolean,
|
|
422
|
+
) {
|
|
357
423
|
const localDeps = this._getLocalDeps(deps);
|
|
358
424
|
await this.override(deps);
|
|
359
425
|
|
|
@@ -365,7 +431,9 @@ class Installer extends Service {
|
|
|
365
431
|
local?.workspace ||
|
|
366
432
|
(request &&
|
|
367
433
|
local?.resolved &&
|
|
368
|
-
satisfies(local.resolved, request, {
|
|
434
|
+
satisfies(local.resolved, request, {
|
|
435
|
+
includePrerelease: true,
|
|
436
|
+
}))
|
|
369
437
|
)
|
|
370
438
|
continue;
|
|
371
439
|
shouldInstall = true;
|
|
@@ -386,7 +454,8 @@ class Installer extends Service {
|
|
|
386
454
|
if (newDep.resolved === local.resolved) continue;
|
|
387
455
|
// 版本变化且旧版本仍驻留内存时才需要整进程重载(判定不走
|
|
388
456
|
// 解析 API,规避 Bun 负缓存,见 isResidentInCache)
|
|
389
|
-
if (isResidentInCache(name))
|
|
457
|
+
if (isResidentInCache(name))
|
|
458
|
+
this.ctx.loader.fullReload();
|
|
390
459
|
}
|
|
391
460
|
this.refreshData();
|
|
392
461
|
|
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
CHANGED
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
* 不真正拉起安装进程(此前 mock execa 包名,全局替换同样可行,
|
|
18
18
|
* 但本地模块拦截范围更小、不受其它包使用 child_process 的干扰)。
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type ChildProcess,
|
|
22
|
+
spawn,
|
|
23
|
+
} from "node:child_process";
|
|
21
24
|
|
|
22
25
|
/** 在指定工作目录启动 bun 子进程(args 已含子命令与参数),stdio 三路 pipe。 */
|
|
23
|
-
export function spawnBun(
|
|
26
|
+
export function spawnBun(
|
|
27
|
+
args: string[],
|
|
28
|
+
cwd: string,
|
|
29
|
+
): ChildProcess {
|
|
24
30
|
return spawn(process.execPath, args, { cwd });
|
|
25
31
|
}
|