@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/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
|
@@ -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
|
|
|
@@ -45,7 +44,9 @@ const logger = new Logger("market");
|
|
|
45
44
|
* 两者被覆盖或删除都会让 peer 解析失去归属,重新拉下 npm 官方包形成
|
|
46
45
|
* 第二份框架副本。
|
|
47
46
|
*/
|
|
48
|
-
function isGuardedRequest(
|
|
47
|
+
function isGuardedRequest(
|
|
48
|
+
request: string | undefined,
|
|
49
|
+
): boolean {
|
|
49
50
|
return (
|
|
50
51
|
request?.startsWith("workspace:") === true ||
|
|
51
52
|
request?.startsWith("npm:@koishi-ce") === true
|
|
@@ -53,10 +54,16 @@ function isGuardedRequest(request: string | undefined): boolean {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** 从单个 .npmrc 文件提取 registry 配置项;文件不存在或读取出错一律视为未配置 */
|
|
56
|
-
function readNpmrcRegistry(
|
|
57
|
+
function readNpmrcRegistry(
|
|
58
|
+
file: string,
|
|
59
|
+
): string | undefined {
|
|
57
60
|
try {
|
|
58
|
-
for (const line of readFileSync(file, "utf8").split(
|
|
59
|
-
|
|
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
|
+
);
|
|
60
67
|
const value = matched?.[1];
|
|
61
68
|
if (value) return value;
|
|
62
69
|
}
|
|
@@ -73,26 +80,26 @@ function readNpmrcRegistry(file: string): string | undefined {
|
|
|
73
80
|
* 源(对齐被移除的 get-registry 的默认值),保证 http 恒有 endpoint
|
|
74
81
|
* 可拼接相对 URL——缺省时相对请求会在 resolveURL 抛 Invalid URL。
|
|
75
82
|
*/
|
|
76
|
-
function getLocalRegistry(
|
|
83
|
+
function getLocalRegistry(
|
|
84
|
+
cwd: string,
|
|
85
|
+
userHome: string = homedir(),
|
|
86
|
+
): string {
|
|
77
87
|
const candidates = [
|
|
78
88
|
process.env["npm_config_registry"],
|
|
79
89
|
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
80
90
|
readNpmrcRegistry(join(userHome, ".npmrc")),
|
|
81
91
|
];
|
|
82
92
|
for (const candidate of candidates) {
|
|
83
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
candidate?.startsWith("https://") ||
|
|
95
|
+
candidate?.startsWith("http://")
|
|
96
|
+
) {
|
|
84
97
|
return candidate;
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
return "https://registry.npmjs.org/";
|
|
88
101
|
}
|
|
89
102
|
|
|
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
103
|
export interface Dependency {
|
|
97
104
|
/**
|
|
98
105
|
* requested semver range
|
|
@@ -112,34 +119,26 @@ export interface Dependency {
|
|
|
112
119
|
latest?: string | undefined;
|
|
113
120
|
}
|
|
114
121
|
|
|
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 {
|
|
122
|
+
interface LocalPackage extends PackageJson {
|
|
130
123
|
private?: boolean;
|
|
131
124
|
$workspace?: boolean;
|
|
132
125
|
/** loadManifest 归一化保证 dependencies 必有 */
|
|
133
126
|
dependencies: Record<string, string>;
|
|
134
127
|
}
|
|
135
128
|
|
|
136
|
-
|
|
129
|
+
function loadManifest(name: string) {
|
|
137
130
|
// resolvePackageJson 以纯 fs 探测为主路径:市场安装流程在包落盘前的
|
|
138
131
|
// 探测不能触碰解析 API,否则触发 Bun 的父目录快照缓存(装完即失败)
|
|
139
132
|
const filename = resolvePackageJson(name);
|
|
140
|
-
const meta: LocalPackage = JSON.parse(
|
|
133
|
+
const meta: LocalPackage = JSON.parse(
|
|
134
|
+
readFileSync(filename, "utf8"),
|
|
135
|
+
);
|
|
141
136
|
meta.dependencies ||= {};
|
|
142
|
-
defineProperty(
|
|
137
|
+
defineProperty(
|
|
138
|
+
meta,
|
|
139
|
+
"$workspace",
|
|
140
|
+
!filename.includes("node_modules"),
|
|
141
|
+
);
|
|
143
142
|
return meta;
|
|
144
143
|
}
|
|
145
144
|
|
|
@@ -164,13 +163,16 @@ function getVersions(versions: RemotePackage[]) {
|
|
|
164
163
|
class Installer extends Service {
|
|
165
164
|
declare http: HTTP;
|
|
166
165
|
declare endpoint: string | undefined;
|
|
167
|
-
public fullCache: Dict<
|
|
168
|
-
|
|
166
|
+
public fullCache: Dict<
|
|
167
|
+
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
168
|
+
> = {};
|
|
169
|
+
public tempCache: Dict<
|
|
170
|
+
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
171
|
+
> = {};
|
|
169
172
|
|
|
170
173
|
private pkgTasks: Dict<
|
|
171
174
|
Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
172
175
|
> = {};
|
|
173
|
-
private agent = whichPMRuns();
|
|
174
176
|
private manifest: LocalPackage;
|
|
175
177
|
private declare depTask: Promise<Dict<Dependency>>;
|
|
176
178
|
private flushData: () => void;
|
|
@@ -182,7 +184,9 @@ class Installer extends Service {
|
|
|
182
184
|
this.config = config;
|
|
183
185
|
this.manifest = loadManifest(this.cwd);
|
|
184
186
|
this.flushData = ctx.throttle(() => {
|
|
185
|
-
ctx
|
|
187
|
+
ctx
|
|
188
|
+
.get("console")
|
|
189
|
+
?.broadcast("market/registry", this.tempCache);
|
|
186
190
|
this.tempCache = {};
|
|
187
191
|
}, 500);
|
|
188
192
|
}
|
|
@@ -207,7 +211,10 @@ class Installer extends Service {
|
|
|
207
211
|
const [left, right] = name.split("/");
|
|
208
212
|
return [`${left}/koishi-plugin-${right}`];
|
|
209
213
|
} else {
|
|
210
|
-
return [
|
|
214
|
+
return [
|
|
215
|
+
`@koishijs/plugin-${name}`,
|
|
216
|
+
`koishi-plugin-${name}`,
|
|
217
|
+
];
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
@@ -215,7 +222,9 @@ class Installer extends Service {
|
|
|
215
222
|
const entries = await Promise.all(
|
|
216
223
|
names.map(async (name) => {
|
|
217
224
|
try {
|
|
218
|
-
const versions = Object.entries(
|
|
225
|
+
const versions = Object.entries(
|
|
226
|
+
await this.getPackage(name),
|
|
227
|
+
);
|
|
219
228
|
const [latest] = versions;
|
|
220
229
|
if (!latest) return undefined;
|
|
221
230
|
return { [name]: latest[0] };
|
|
@@ -224,19 +233,30 @@ class Installer extends Service {
|
|
|
224
233
|
}
|
|
225
234
|
}),
|
|
226
235
|
);
|
|
227
|
-
return entries.find(
|
|
236
|
+
return entries.find(
|
|
237
|
+
(entry): entry is Dict<string> => entry !== undefined,
|
|
238
|
+
);
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
private async _getPackage(name: string) {
|
|
231
242
|
try {
|
|
232
|
-
const registry = await this.http.get<Registry>(
|
|
243
|
+
const registry = await this.http.get<Registry>(
|
|
244
|
+
`/${name}`,
|
|
245
|
+
);
|
|
233
246
|
const versions = getVersions(
|
|
234
|
-
Object.values(registry.versions).filter(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
),
|
|
238
257
|
);
|
|
239
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
258
|
+
this.fullCache[name] = this.tempCache[name] =
|
|
259
|
+
versions;
|
|
240
260
|
this.flushData();
|
|
241
261
|
return versions;
|
|
242
262
|
} catch (error) {
|
|
@@ -246,9 +266,12 @@ class Installer extends Service {
|
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
setPackage(name: string, versions: RemotePackage[]) {
|
|
249
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
269
|
+
this.fullCache[name] = this.tempCache[name] =
|
|
270
|
+
getVersions(versions);
|
|
250
271
|
this.flushData();
|
|
251
|
-
this.pkgTasks[name] = Promise.resolve(
|
|
272
|
+
this.pkgTasks[name] = Promise.resolve(
|
|
273
|
+
this.fullCache[name],
|
|
274
|
+
);
|
|
252
275
|
}
|
|
253
276
|
|
|
254
277
|
getPackage(name: string) {
|
|
@@ -256,11 +279,17 @@ class Installer extends Service {
|
|
|
256
279
|
}
|
|
257
280
|
|
|
258
281
|
private async _getDeps() {
|
|
259
|
-
const result = valueMap(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
const result = valueMap(
|
|
283
|
+
this.manifest.dependencies,
|
|
284
|
+
(request) => {
|
|
285
|
+
return {
|
|
286
|
+
request: request.replace(/^[~^]/, ""),
|
|
287
|
+
} as Dependency;
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
await mapLimit(
|
|
263
291
|
Object.keys(result),
|
|
292
|
+
10,
|
|
264
293
|
async (name) => {
|
|
265
294
|
const dep = result[name];
|
|
266
295
|
if (!dep) return;
|
|
@@ -279,7 +308,6 @@ class Installer extends Service {
|
|
|
279
308
|
const versions = await this.getPackage(name);
|
|
280
309
|
if (versions) dep.latest = Object.keys(versions)[0];
|
|
281
310
|
},
|
|
282
|
-
{ concurrency: 10 },
|
|
283
311
|
);
|
|
284
312
|
return result;
|
|
285
313
|
}
|
|
@@ -303,19 +331,18 @@ class Installer extends Service {
|
|
|
303
331
|
}
|
|
304
332
|
|
|
305
333
|
async exec(args: string[]) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
334
|
+
// Bun-first:CE 生态只存在 bun 这一种包管理器,直接驱动 bun 执行
|
|
335
|
+
// 安装(上游经 which-pm-runs 探测 npm/yarn/bun,此处固定为 bun)
|
|
336
|
+
args.unshift("install");
|
|
309
337
|
return new Promise<number>((resolve) => {
|
|
310
|
-
|
|
311
|
-
const child = spawn(name, args, { cwd: this.cwd });
|
|
338
|
+
const child = spawnBun(args, this.cwd);
|
|
312
339
|
child.on("exit", (code) => resolve(code ?? -1));
|
|
313
340
|
child.on("error", () => resolve(-1));
|
|
314
341
|
|
|
315
342
|
let stderr = "";
|
|
316
343
|
child.stderr?.on("data", (data) => {
|
|
317
|
-
|
|
318
|
-
const lines =
|
|
344
|
+
stderr += data.toString();
|
|
345
|
+
const lines = stderr.split("\n");
|
|
319
346
|
stderr = lines.pop() ?? "";
|
|
320
347
|
for (const line of lines) {
|
|
321
348
|
logger.warn(line);
|
|
@@ -324,23 +351,11 @@ class Installer extends Service {
|
|
|
324
351
|
|
|
325
352
|
let stdout = "";
|
|
326
353
|
child.stdout?.on("data", (data) => {
|
|
327
|
-
|
|
328
|
-
const lines =
|
|
354
|
+
stdout += data.toString();
|
|
355
|
+
const lines = stdout.split("\n");
|
|
329
356
|
stdout = lines.pop() ?? "";
|
|
330
357
|
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
|
-
}
|
|
358
|
+
logger.info(line);
|
|
344
359
|
}
|
|
345
360
|
});
|
|
346
361
|
});
|
|
@@ -350,26 +365,35 @@ class Installer extends Service {
|
|
|
350
365
|
const filename = resolve(this.cwd, "package.json");
|
|
351
366
|
// 现读现写:this.manifest 原为构造期的启动快照,运行期间根
|
|
352
367
|
// package.json 可能已被外部更新,基于快照整体重写会抹掉变更
|
|
353
|
-
this.manifest = JSON.parse(
|
|
368
|
+
this.manifest = JSON.parse(
|
|
369
|
+
readFileSync(filename, "utf8"),
|
|
370
|
+
) as LocalPackage;
|
|
354
371
|
this.manifest.dependencies ||= {};
|
|
355
372
|
for (const key in deps) {
|
|
356
373
|
if (deps[key]) {
|
|
357
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
isGuardedRequest(this.manifest.dependencies[key])
|
|
376
|
+
) {
|
|
358
377
|
continue;
|
|
359
378
|
}
|
|
360
379
|
this.manifest.dependencies[key] = deps[key];
|
|
361
|
-
} else if (
|
|
380
|
+
} else if (
|
|
381
|
+
!isGuardedRequest(this.manifest.dependencies[key])
|
|
382
|
+
) {
|
|
362
383
|
delete this.manifest.dependencies[key];
|
|
363
384
|
}
|
|
364
385
|
}
|
|
365
386
|
this.manifest.dependencies = Object.fromEntries(
|
|
366
|
-
Object.entries(this.manifest.dependencies).sort(
|
|
367
|
-
a[0].localeCompare(b[0]),
|
|
387
|
+
Object.entries(this.manifest.dependencies).sort(
|
|
388
|
+
(a, b) => a[0].localeCompare(b[0]),
|
|
368
389
|
),
|
|
369
390
|
);
|
|
370
391
|
// 仓库格式权威是 biome(tab 缩进),按 tab 写出避免装插件后
|
|
371
392
|
// package.json 被重排成空格、lint 报格式漂移
|
|
372
|
-
await Bun.write(
|
|
393
|
+
await Bun.write(
|
|
394
|
+
filename,
|
|
395
|
+
`${JSON.stringify(this.manifest, null, "\t")}\n`,
|
|
396
|
+
);
|
|
373
397
|
}
|
|
374
398
|
|
|
375
399
|
private _install() {
|
|
@@ -392,7 +416,10 @@ class Installer extends Service {
|
|
|
392
416
|
});
|
|
393
417
|
}
|
|
394
418
|
|
|
395
|
-
async install(
|
|
419
|
+
async install(
|
|
420
|
+
deps: Dict<string | null>,
|
|
421
|
+
forced?: boolean,
|
|
422
|
+
) {
|
|
396
423
|
const localDeps = this._getLocalDeps(deps);
|
|
397
424
|
await this.override(deps);
|
|
398
425
|
|
|
@@ -404,7 +431,9 @@ class Installer extends Service {
|
|
|
404
431
|
local?.workspace ||
|
|
405
432
|
(request &&
|
|
406
433
|
local?.resolved &&
|
|
407
|
-
satisfies(local.resolved, request, {
|
|
434
|
+
satisfies(local.resolved, request, {
|
|
435
|
+
includePrerelease: true,
|
|
436
|
+
}))
|
|
408
437
|
)
|
|
409
438
|
continue;
|
|
410
439
|
shouldInstall = true;
|
|
@@ -425,7 +454,8 @@ class Installer extends Service {
|
|
|
425
454
|
if (newDep.resolved === local.resolved) continue;
|
|
426
455
|
// 版本变化且旧版本仍驻留内存时才需要整进程重载(判定不走
|
|
427
456
|
// 解析 API,规避 Bun 负缓存,见 isResidentInCache)
|
|
428
|
-
if (isResidentInCache(name))
|
|
457
|
+
if (isResidentInCache(name))
|
|
458
|
+
this.ctx.loader.fullReload();
|
|
429
459
|
}
|
|
430
460
|
this.refreshData();
|
|
431
461
|
|