@koishi-ce/plugin-market 1.1.4 → 1.3.0

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.
@@ -33,11 +33,14 @@ import {
33
33
  type RemotePackage,
34
34
  } from "@koishi-ce/registry";
35
35
  import { satisfies, valid } from "semver";
36
+ import type { Dependency } from "../dependencies/types.ts";
36
37
  import { runBun } from "./exec.ts";
37
38
  import { findMissingDeps } from "./integrity.ts";
38
39
  import {
40
+ backupManifest,
39
41
  type LocalPackage,
40
42
  loadManifest,
43
+ restoreManifest,
41
44
  writeManifest,
42
45
  } from "./manifest.ts";
43
46
  import {
@@ -52,25 +55,6 @@ import {
52
55
 
53
56
  const logger = new Logger("market");
54
57
 
55
- export interface Dependency {
56
- /**
57
- * requested semver range
58
- * @example `^1.2.3` -> `1.2.3`
59
- */
60
- request: string;
61
- /**
62
- * installed package version
63
- * @example `1.2.5`
64
- */
65
- resolved?: string | undefined;
66
- /** whether it is a workspace package */
67
- workspace?: boolean | undefined;
68
- /** valid (unsupported) syntax */
69
- invalid?: boolean | undefined;
70
- /** latest version */
71
- latest?: string | undefined;
72
- }
73
-
74
58
  class Installer extends Service {
75
59
  declare http: HTTP;
76
60
  declare endpoint: string | undefined;
@@ -78,8 +62,12 @@ class Installer extends Service {
78
62
  public tempCache: Dict<VersionMap> = {};
79
63
 
80
64
  private pkgTasks: Dict<Promise<VersionMap>> = {};
65
+ /** 各包最近一次拉取失败的归类(404 与否);依赖页的 error 徽标消费 */
66
+ public pkgErrors: Dict<{ notFound: boolean }> = {};
81
67
  private manifest: LocalPackage;
82
- declare private depTask: Promise<Dict<Dependency>>;
68
+ declare private depTask:
69
+ | Promise<Dict<Dependency>>
70
+ | undefined;
83
71
  private flushData: () => void;
84
72
 
85
73
  override config: Installer.Config;
@@ -149,11 +137,23 @@ class Installer extends Service {
149
137
  private async _getPackage(name: string) {
150
138
  try {
151
139
  const versions = await fetchVersions(this.http, name);
140
+ delete this.pkgErrors[name];
152
141
  this.fullCache[name] = this.tempCache[name] =
153
142
  versions;
154
143
  this.flushData();
155
144
  return versions;
156
145
  } catch (error) {
146
+ // 404 与否决定负缓存策略:404 是 registry 的确定性答复,
147
+ // resolved 空表照常驻留 pkgTasks(会话内不再重试);其余
148
+ // (网络抖动、超时等瞬态)清除任务让下次调用重试
149
+ const notFound =
150
+ this.http.isError(error) &&
151
+ error.response?.status === 404;
152
+ this.pkgErrors[name] = { notFound };
153
+ if (!notFound) {
154
+ // 本任务自身 catch 后恒 resolve,直接移除即可让下次调用重试
155
+ delete this.pkgTasks[name];
156
+ }
157
157
  logger.warn(error);
158
158
  return {};
159
159
  }
@@ -217,6 +217,7 @@ class Installer extends Service {
217
217
 
218
218
  refresh(refresh = false) {
219
219
  this.pkgTasks = {};
220
+ this.pkgErrors = {};
220
221
  this.fullCache = {};
221
222
  this.tempCache = {};
222
223
  this.depTask = this._getDeps();
@@ -224,6 +225,12 @@ class Installer extends Service {
224
225
  this.refreshData();
225
226
  }
226
227
 
228
+ /** 轻量重建:重读宿主清单并重置依赖汇总任务(registry 缓存不动)。 */
229
+ reload() {
230
+ this.manifest = loadManifest(this.cwd);
231
+ this.depTask = undefined;
232
+ }
233
+
227
234
  async exec(args: string[]) {
228
235
  // Bun-first:CE 生态只存在 bun 这一种包管理器,直接驱动 bun 执行
229
236
  // 安装(上游经 which-pm-runs 探测 npm/yarn/bun,此处固定为 bun)
@@ -260,6 +267,8 @@ class Installer extends Service {
260
267
  forced?: boolean,
261
268
  ) {
262
269
  const localDeps = this._getLocalDeps(deps);
270
+ // 备份清单原文:主安装失败时整体还原,防止半改状态残留在磁盘
271
+ const backup = await backupManifest(this.cwd);
263
272
  await this.override(deps);
264
273
 
265
274
  let shouldInstall = forced === true;
@@ -281,7 +290,14 @@ class Installer extends Service {
281
290
 
282
291
  if (shouldInstall) {
283
292
  const code = await this._install();
284
- if (code) return code;
293
+ if (code) {
294
+ // 安装失败:还原清单原文并轻量重建内存态
295
+ // (registry 缓存与依赖服务快照不受影响,后者由
296
+ // 安装监听器统一 refresh 触发重建)
297
+ await restoreManifest(this.cwd, backup);
298
+ this.reload();
299
+ return code;
300
+ }
285
301
  // isolated 布局的增量安装可能漏建新包目录的依赖链接(机理
286
302
  // 见 integrity.ts 模块注释):装完即校验,缺失时补跑一次
287
303
  // 安装——bun 按磁盘实际状态重建链接,秒级自愈,无需人工
@@ -20,7 +20,7 @@ import {
20
20
  startApp,
21
21
  stopApp,
22
22
  tmp,
23
- } from "./helpers.ts";
23
+ } from "../__tests__/helpers.ts";
24
24
 
25
25
  const { isResidentInCache } = await import(
26
26
  "@koishi-ce/registry"
@@ -90,6 +90,33 @@ describe("Installer 安装链路", () => {
90
90
  15000,
91
91
  );
92
92
 
93
+ itQuiet(
94
+ "安装失败时还原 package.json 备份(失败回滚链)",
95
+ async () => {
96
+ setNextExitCode(1);
97
+ const before = await Bun.file(
98
+ join(tmp, "package.json"),
99
+ ).text();
100
+ const code = await app.installer.install(
101
+ { "koishi-plugin-demo": "^2.0.0" },
102
+ true,
103
+ );
104
+ expect(code).toBe(1);
105
+ // 失败后清单原文原样还原(依赖改动不留半改状态)
106
+ const after = await Bun.file(
107
+ join(tmp, "package.json"),
108
+ ).text();
109
+ expect(after).toBe(before);
110
+ // 内存清单同步重建:还原后的清单仍是 1.0.0 请求
111
+ const deps = await app.installer.getDeps();
112
+ expect(deps["koishi-plugin-demo"]?.request).toBe(
113
+ "1.0.0",
114
+ );
115
+ setNextExitCode(0);
116
+ },
117
+ 15000,
118
+ );
119
+
93
120
  itQuiet("子进程 spawn 失败返回 -1", async () => {
94
121
  setNextSpawnError(true);
95
122
  setNextExitCode(0);
@@ -62,6 +62,21 @@ export function loadManifest(name: string): LocalPackage {
62
62
  return meta;
63
63
  }
64
64
 
65
+ /** 读取项目根 package.json 原文(安装前的备份快照)。 */
66
+ export async function backupManifest(
67
+ cwd: string,
68
+ ): Promise<string> {
69
+ return Bun.file(resolve(cwd, "package.json")).text();
70
+ }
71
+
72
+ /** 把备份原文原样写回项目根 package.json(安装失败的回滚路径)。 */
73
+ export async function restoreManifest(
74
+ cwd: string,
75
+ backup: string,
76
+ ): Promise<void> {
77
+ await Bun.write(resolve(cwd, "package.json"), backup);
78
+ }
79
+
65
80
  /**
66
81
  * 把依赖增删写回项目根 package.json,返回写回后的清单。
67
82
  *
File without changes