@koishi-ce/plugin-config 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.
@@ -12,7 +12,10 @@
12
12
  * client 源码(供开发调试用)。
13
13
  */
14
14
  import { type Context, Schema } from "@koishi-ce/koishi";
15
- import { ConfigWriter, ServiceProvider } from "../shared/index.ts";
15
+ import {
16
+ ConfigWriter,
17
+ ServiceProvider,
18
+ } from "../shared/index.ts";
16
19
  import { PackageProvider } from "./packages.ts";
17
20
 
18
21
  export * from "../shared/index.ts";
@@ -35,6 +38,11 @@ export function apply(ctx: Context) {
35
38
  `${process.env["KOISHI_BASE"]}/dist/index.js`,
36
39
  `${process.env["KOISHI_BASE"]}/dist/style.css`,
37
40
  ]
38
- : [import.meta.url.replace(/\/src\/[^/]+\/[^/]+$/, "/client/index.ts")],
41
+ : [
42
+ import.meta.url.replace(
43
+ /\/src\/[^/]+\/[^/]+$/,
44
+ "/client/index.ts",
45
+ ),
46
+ ],
39
47
  );
40
48
  }
package/src/node/index.ts CHANGED
@@ -12,13 +12,21 @@
12
12
 
13
13
  import { resolve } from "node:path";
14
14
  import { type Context, Schema } from "@koishi-ce/koishi";
15
- import { ConfigWriter, ServiceProvider } from "../shared/index.ts";
15
+ import {
16
+ ConfigWriter,
17
+ ServiceProvider,
18
+ } from "../shared/index.ts";
16
19
  import { PackageProvider } from "./packages.ts";
17
20
 
18
21
  export * from "../shared/index.ts";
19
22
 
20
23
  export const name = "config";
21
- export const inject = ["console"];
24
+ // loader 由宿主以 builtin 服务提供,生产恒存在;测试等裸 App 环境可缺席,
25
+ // 声明为非必需注入以免 cordis 报「property loader is not registered」
26
+ export const inject = {
27
+ console: { required: true },
28
+ loader: { required: false },
29
+ };
22
30
 
23
31
  export type Config = Record<never, never>;
24
32
 
@@ -90,13 +90,21 @@ export class PackageProvider extends shared.PackageProvider {
90
90
  shared.PackageProvider.Data[]
91
91
  > {
92
92
  // 以完整包名为键索引全部条目(浅拷贝,避免跨 forced 扫描累积 paths)
93
- const index = new Map<string, shared.PackageProvider.Data>();
93
+ const index = new Map<
94
+ string,
95
+ shared.PackageProvider.Data
96
+ >();
94
97
  for (const object of this.scanner.objects) {
95
- index.set(object.package.name, { ...object, paths: [] });
98
+ index.set(object.package.name, {
99
+ ...object,
100
+ paths: [],
101
+ });
96
102
  }
97
103
  try {
98
104
  await walkPlugins(
99
- this.ctx.loader?.config?.plugins as Record<string, unknown> | undefined,
105
+ this.ctx.loader?.config?.plugins as
106
+ | Record<string, unknown>
107
+ | undefined,
100
108
  async (key) => {
101
109
  // 配置键形如 ./plugins/webui/config:uid,取 : 前的路径部分
102
110
  const path = key.split(":", 1)[0];
@@ -106,14 +114,19 @@ export class PackageProvider extends shared.PackageProvider {
106
114
  );
107
115
  if (!object) return;
108
116
  const name = object.package.name;
109
- const entry = index.get(name) ?? { ...object, paths: [] };
117
+ const entry = index.get(name) ?? {
118
+ ...object,
119
+ paths: [],
120
+ };
110
121
  entry.paths ||= [];
111
- if (!entry.paths.includes(path)) entry.paths.push(path);
122
+ if (!entry.paths.includes(path))
123
+ entry.paths.push(path);
112
124
  index.set(name, entry);
113
125
  // 登记「包名 → 配置键」反查,供 request-runtime 按路径解析
114
126
  this.pathKeys[name] ||= path;
115
127
  // 顺带解析运行时信息(loader.resolve 对路径键原生支持)
116
- this.cache[path] ||= await this.parseExports(path);
128
+ this.cache[path] ||=
129
+ await this.parseExports(path);
117
130
  },
118
131
  );
119
132
  } catch (error) {
@@ -136,7 +149,9 @@ async function walkPlugins(
136
149
  if (key.split(":", 1)[0] === "group") {
137
150
  const value = plugins?.[key];
138
151
  await walkPlugins(
139
- (value ?? {}) as Record<string, unknown> | undefined,
152
+ (value ?? {}) as
153
+ | Record<string, unknown>
154
+ | undefined,
140
155
  handler,
141
156
  );
142
157
  } else {
@@ -50,6 +50,8 @@ export abstract class PackageProvider extends DataService<
50
50
  Dict<PackageProvider.Data>
51
51
  > {
52
52
  cache: Dict<PackageProvider.RuntimeData> = {};
53
+ /** 进行中的解析(键 → 共享 promise),同键并发请求合并为一次模块加载 */
54
+ inflight: Dict<Promise<PackageProvider.RuntimeData>> = {};
53
55
  debouncedRefresh: () => void;
54
56
 
55
57
  /** 完整包名 → 引用它的首个配置键(workspace 相对路径),由 node 端 collect 填充 */
@@ -64,11 +66,20 @@ export abstract class PackageProvider extends DataService<
64
66
  super(ctx, "packages", { authority: 4 });
65
67
  this.ctx = ctx;
66
68
 
67
- this.debouncedRefresh = ctx.debounce(() => this.refresh(false), 0);
69
+ this.debouncedRefresh = ctx.debounce(
70
+ () => this.refresh(false),
71
+ 0,
72
+ );
68
73
  // 插件的加载 / fork / 状态变化都可能影响运行时信息,统一走 update
69
- ctx.on("internal/runtime", (scope) => this.update(scope.runtime.plugin));
70
- ctx.on("internal/fork", (scope) => this.update(scope.runtime.plugin));
71
- ctx.on("internal/status", (scope) => this.update(scope.runtime.plugin));
74
+ ctx.on("internal/runtime", (scope) =>
75
+ this.update(scope.runtime.plugin),
76
+ );
77
+ ctx.on("internal/fork", (scope) =>
78
+ this.update(scope.runtime.plugin),
79
+ );
80
+ ctx.on("internal/status", (scope) =>
81
+ this.update(scope.runtime.plugin),
82
+ );
72
83
  ctx.on("hmr/reload", (reloads) => {
73
84
  for (const [plugin] of reloads) {
74
85
  this.update(plugin);
@@ -76,25 +87,27 @@ export abstract class PackageProvider extends DataService<
76
87
  });
77
88
 
78
89
  // 前端按需请求:某插件缺少运行时信息时(如刚安装尚未解析),
79
- // 按其配置键(workspace 相对路径)或短名解析其导出并立即推送
90
+ // 按其配置键(workspace 相对路径)或短名解析其导出并立即推送。
91
+ // 解析失败同样写入缓存(failed 标记随数据下发):前端展示失败
92
+ // 提示后不再重发请求——否则「请求 → 失败 → 广播 → 前端再请求」
93
+ // 互相喂养成活锁,加载即崩的插件会把日志瞬间打爆
80
94
  ctx.console.addListener(
81
95
  "config/request-runtime",
82
96
  async (name) => {
83
- // 依次尝试:路径配置键 → 短名 → 原名,首个解析成功的为准
97
+ // 依次尝试:路径配置键 → 短名 → 原名,首个解析成功的为准;
98
+ // 已有结果(含失败标记)的键直接复用,不重新解析
84
99
  const candidates = [
85
100
  this.pathKeys[name],
86
101
  getPluginShortname(name),
87
102
  name,
88
103
  ];
89
- for (const key of candidates) {
104
+ for (const key of new Set(candidates)) {
90
105
  if (!key) continue;
91
- const result = await this.parseExports(key);
92
- if (!result.failed) {
93
- this.cache[key] = result;
94
- break;
95
- }
106
+ const result = (this.cache[key] ??=
107
+ await this.parseExports(key));
108
+ if (!result.failed) break;
96
109
  }
97
- this.refresh(false);
110
+ this.debouncedRefresh();
98
111
  },
99
112
  { authority: 4 },
100
113
  );
@@ -106,7 +119,9 @@ export abstract class PackageProvider extends DataService<
106
119
  *
107
120
  * @param forced 是否强制重新收集(绕过缓存)
108
121
  */
109
- abstract collect(forced: boolean): Promise<PackageProvider.Data[]>;
122
+ abstract collect(
123
+ forced: boolean,
124
+ ): Promise<PackageProvider.Data[]>;
110
125
 
111
126
  /**
112
127
  * 某插件的运行时状态发生变化时,重新解析其导出并去抖刷新服务。
@@ -126,7 +141,10 @@ export abstract class PackageProvider extends DataService<
126
141
  * @param state 插件的主作用域
127
142
  * @param result 待填充的运行时数据(原地修改)
128
143
  */
129
- parseRuntime(state: MainScope, result: PackageProvider.RuntimeData) {
144
+ parseRuntime(
145
+ state: MainScope,
146
+ result: PackageProvider.RuntimeData,
147
+ ) {
130
148
  // 已销毁的 runtime(uid 为 null)不展示 id
131
149
  if (state.runtime.uid !== null) {
132
150
  result.id = state.runtime.uid;
@@ -152,7 +170,10 @@ export abstract class PackageProvider extends DataService<
152
170
  object.name = object.package?.name || "";
153
171
  // 运行时缓存的键与写入方一致:workspace 包用配置键(./...),
154
172
  // npm 包用短名;paths 为空时退回短名查找
155
- const cached = [...(object.paths ?? []), object.shortname]
173
+ const cached = [
174
+ ...(object.paths ?? []),
175
+ object.shortname,
176
+ ]
156
177
  .map((key) => this.cache[key])
157
178
  .find(Boolean);
158
179
  if (!cached) continue;
@@ -168,18 +189,32 @@ export abstract class PackageProvider extends DataService<
168
189
  package: { name: "" },
169
190
  // 全局设置条目不带市场元数据字段,用双断言放宽必填约束
170
191
  } as unknown as PackageProvider.Data);
171
- return Object.fromEntries(objects.map((data) => [data.name, data]));
192
+ return Object.fromEntries(
193
+ objects.map((data) => [data.name, data]),
194
+ );
172
195
  }
173
196
 
174
197
  /**
175
198
  * 解析插件模块的导出,提取配置 schema、用法文档、过滤器开关与
176
199
  * 注入的服务依赖(required / optional),并附带运行时 fork 状态。
177
- * 解析失败时返回 `{ failed: true }` 而不是抛错。
200
+ * 解析失败时返回 `{ failed: true }` 而不是抛错。同键并发调用共享
201
+ * 一次模块加载(抛错的模块不进 require.cache,每次都重新求值,
202
+ * 并发即多份重复求值与告警)。
178
203
  *
179
204
  * @param name 插件短名(不含 koishi-plugin- 前缀)
180
205
  * @returns 可序列化的运行时数据
181
206
  */
182
- async parseExports(name: string) {
207
+ parseExports(
208
+ name: string,
209
+ ): Promise<PackageProvider.RuntimeData> {
210
+ const pending = (this.inflight[name] ??=
211
+ this.resolveExports(name).finally(() => {
212
+ delete this.inflight[name];
213
+ }));
214
+ return pending;
215
+ }
216
+
217
+ private async resolveExports(name: string) {
183
218
  try {
184
219
  // 插件本体之外的附加导出(配置面板消费的元信息)
185
220
  type Extras = {
@@ -187,19 +222,25 @@ export abstract class PackageProvider extends DataService<
187
222
  schema?: Schema;
188
223
  usage?: string;
189
224
  filter?: boolean;
190
- using?: string[] | { required?: string[]; optional?: string[] };
191
- inject?: string[] | { required?: string[]; optional?: string[] };
225
+ using?:
226
+ | string[]
227
+ | { required?: string[]; optional?: string[] };
228
+ inject?:
229
+ | string[]
230
+ | { required?: string[]; optional?: string[] };
192
231
  };
193
- const exports = (await this.ctx.loader.resolve(name)) as
194
- | (Plugin & Extras)
195
- | undefined;
232
+ const exports = (await this.ctx.loader.resolve(
233
+ name,
234
+ )) as (Plugin & Extras) | undefined;
196
235
  const result: PackageProvider.RuntimeData = {};
197
236
  const schema = exports?.Config || exports?.schema;
198
237
  if (schema) result.schema = schema;
199
238
  if (exports?.usage) result.usage = exports.usage;
200
- if (exports?.filter !== undefined) result.filter = exports.filter;
239
+ if (exports?.filter !== undefined)
240
+ result.filter = exports.filter;
201
241
  // using 与 inject 是新旧两种声明注入服务的方式
202
- const inject = exports?.using || exports?.inject || [];
242
+ const inject =
243
+ exports?.using || exports?.inject || [];
203
244
  if (Array.isArray(inject)) {
204
245
  result.required = inject;
205
246
  result.optional = [];
@@ -211,7 +252,9 @@ export abstract class PackageProvider extends DataService<
211
252
  // 确保 result 可以被 JSON 序列化(schema 中可能混入不可序列化的值)
212
253
  JSON.stringify(result);
213
254
 
214
- const runtime = exports ? this.ctx.registry.get(exports) : undefined;
255
+ const runtime = exports
256
+ ? this.ctx.registry.get(exports)
257
+ : undefined;
215
258
  if (runtime) this.parseRuntime(runtime, result);
216
259
  return result;
217
260
  } catch (error) {
@@ -241,7 +284,10 @@ export namespace PackageProvider {
241
284
  runtime?: RuntimeData;
242
285
  package: Pick<
243
286
  PackageJson,
244
- "name" | "version" | "peerDependencies" | "peerDependenciesMeta"
287
+ | "name"
288
+ | "version"
289
+ | "peerDependencies"
290
+ | "peerDependenciesMeta"
245
291
  >;
246
292
  }
247
293
 
@@ -14,7 +14,9 @@
14
14
  import { DataService } from "@koishi-ce/console";
15
15
  import { Context, type Dict } from "@koishi-ce/koishi";
16
16
 
17
- export class ServiceProvider extends DataService<Dict<number>> {
17
+ export class ServiceProvider extends DataService<
18
+ Dict<number>
19
+ > {
18
20
  constructor(ctx: Context) {
19
21
  super(ctx, "services");
20
22
  // 任何服务的注册 / 注销都会改变统计结果
@@ -28,21 +30,28 @@ export class ServiceProvider extends DataService<Dict<number>> {
28
30
  *
29
31
  * @param internal 当前层级的 internal 对象
30
32
  */
31
- const attach = (internal: Context[typeof Context.internal]) => {
33
+ const attach = (
34
+ internal: Context[typeof Context.internal],
35
+ ) => {
32
36
  if (!internal) return;
33
37
  attach(Object.getPrototypeOf(internal));
34
- for (const [key, { type }] of Object.entries(internal)) {
38
+ for (const [key, { type }] of Object.entries(
39
+ internal,
40
+ )) {
35
41
  if (type !== "service") continue;
36
42
  const instance = this.ctx.get(key);
37
43
  if (!(instance instanceof Object)) continue;
38
44
  // Context.current 是个访问器属性,其值即服务实例所属的上下文
39
- const ctx: Context = Reflect.getOwnPropertyDescriptor(
40
- instance,
41
- Context.current,
42
- )?.value;
45
+ const ctx: Context =
46
+ Reflect.getOwnPropertyDescriptor(
47
+ instance,
48
+ Context.current,
49
+ )?.value;
43
50
  if (!ctx) continue;
44
51
  // 服务内部名形如 __foo__,对外展示时去掉首尾下划线
45
- const name = key.replace(/^__/, "").replace(/__$/, "");
52
+ const name = key
53
+ .replace(/^__/, "")
54
+ .replace(/__$/, "");
46
55
  // 已销毁的 scope(uid 为 null)不参与统计
47
56
  if (ctx.scope.uid !== null) {
48
57
  services[name] = ctx.scope.uid;
@@ -15,8 +15,15 @@
15
15
  * 前提下增删改键"这一目标实现。
16
16
  */
17
17
  import { DataService } from "@koishi-ce/console";
18
- import { type Context, Logger, remove } from "@koishi-ce/koishi";
19
- import { Loader, type LoaderScope } from "@koishi-ce/loader";
18
+ import {
19
+ type Context,
20
+ Logger,
21
+ remove,
22
+ } from "@koishi-ce/koishi";
23
+ import {
24
+ Loader,
25
+ type LoaderScope,
26
+ } from "@koishi-ce/loader";
20
27
 
21
28
  // 声明合并:浏览器端通过 WebSocket 发送的 manager/* 事件及其载荷类型。
22
29
  // 服务端在构造函数中为每个事件注册监听器,见下方 ConfigWriter 构造函数。
@@ -29,7 +36,11 @@ declare module "@koishi-ce/console" {
29
36
  target: string,
30
37
  index: number,
31
38
  ): void;
32
- "manager/reload"(parent: string, key: string, config: unknown): void;
39
+ "manager/reload"(
40
+ parent: string,
41
+ key: string,
42
+ config: unknown,
43
+ ): void;
33
44
  "manager/unload"(
34
45
  parent: string,
35
46
  key: string,
@@ -80,7 +91,9 @@ function rename(
80
91
  value: unknown,
81
92
  ) {
82
93
  const keys = Object.keys(object);
83
- const index = keys.findIndex((key) => key === old || key === `~${old}`);
94
+ const index = keys.findIndex(
95
+ (key) => key === old || key === `~${old}`,
96
+ );
84
97
  const rest = index < 0 ? [] : keys.slice(index + 1);
85
98
  const temp = { [neo]: value };
86
99
  delete object[old];
@@ -95,7 +108,10 @@ function rename(
95
108
  * @param plugins 插件配置对象
96
109
  * @param name 插件键名
97
110
  */
98
- function dropKey(plugins: Record<string, unknown>, name: string) {
111
+ function dropKey(
112
+ plugins: Record<string, unknown>,
113
+ name: string,
114
+ ) {
99
115
  if (!(name in plugins)) {
100
116
  name = `~${name}`;
101
117
  }
@@ -139,7 +155,9 @@ export class ConfigWriter extends DataService<Context.Config> {
139
155
  async (...args: unknown[]) => {
140
156
  try {
141
157
  // 五个方法签名各异,统一视为泛化调用目标后 apply
142
- const action = this[key] as (...args: unknown[]) => Promise<void>;
158
+ const action = this[key] as (
159
+ ...args: unknown[]
160
+ ) => Promise<void>;
143
161
  await action.apply(this, args);
144
162
  } catch (error) {
145
163
  logger.error(error);
@@ -164,13 +182,21 @@ export class ConfigWriter extends DataService<Context.Config> {
164
182
  * @param plugins 某层上下文的 plugins 配置对象
165
183
  * @param ctx 该层对应的运行时上下文(用于查找 fork 记录)
166
184
  */
167
- getGroup(plugins: Record<string, unknown> | undefined, ctx: Context) {
185
+ getGroup(
186
+ plugins: Record<string, unknown> | undefined,
187
+ ctx: Context,
188
+ ) {
168
189
  const result = { ...plugins };
169
190
  for (const key in plugins ?? {}) {
170
191
  if (key.startsWith("$")) continue;
171
192
  // 配置值本质是任意 JSON 对象(插件配置或嵌套分组),此处按字典收窄使用
172
- const value = plugins?.[key] as Record<string, unknown> | undefined;
173
- const name = (key.split(":", 1)[0] ?? "").replace(/^~/, "");
193
+ const value = plugins?.[key] as
194
+ | Record<string, unknown>
195
+ | undefined;
196
+ const name = (key.split(":", 1)[0] ?? "").replace(
197
+ /^~/,
198
+ "",
199
+ );
174
200
 
175
201
  if (!this.loader.isTruthyLike(value?.["$if"])) {
176
202
  // $if-disabled 的插件不应显示在配置树中
@@ -180,7 +206,9 @@ export class ConfigWriter extends DataService<Context.Config> {
180
206
  }
181
207
 
182
208
  // 处理插件分组:分组本身是个子上下文,需要递归
183
- const fork = (ctx.scope as LoaderScope)[Loader.kRecord]?.[key];
209
+ const fork = (ctx.scope as LoaderScope)[
210
+ Loader.kRecord
211
+ ]?.[key];
184
212
  if (!fork) continue;
185
213
  if (name === "group") {
186
214
  result[key] = this.getGroup(value, fork.ctx);
@@ -191,8 +219,13 @@ export class ConfigWriter extends DataService<Context.Config> {
191
219
 
192
220
  /** 数据服务读取入口:返回去除敏感内部键后的整份应用配置。 */
193
221
  override async get() {
194
- const result: Context.Config = { ...this.loader.config };
195
- result.plugins = this.getGroup(result.plugins, this.loader.entry);
222
+ const result: Context.Config = {
223
+ ...this.loader.config,
224
+ };
225
+ result.plugins = this.getGroup(
226
+ result.plugins,
227
+ this.loader.entry,
228
+ );
196
229
  return result;
197
230
  }
198
231
 
@@ -227,7 +260,8 @@ export class ConfigWriter extends DataService<Context.Config> {
227
260
  for (const main of this.ctx.registry.values()) {
228
261
  for (const fork of main.children) {
229
262
  // fork 的 key 由 loader 写入(ForkScope 类型上没有该属性)
230
- if ((fork as LoaderScope).key === ident) return fork;
263
+ if ((fork as LoaderScope).key === ident)
264
+ return fork;
231
265
  }
232
266
  }
233
267
  return;
@@ -243,17 +277,21 @@ export class ConfigWriter extends DataService<Context.Config> {
243
277
  */
244
278
  private resolveConfig(
245
279
  ident: string,
246
- config: Record<string, unknown> | undefined = this.loader.config.plugins,
280
+ config: Record<string, unknown> | undefined = this
281
+ .loader.config.plugins,
247
282
  ): [Record<string, unknown>, string] {
248
283
  if (config) {
249
284
  for (const key in config) {
250
285
  const name = key.split(":", 1)[0] ?? "";
251
- if (key.slice(name.length + 1) === ident) return [config, key];
286
+ if (key.slice(name.length + 1) === ident)
287
+ return [config, key];
252
288
  if (name === "group" || name === "~group") {
253
289
  try {
254
290
  return this.resolveConfig(
255
291
  ident,
256
- config[key] as Record<string, unknown> | undefined,
292
+ config[key] as
293
+ | Record<string, unknown>
294
+ | undefined,
257
295
  );
258
296
  } catch {}
259
297
  }
@@ -271,7 +309,10 @@ export class ConfigWriter extends DataService<Context.Config> {
271
309
  * @param ident 插件路径标识
272
310
  * @param config 待合并的元数据键值对
273
311
  */
274
- async meta(ident: string, config: Record<string, unknown>) {
312
+ async meta(
313
+ ident: string,
314
+ config: Record<string, unknown>,
315
+ ) {
275
316
  const [parent, key] = this.resolveConfig(ident);
276
317
  const target = parent[key] as Record<string, unknown>;
277
318
  for (const key of Object.keys(config)) {
@@ -291,7 +332,11 @@ export class ConfigWriter extends DataService<Context.Config> {
291
332
  * @param key 插件键名(形如 `name:ident`)
292
333
  * @param config 新的插件配置
293
334
  */
294
- async reload(parent: string, key: string, config: unknown) {
335
+ async reload(
336
+ parent: string,
337
+ key: string,
338
+ config: unknown,
339
+ ) {
295
340
  const scope = this.resolveFork(parent);
296
341
  if (!scope) throw new Error("plugin not found");
297
342
  await this.loader.reload(scope.ctx, key, config);
@@ -320,7 +365,11 @@ export class ConfigWriter extends DataService<Context.Config> {
320
365
  this.loader.unload(scope.ctx, key);
321
366
  if (index) {
322
367
  const rest = Object.keys(scope.config).slice(index);
323
- insertKey(scope.config, { [`~${key}`]: config }, rest);
368
+ insertKey(
369
+ scope.config,
370
+ { [`~${key}`]: config },
371
+ rest,
372
+ );
324
373
  } else {
325
374
  rename(scope.config, key, `~${key}`, config);
326
375
  }
@@ -356,19 +405,29 @@ export class ConfigWriter extends DataService<Context.Config> {
356
405
  * @param target 目标分组的路径标识
357
406
  * @param index 目标插入位置(目标分组键序列中的序号)
358
407
  */
359
- async teleport(source: string, key: string, target: string, index: number) {
408
+ async teleport(
409
+ source: string,
410
+ key: string,
411
+ target: string,
412
+ index: number,
413
+ ) {
360
414
  const parentS = this.resolveFork(source);
361
415
  const parentT = this.resolveFork(target);
362
- if (!parentS || !parentT) throw new Error("plugin not found");
416
+ if (!parentS || !parentT)
417
+ throw new Error("plugin not found");
363
418
 
364
419
  // 第一步:迁移运行时 fork(仅在跨分组时需要)
365
- const fork = (parentS as LoaderScope)[Loader.kRecord]?.[key];
420
+ const fork = (parentS as LoaderScope)[Loader.kRecord]?.[
421
+ key
422
+ ];
366
423
  if (fork && parentS !== parentT) {
367
- const sourceRecord = ((parentS as LoaderScope)[Loader.kRecord] ??=
368
- Object.create(null));
424
+ const sourceRecord = ((parentS as LoaderScope)[
425
+ Loader.kRecord
426
+ ] ??= Object.create(null));
369
427
  delete sourceRecord[key];
370
- const targetRecord = ((parentT as LoaderScope)[Loader.kRecord] ??=
371
- Object.create(null));
428
+ const targetRecord = ((parentT as LoaderScope)[
429
+ Loader.kRecord
430
+ ] ??= Object.create(null));
372
431
  targetRecord[key] = fork;
373
432
  remove(parentS.disposables, fork.dispose);
374
433
  parentT.disposables.push(fork.dispose);
@@ -378,7 +437,9 @@ export class ConfigWriter extends DataService<Context.Config> {
378
437
  // scope 实例是转发到 config 的 Proxy,用 Reflect.get 保持原读取语义
379
438
  if (
380
439
  Object.keys(fork.runtime.inject).some(
381
- (name) => Reflect.get(parentS, name) !== Reflect.get(parentT, name),
440
+ (name) =>
441
+ Reflect.get(parentS, name) !==
442
+ Reflect.get(parentT, name),
382
443
  )
383
444
  ) {
384
445
  fork.restart();