@koishi-ce/plugin-config 1.0.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PackageProvider 的 Node 端实现:基于 LocalScanner 扫描本机
3
+ * node_modules 中的插件包,并在扫描过程中顺带解析已加载插件的
4
+ * 运行时信息写入共享 cache。
5
+ */
6
+ import { Logger } from "@koishi-ce/koishi";
7
+ import { LocalScanner } from "@koishi-ce/registry";
8
+ import * as shared from "../shared/index.ts";
9
+
10
+ const logger = new Logger("config");
11
+
12
+ /**
13
+ * 本机插件扫描器:在 LocalScanner 的基础上扩展 parsePackage,
14
+ * 让"扫描到某个包"与"解析该插件的运行时信息"在同一次遍历中完成。
15
+ */
16
+ class PackageScanner extends LocalScanner {
17
+ private service: PackageProvider;
18
+
19
+ constructor(service: PackageProvider) {
20
+ super(service.ctx.baseDir);
21
+ this.service = service;
22
+ }
23
+
24
+ /** 单个包解析失败时仅记录警告,不中断整体扫描。 */
25
+ override async onError(error: unknown, name: string) {
26
+ logger.warn("failed to resolve %c", name);
27
+ logger.warn(error);
28
+ }
29
+
30
+ /**
31
+ * 解析包元数据后,若该插件的入口模块已被 require 进内存,
32
+ * 则顺带解析其导出并写入服务缓存,供前端立即取用。
33
+ */
34
+ override async parsePackage(name: string) {
35
+ const result = await super.parsePackage(name);
36
+ try {
37
+ // require.resolve(name) 的结果可能与 require.resolve(path) 不同,
38
+ // 因为 tsconfig-paths 可能以不同方式解析路径
39
+ const entry = require.resolve(name);
40
+ if (require.cache[entry]) {
41
+ name = name.replace(/(koishi-|^@koishijs\/)plugin-/, "");
42
+ this.service.cache[name] = await this.service.parseExports(name);
43
+ }
44
+ } catch (error) {
45
+ void this.onError(error, name);
46
+ }
47
+ return result;
48
+ }
49
+ }
50
+
51
+ /** Node 端包提供器:collect 委托给本机扫描器。 */
52
+ export class PackageProvider extends shared.PackageProvider {
53
+ scanner = new PackageScanner(this);
54
+
55
+ /**
56
+ * 扫描本机插件包并返回扫描结果。
57
+ *
58
+ * @param forced 为 true 时强制重新扫描(不走缓存)
59
+ */
60
+ async collect(forced: boolean) {
61
+ await this.scanner.collect(forced);
62
+ return this.scanner.objects;
63
+ }
64
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 服务端共享代码的汇总出口。
3
+ *
4
+ * 一方面把 packages / services / writer 三个模块统一 re-export 给
5
+ * node 与 browser 两个入口使用;另一方面通过声明合并把三个数据服务
6
+ * 注册进 console 的 Services 接口,使浏览器端能以类型安全的方式
7
+ * 访问 `store.packages`、`store.services`、`store.config`。
8
+ */
9
+ import type { PackageProvider } from "./packages.ts";
10
+ import type { ServiceProvider } from "./services.ts";
11
+ import type { ConfigWriter } from "./writer.ts";
12
+
13
+ declare module "@koishi-ce/console" {
14
+ namespace Console {
15
+ interface Services {
16
+ packages: PackageProvider;
17
+ services: ServiceProvider;
18
+ config: ConfigWriter;
19
+ }
20
+ }
21
+ }
22
+
23
+ export * from "./packages.ts";
24
+ export * from "./services.ts";
25
+ export * from "./writer.ts";
@@ -0,0 +1,245 @@
1
+ /**
2
+ * 插件包元数据提供器(服务端共享实现)。
3
+ *
4
+ * 以 `packages` 数据服务的形式向浏览器端提供本机可用的插件包列表及
5
+ * 各自的运行时信息(Config schema、用法文档、注入的服务依赖、fork
6
+ * 状态等)。运行时信息通过解析插件模块的导出(parseExports)获得并
7
+ * 缓存在 cache 中,监听 runtime/fork/status 等内部事件按需刷新。
8
+ *
9
+ * 抽象类只定义"运行时信息"这一半;列表如何收集(本机扫描还是市场
10
+ * 数据)分别由 node / browser 两个运行环境的子类实现。
11
+ */
12
+ import { DataService } from "@koishi-ce/console";
13
+ import {
14
+ Context,
15
+ type Dict,
16
+ Logger,
17
+ type MainScope,
18
+ type Plugin,
19
+ type Schema,
20
+ type ScopeStatus,
21
+ } from "@koishi-ce/koishi";
22
+ import type { LoaderScope } from "@koishi-ce/loader";
23
+ import type {} from "@koishi-ce/plugin-hmr";
24
+ import type {
25
+ PackageJson,
26
+ SearchObject,
27
+ SearchResult,
28
+ } from "@koishi-ce/registry";
29
+
30
+ declare module "@koishi-ce/loader" {
31
+ interface Loader {
32
+ market: SearchResult;
33
+ }
34
+ }
35
+
36
+ declare module "@koishi-ce/console" {
37
+ interface Events {
38
+ "config/request-runtime"(name: string): void;
39
+ }
40
+ }
41
+
42
+ const logger = new Logger("config");
43
+
44
+ export abstract class PackageProvider extends DataService<
45
+ Dict<PackageProvider.Data>
46
+ > {
47
+ cache: Dict<PackageProvider.RuntimeData> = {};
48
+ debouncedRefresh: () => void;
49
+
50
+ override ctx: Context;
51
+
52
+ /**
53
+ * @param ctx 应用上下文
54
+ */
55
+ constructor(ctx: Context) {
56
+ super(ctx, "packages", { authority: 4 });
57
+ this.ctx = ctx;
58
+
59
+ this.debouncedRefresh = ctx.debounce(() => this.refresh(false), 0);
60
+ // 插件的加载 / fork / 状态变化都可能影响运行时信息,统一走 update
61
+ ctx.on("internal/runtime", (scope) => this.update(scope.runtime.plugin));
62
+ ctx.on("internal/fork", (scope) => this.update(scope.runtime.plugin));
63
+ ctx.on("internal/status", (scope) => this.update(scope.runtime.plugin));
64
+ ctx.on("hmr/reload", (reloads) => {
65
+ for (const [plugin] of reloads) {
66
+ this.update(plugin);
67
+ }
68
+ });
69
+
70
+ // 前端按需请求:某插件缺少运行时信息时(如刚安装尚未解析),
71
+ // 按短名解析其导出并立即推送
72
+ ctx.console.addListener(
73
+ "config/request-runtime",
74
+ async (name) => {
75
+ name = name.replace(/(koishi-|^@koishijs\/)plugin-/, "");
76
+ this.cache[name] = await this.parseExports(name);
77
+ this.refresh(false);
78
+ },
79
+ { authority: 4 },
80
+ );
81
+ }
82
+
83
+ /**
84
+ * 收集插件包列表。由运行环境子类实现:node 端本机扫描 node_modules,
85
+ * browser 端复用 loader 缓存的市场搜索结果。
86
+ *
87
+ * @param forced 是否强制重新收集(绕过缓存)
88
+ */
89
+ abstract collect(forced: boolean): Promise<PackageProvider.Data[]>;
90
+
91
+ /**
92
+ * 某插件的运行时状态发生变化时,重新解析其导出并去抖刷新服务。
93
+ *
94
+ * @param plugin 发生变化的插件对象
95
+ */
96
+ async update(plugin: Plugin) {
97
+ const name = this.ctx.loader.keyFor(plugin);
98
+ if (!name || !this.cache[name]) return;
99
+ this.cache[name] = await this.parseExports(name);
100
+ this.debouncedRefresh();
101
+ }
102
+
103
+ /**
104
+ * 从插件主作用域提取 fork 运行状态,写入 result 的 id/forkable/forks 字段。
105
+ *
106
+ * @param state 插件的主作用域
107
+ * @param result 待填充的运行时数据(原地修改)
108
+ */
109
+ parseRuntime(state: MainScope, result: PackageProvider.RuntimeData) {
110
+ // 已销毁的 runtime(uid 为 null)不展示 id
111
+ if (state.runtime.uid !== null) {
112
+ result.id = state.runtime.uid;
113
+ }
114
+ result.forkable = state.runtime.isForkable;
115
+ // fork 的 key 由 loader 写入(cordis 的 ForkScope 类型上没有该属性)
116
+ const forks: Dict<{ status?: ScopeStatus }> = {};
117
+ for (const fork of state.children) {
118
+ const key = (fork as LoaderScope).key;
119
+ if (!key) continue;
120
+ forks[key] = { status: fork.status };
121
+ }
122
+ result.forks = forks;
123
+ }
124
+
125
+ /**
126
+ * 数据服务读取入口:收集包列表,合并缓存的运行时信息,
127
+ * 并在最前面附加一个 name 为空串的"应用全局设置"条目。
128
+ */
129
+ override async get(forced = false) {
130
+ const objects = (await this.collect(forced)).slice();
131
+ for (const object of objects) {
132
+ object.name = object.package?.name || "";
133
+ const cached = this.cache[object.shortname];
134
+ if (!cached) continue;
135
+ object.runtime = cached;
136
+ }
137
+
138
+ // 附加应用全局设置(使用 Context.Config 作为 schema,name 为空串)
139
+ objects.unshift({
140
+ name: "",
141
+ runtime: {
142
+ schema: Context.Config,
143
+ },
144
+ package: { name: "" },
145
+ // 全局设置条目不带市场元数据字段,用双断言放宽必填约束
146
+ } as unknown as PackageProvider.Data);
147
+ return Object.fromEntries(objects.map((data) => [data.name, data]));
148
+ }
149
+
150
+ /**
151
+ * 解析插件模块的导出,提取配置 schema、用法文档、过滤器开关与
152
+ * 注入的服务依赖(required / optional),并附带运行时 fork 状态。
153
+ * 解析失败时返回 `{ failed: true }` 而不是抛错。
154
+ *
155
+ * @param name 插件短名(不含 koishi-plugin- 前缀)
156
+ * @returns 可序列化的运行时数据
157
+ */
158
+ async parseExports(name: string) {
159
+ try {
160
+ // 插件本体之外的附加导出(配置面板消费的元信息)
161
+ type Extras = {
162
+ Config?: Schema;
163
+ schema?: Schema;
164
+ usage?: string;
165
+ filter?: boolean;
166
+ using?: string[] | { required?: string[]; optional?: string[] };
167
+ inject?: string[] | { required?: string[]; optional?: string[] };
168
+ };
169
+ const exports = (await this.ctx.loader.resolve(name)) as
170
+ | (Plugin & Extras)
171
+ | undefined;
172
+ const result: PackageProvider.RuntimeData = {};
173
+ const schema = exports?.Config || exports?.schema;
174
+ if (schema) result.schema = schema;
175
+ if (exports?.usage) result.usage = exports.usage;
176
+ if (exports?.filter !== undefined) result.filter = exports.filter;
177
+ // using 与 inject 是新旧两种声明注入服务的方式
178
+ const inject = exports?.using || exports?.inject || [];
179
+ if (Array.isArray(inject)) {
180
+ result.required = inject;
181
+ result.optional = [];
182
+ } else {
183
+ result.required = inject.required || [];
184
+ result.optional = inject.optional || [];
185
+ }
186
+
187
+ // 确保 result 可以被 JSON 序列化(schema 中可能混入不可序列化的值)
188
+ JSON.stringify(result);
189
+
190
+ const runtime = exports ? this.ctx.registry.get(exports) : undefined;
191
+ if (runtime) this.parseRuntime(runtime, result);
192
+ return result;
193
+ } catch (error) {
194
+ logger.warn("failed to load %c", name);
195
+ logger.warn(error);
196
+ return { failed: true };
197
+ }
198
+ }
199
+ }
200
+
201
+ /** PackageProvider 相关的数据类型定义。 */
202
+ export namespace PackageProvider {
203
+ /**
204
+ * 下发给前端的插件包条目:市场/本机元数据 + 运行时信息。
205
+ * name 为空串的条目特指应用全局设置。
206
+ */
207
+ export interface Data
208
+ extends Pick<
209
+ SearchObject,
210
+ "shortname" | "workspace" | "manifest" | "portable"
211
+ > {
212
+ /** 完整包名 */
213
+ name?: string;
214
+ /** 运行时信息(schema、依赖、fork 状态等) */
215
+ runtime?: RuntimeData;
216
+ package: Pick<
217
+ PackageJson,
218
+ "name" | "version" | "peerDependencies" | "peerDependenciesMeta"
219
+ >;
220
+ }
221
+
222
+ /** 从插件模块导出解析出的运行时数据。 */
223
+ export interface RuntimeData {
224
+ /** 插件主作用域 uid(未加载时缺失) */
225
+ id?: number;
226
+ /** 是否暴露 filter(过滤器设置) */
227
+ filter?: boolean;
228
+ /** 是否可重用(允许同时存在多份配置) */
229
+ forkable?: boolean;
230
+ /** 插件的配置 schema */
231
+ schema?: Schema;
232
+ /** 用法说明(Markdown 文档) */
233
+ usage?: string;
234
+ /** 必需注入的服务列表 */
235
+ required?: string[];
236
+ /** 可选注入的服务列表 */
237
+ optional?: string[];
238
+ /** 模块解析失败标记 */
239
+ failed?: boolean;
240
+ /** 各 fork 配置的运行状态,键为配置路径 */
241
+ forks?: Dict<{
242
+ status?: ScopeStatus;
243
+ }>;
244
+ }
245
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 服务状态提供器:以 `services` 数据服务的形式向浏览器端上报当前
3
+ * 应用中每个已注册服务的提供者信息(服务名 → 提供该服务的上下文 uid)。
4
+ * 前端据此判断某插件"必需/可选注入的服务"是否已可用。
5
+ *
6
+ * 遍历方式:从根上下文的 internal 对象出发沿原型链逐层收集类型为
7
+ * "service" 的属性,再通过 Context.current 访问器反查服务实例所属的
8
+ * 上下文,取其作用域 uid。
9
+ */
10
+ import { DataService } from "@koishi-ce/console";
11
+ import { Context, type Dict } from "@koishi-ce/koishi";
12
+
13
+ export class ServiceProvider extends DataService<Dict<number>> {
14
+ constructor(ctx: Context) {
15
+ super(ctx, "services");
16
+ // 任何服务的注册 / 注销都会改变统计结果
17
+ ctx.on("internal/service", () => this.refresh());
18
+ }
19
+
20
+ override async get() {
21
+ const services = {} as Dict<number>;
22
+ /**
23
+ * 递归遍历 internal 对象的原型链,收集所有服务属性。
24
+ *
25
+ * @param internal 当前层级的 internal 对象
26
+ */
27
+ const attach = (internal: Context[typeof Context.internal]) => {
28
+ if (!internal) return;
29
+ attach(Object.getPrototypeOf(internal));
30
+ for (const [key, { type }] of Object.entries(internal)) {
31
+ if (type !== "service") continue;
32
+ const instance = this.ctx.get(key);
33
+ if (!(instance instanceof Object)) continue;
34
+ // Context.current 是个访问器属性,其值即服务实例所属的上下文
35
+ const ctx: Context = Reflect.getOwnPropertyDescriptor(
36
+ instance,
37
+ Context.current,
38
+ )?.value;
39
+ if (!ctx) continue;
40
+ // 服务内部名形如 __foo__,对外展示时去掉首尾下划线
41
+ const name = key.replace(/^__/, "").replace(/__$/, "");
42
+ // 已销毁的 scope(uid 为 null)不参与统计
43
+ if (ctx.scope.uid !== null) {
44
+ services[name] = ctx.scope.uid;
45
+ }
46
+ }
47
+ };
48
+ attach(this.ctx.root[Context.internal]);
49
+ return services;
50
+ }
51
+ }