@chidchanun/bcp 0.2.13 → 0.2.15
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/README.md +224 -99
- package/docs/README.md +40 -35
- package/docs/api-manifest.json +26 -10
- package/docs/api-reference.md +173 -29
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +33 -4
- package/docs/plugin-module-platform.md +391 -0
- package/docs/releases/0.2.14.md +241 -0
- package/docs/releases/0.2.15.md +118 -0
- package/docs/testing-platform.md +602 -0
- package/package.json +11 -1
- package/packages/bundler/src/client-boundary.ts +2 -0
- package/packages/client/src/plugins.mjs +811 -0
- package/packages/client/src/plugins.ts +26 -0
- package/packages/client/src/realtime.mjs +67 -30
- package/packages/client/src/testing.mjs +2357 -0
- package/packages/client/src/testing.ts +52 -0
- package/packages/server/src/plugins.ts +1225 -0
- package/packages/server/src/realtime.ts +95 -41
- package/packages/server/src/testing-page.ts +274 -0
- package/packages/server/src/testing.ts +1884 -0
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
export type PluginServiceKey<T = unknown> =
|
|
2
|
+
string | symbol;
|
|
3
|
+
|
|
4
|
+
export type PluginState =
|
|
5
|
+
| "registered"
|
|
6
|
+
| "setting-up"
|
|
7
|
+
| "ready"
|
|
8
|
+
| "starting"
|
|
9
|
+
| "started"
|
|
10
|
+
| "stopping"
|
|
11
|
+
| "stopped"
|
|
12
|
+
| "failed";
|
|
13
|
+
|
|
14
|
+
export interface PluginConfigSchema<TConfig> {
|
|
15
|
+
parse(value: unknown): TConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PluginConfigParser<TConfig> =
|
|
19
|
+
| PluginConfigSchema<TConfig>
|
|
20
|
+
| ((value: unknown) => TConfig);
|
|
21
|
+
|
|
22
|
+
export interface PluginServiceRegistry {
|
|
23
|
+
provide<T>(
|
|
24
|
+
key: PluginServiceKey<T>,
|
|
25
|
+
value: T,
|
|
26
|
+
options?: {
|
|
27
|
+
replace?: boolean;
|
|
28
|
+
}
|
|
29
|
+
): void;
|
|
30
|
+
get<T>(key: PluginServiceKey<T>): T;
|
|
31
|
+
optional<T>(
|
|
32
|
+
key: PluginServiceKey<T>
|
|
33
|
+
): T | undefined;
|
|
34
|
+
has(key: PluginServiceKey): boolean;
|
|
35
|
+
delete(key: PluginServiceKey): boolean;
|
|
36
|
+
keys(): PluginServiceKey[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PluginHookHandler<TPayload = unknown> = (
|
|
40
|
+
payload: TPayload
|
|
41
|
+
) => void | Promise<void>;
|
|
42
|
+
|
|
43
|
+
export interface PluginHookBus {
|
|
44
|
+
on<TPayload = unknown>(
|
|
45
|
+
name: string,
|
|
46
|
+
handler: PluginHookHandler<TPayload>
|
|
47
|
+
): () => void;
|
|
48
|
+
emit<TPayload = unknown>(
|
|
49
|
+
name: string,
|
|
50
|
+
payload: TPayload
|
|
51
|
+
): Promise<void>;
|
|
52
|
+
listenerCount(name: string): number;
|
|
53
|
+
clear(name?: string): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PluginHostView {
|
|
57
|
+
readonly started: boolean;
|
|
58
|
+
plugin(name: string): PluginRecord | null;
|
|
59
|
+
plugins(): PluginRecord[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PluginContext<TConfig = unknown> {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly config: TConfig;
|
|
65
|
+
readonly services: PluginServiceRegistry;
|
|
66
|
+
readonly hooks: PluginHookBus;
|
|
67
|
+
readonly host: PluginHostView;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PluginDefinition<TConfig = unknown> {
|
|
71
|
+
name: string;
|
|
72
|
+
version?: string;
|
|
73
|
+
requires?: readonly string[];
|
|
74
|
+
optional?: readonly string[];
|
|
75
|
+
config?: unknown;
|
|
76
|
+
schema?: PluginConfigParser<TConfig>;
|
|
77
|
+
setup?(
|
|
78
|
+
context: PluginContext<TConfig>
|
|
79
|
+
): void | Promise<void>;
|
|
80
|
+
start?(
|
|
81
|
+
context: PluginContext<TConfig>
|
|
82
|
+
): void | Promise<void>;
|
|
83
|
+
stop?(
|
|
84
|
+
context: PluginContext<TConfig>
|
|
85
|
+
): void | Promise<void>;
|
|
86
|
+
dispose?(
|
|
87
|
+
context: PluginContext<TConfig>
|
|
88
|
+
): void | Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PluginModule {
|
|
92
|
+
name: string;
|
|
93
|
+
plugins: readonly PluginDefinition<any>[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface PluginRecord {
|
|
97
|
+
name: string;
|
|
98
|
+
version?: string;
|
|
99
|
+
state: PluginState;
|
|
100
|
+
requires: string[];
|
|
101
|
+
optional: string[];
|
|
102
|
+
registeredAt: number;
|
|
103
|
+
setupAt?: number;
|
|
104
|
+
startedAt?: number;
|
|
105
|
+
stoppedAt?: number;
|
|
106
|
+
error?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface PluginHostOptions {
|
|
110
|
+
plugins?: readonly PluginDefinition<any>[];
|
|
111
|
+
modules?: readonly PluginModule[];
|
|
112
|
+
configs?: Record<string, unknown>;
|
|
113
|
+
services?: Iterable<
|
|
114
|
+
readonly [PluginServiceKey, unknown]
|
|
115
|
+
>;
|
|
116
|
+
now?: () => number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface PluginHost
|
|
120
|
+
extends PluginHostView {
|
|
121
|
+
readonly services: PluginServiceRegistry;
|
|
122
|
+
readonly hooks: PluginHookBus;
|
|
123
|
+
use(
|
|
124
|
+
extension:
|
|
125
|
+
| PluginDefinition<any>
|
|
126
|
+
| PluginModule
|
|
127
|
+
): PluginHost;
|
|
128
|
+
resolveOrder(): string[];
|
|
129
|
+
setup(): Promise<void>;
|
|
130
|
+
start(): Promise<void>;
|
|
131
|
+
stop(): Promise<void>;
|
|
132
|
+
close(): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface InternalPlugin {
|
|
136
|
+
definition: PluginDefinition<any>;
|
|
137
|
+
record: PluginRecord;
|
|
138
|
+
context?: PluginContext<any>;
|
|
139
|
+
disposed: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class PluginDependencyError
|
|
143
|
+
extends Error {
|
|
144
|
+
constructor(message: string) {
|
|
145
|
+
super(message);
|
|
146
|
+
this.name =
|
|
147
|
+
"PluginDependencyError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class PluginLifecycleError
|
|
152
|
+
extends Error {
|
|
153
|
+
readonly plugin: string;
|
|
154
|
+
readonly phase: string;
|
|
155
|
+
readonly cause: unknown;
|
|
156
|
+
|
|
157
|
+
constructor(
|
|
158
|
+
plugin: string,
|
|
159
|
+
phase: string,
|
|
160
|
+
cause: unknown
|
|
161
|
+
) {
|
|
162
|
+
super(
|
|
163
|
+
`BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError(cause)}`
|
|
164
|
+
);
|
|
165
|
+
this.name =
|
|
166
|
+
"PluginLifecycleError";
|
|
167
|
+
this.plugin = plugin;
|
|
168
|
+
this.phase = phase;
|
|
169
|
+
this.cause = cause;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function definePlugin<TConfig = unknown>(
|
|
174
|
+
definition: PluginDefinition<TConfig>
|
|
175
|
+
): PluginDefinition<TConfig> {
|
|
176
|
+
validatePluginDefinition(
|
|
177
|
+
definition
|
|
178
|
+
);
|
|
179
|
+
return definition;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function defineModule(
|
|
183
|
+
module: PluginModule
|
|
184
|
+
): PluginModule {
|
|
185
|
+
if (!module || typeof module !== "object") {
|
|
186
|
+
throw new TypeError(
|
|
187
|
+
"BCP Plugins: module must be an object."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
normalizeName(
|
|
191
|
+
module.name,
|
|
192
|
+
"module name"
|
|
193
|
+
);
|
|
194
|
+
if (!Array.isArray(module.plugins)) {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
"BCP Plugins: module plugins must be an array."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
for (const plugin of module.plugins) {
|
|
200
|
+
validatePluginDefinition(plugin);
|
|
201
|
+
}
|
|
202
|
+
return module;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createPluginServiceRegistry(
|
|
206
|
+
initial?: Iterable<
|
|
207
|
+
readonly [PluginServiceKey, unknown]
|
|
208
|
+
>
|
|
209
|
+
): PluginServiceRegistry {
|
|
210
|
+
const values =
|
|
211
|
+
new Map<PluginServiceKey, unknown>();
|
|
212
|
+
|
|
213
|
+
if (initial) {
|
|
214
|
+
for (const [key, value] of initial) {
|
|
215
|
+
assertServiceKey(key);
|
|
216
|
+
if (values.has(key)) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`BCP Plugins: duplicate initial service ${formatServiceKey(key)}.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
values.set(key, value);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const registry:
|
|
226
|
+
PluginServiceRegistry = {
|
|
227
|
+
provide<T>(
|
|
228
|
+
key: PluginServiceKey<T>,
|
|
229
|
+
value: T,
|
|
230
|
+
options: {
|
|
231
|
+
replace?: boolean;
|
|
232
|
+
} = {}
|
|
233
|
+
): void {
|
|
234
|
+
assertServiceKey(key);
|
|
235
|
+
if (
|
|
236
|
+
values.has(key) &&
|
|
237
|
+
!options.replace
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`BCP Plugins: service ${formatServiceKey(key)} is already registered.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
values.set(key, value);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
get<T>(key: PluginServiceKey<T>): T {
|
|
247
|
+
assertServiceKey(key);
|
|
248
|
+
if (!values.has(key)) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`BCP Plugins: service ${formatServiceKey(key)} is not registered.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return values.get(key) as T;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
optional<T>(
|
|
257
|
+
key: PluginServiceKey<T>
|
|
258
|
+
): T | undefined {
|
|
259
|
+
assertServiceKey(key);
|
|
260
|
+
return values.get(key) as
|
|
261
|
+
T | undefined;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
has(key: PluginServiceKey): boolean {
|
|
265
|
+
assertServiceKey(key);
|
|
266
|
+
return values.has(key);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
delete(key: PluginServiceKey): boolean {
|
|
270
|
+
assertServiceKey(key);
|
|
271
|
+
return values.delete(key);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
keys(): PluginServiceKey[] {
|
|
275
|
+
return [
|
|
276
|
+
...values.keys(),
|
|
277
|
+
];
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return registry;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createPluginHookBus():
|
|
285
|
+
PluginHookBus {
|
|
286
|
+
const hooks =
|
|
287
|
+
new Map<
|
|
288
|
+
string,
|
|
289
|
+
Set<PluginHookHandler<any>>
|
|
290
|
+
>();
|
|
291
|
+
|
|
292
|
+
const bus:
|
|
293
|
+
PluginHookBus = {
|
|
294
|
+
on<TPayload = unknown>(
|
|
295
|
+
name: string,
|
|
296
|
+
handler: PluginHookHandler<TPayload>
|
|
297
|
+
): () => void {
|
|
298
|
+
const normalized =
|
|
299
|
+
normalizeName(
|
|
300
|
+
name,
|
|
301
|
+
"hook name"
|
|
302
|
+
);
|
|
303
|
+
if (typeof handler !== "function") {
|
|
304
|
+
throw new TypeError(
|
|
305
|
+
"BCP Plugins: hook handler must be a function."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
let group =
|
|
309
|
+
hooks.get(normalized);
|
|
310
|
+
if (!group) {
|
|
311
|
+
group = new Set();
|
|
312
|
+
hooks.set(
|
|
313
|
+
normalized,
|
|
314
|
+
group
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
group.add(
|
|
318
|
+
handler as PluginHookHandler<any>
|
|
319
|
+
);
|
|
320
|
+
return () => {
|
|
321
|
+
group?.delete(
|
|
322
|
+
handler as PluginHookHandler<any>
|
|
323
|
+
);
|
|
324
|
+
if (group?.size === 0) {
|
|
325
|
+
hooks.delete(normalized);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async emit<TPayload = unknown>(
|
|
331
|
+
name: string,
|
|
332
|
+
payload: TPayload
|
|
333
|
+
): Promise<void> {
|
|
334
|
+
const normalized =
|
|
335
|
+
normalizeName(
|
|
336
|
+
name,
|
|
337
|
+
"hook name"
|
|
338
|
+
);
|
|
339
|
+
const group =
|
|
340
|
+
hooks.get(normalized);
|
|
341
|
+
if (!group) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
for (const handler of [
|
|
345
|
+
...group,
|
|
346
|
+
]) {
|
|
347
|
+
await handler(payload);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
listenerCount(name: string): number {
|
|
352
|
+
return hooks.get(
|
|
353
|
+
normalizeName(
|
|
354
|
+
name,
|
|
355
|
+
"hook name"
|
|
356
|
+
)
|
|
357
|
+
)?.size ?? 0;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
clear(name?: string): void {
|
|
361
|
+
if (name === undefined) {
|
|
362
|
+
hooks.clear();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
hooks.delete(
|
|
366
|
+
normalizeName(
|
|
367
|
+
name,
|
|
368
|
+
"hook name"
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return bus;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function createPluginHost(
|
|
378
|
+
options: PluginHostOptions = {}
|
|
379
|
+
): PluginHost {
|
|
380
|
+
const now =
|
|
381
|
+
options.now ?? Date.now;
|
|
382
|
+
const services =
|
|
383
|
+
createPluginServiceRegistry(
|
|
384
|
+
options.services
|
|
385
|
+
);
|
|
386
|
+
const hooks =
|
|
387
|
+
createPluginHookBus();
|
|
388
|
+
const entries =
|
|
389
|
+
new Map<string, InternalPlugin>();
|
|
390
|
+
const configs = {
|
|
391
|
+
...(options.configs ?? {}),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
let setupCompleted = false;
|
|
395
|
+
let started = false;
|
|
396
|
+
let closed = false;
|
|
397
|
+
let lifecycleActive = false;
|
|
398
|
+
let fatalError:
|
|
399
|
+
PluginLifecycleError | null =
|
|
400
|
+
null;
|
|
401
|
+
|
|
402
|
+
const host:
|
|
403
|
+
PluginHost = {
|
|
404
|
+
services,
|
|
405
|
+
hooks,
|
|
406
|
+
|
|
407
|
+
get started() {
|
|
408
|
+
return started;
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
use(extension) {
|
|
412
|
+
assertMutable();
|
|
413
|
+
if (isPluginModule(extension)) {
|
|
414
|
+
defineModule(extension);
|
|
415
|
+
for (const plugin of extension.plugins) {
|
|
416
|
+
register(plugin);
|
|
417
|
+
}
|
|
418
|
+
return host;
|
|
419
|
+
}
|
|
420
|
+
register(extension);
|
|
421
|
+
return host;
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
resolveOrder() {
|
|
425
|
+
return resolvePluginOrder(
|
|
426
|
+
entries
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
async setup() {
|
|
431
|
+
assertOpen();
|
|
432
|
+
assertHealthy();
|
|
433
|
+
if (setupCompleted) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
assertNotActive();
|
|
437
|
+
lifecycleActive = true;
|
|
438
|
+
try {
|
|
439
|
+
const order =
|
|
440
|
+
resolvePluginOrder(
|
|
441
|
+
entries
|
|
442
|
+
);
|
|
443
|
+
for (const name of order) {
|
|
444
|
+
const entry =
|
|
445
|
+
requireInternal(name);
|
|
446
|
+
if (
|
|
447
|
+
entry.record.state !==
|
|
448
|
+
"registered"
|
|
449
|
+
) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
entry.record.state =
|
|
453
|
+
"setting-up";
|
|
454
|
+
try {
|
|
455
|
+
const context =
|
|
456
|
+
createContext(entry);
|
|
457
|
+
entry.context = context;
|
|
458
|
+
await entry.definition.setup?.(
|
|
459
|
+
context
|
|
460
|
+
);
|
|
461
|
+
entry.record.state =
|
|
462
|
+
"ready";
|
|
463
|
+
entry.record.setupAt =
|
|
464
|
+
timestamp();
|
|
465
|
+
} catch (error) {
|
|
466
|
+
markFailed(
|
|
467
|
+
entry,
|
|
468
|
+
error
|
|
469
|
+
);
|
|
470
|
+
const lifecycleError =
|
|
471
|
+
error instanceof PluginLifecycleError
|
|
472
|
+
? error
|
|
473
|
+
: new PluginLifecycleError(
|
|
474
|
+
name,
|
|
475
|
+
"setup",
|
|
476
|
+
error
|
|
477
|
+
);
|
|
478
|
+
fatalError =
|
|
479
|
+
lifecycleError;
|
|
480
|
+
await disposePrepared(
|
|
481
|
+
order,
|
|
482
|
+
name
|
|
483
|
+
);
|
|
484
|
+
throw lifecycleError;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
setupCompleted = true;
|
|
488
|
+
} finally {
|
|
489
|
+
lifecycleActive = false;
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
async start() {
|
|
494
|
+
assertOpen();
|
|
495
|
+
assertHealthy();
|
|
496
|
+
if (started) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!setupCompleted) {
|
|
500
|
+
await host.setup();
|
|
501
|
+
}
|
|
502
|
+
assertHealthy();
|
|
503
|
+
assertNotActive();
|
|
504
|
+
lifecycleActive = true;
|
|
505
|
+
const order =
|
|
506
|
+
resolvePluginOrder(
|
|
507
|
+
entries
|
|
508
|
+
);
|
|
509
|
+
const startedNames:
|
|
510
|
+
string[] = [];
|
|
511
|
+
try {
|
|
512
|
+
for (const name of order) {
|
|
513
|
+
const entry =
|
|
514
|
+
requireInternal(name);
|
|
515
|
+
if (
|
|
516
|
+
entry.record.state !== "ready" &&
|
|
517
|
+
entry.record.state !== "stopped"
|
|
518
|
+
) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
entry.record.state =
|
|
522
|
+
"starting";
|
|
523
|
+
try {
|
|
524
|
+
await entry.definition.start?.(
|
|
525
|
+
requireContext(entry)
|
|
526
|
+
);
|
|
527
|
+
entry.record.state =
|
|
528
|
+
"started";
|
|
529
|
+
entry.record.startedAt =
|
|
530
|
+
timestamp();
|
|
531
|
+
entry.record.error =
|
|
532
|
+
undefined;
|
|
533
|
+
startedNames.push(name);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
markFailed(
|
|
536
|
+
entry,
|
|
537
|
+
error
|
|
538
|
+
);
|
|
539
|
+
const lifecycleError =
|
|
540
|
+
new PluginLifecycleError(
|
|
541
|
+
name,
|
|
542
|
+
"start",
|
|
543
|
+
error
|
|
544
|
+
);
|
|
545
|
+
fatalError =
|
|
546
|
+
lifecycleError;
|
|
547
|
+
await stopNames(
|
|
548
|
+
[
|
|
549
|
+
...startedNames,
|
|
550
|
+
].reverse()
|
|
551
|
+
);
|
|
552
|
+
throw lifecycleError;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
started = true;
|
|
556
|
+
} finally {
|
|
557
|
+
lifecycleActive = false;
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
async stop() {
|
|
562
|
+
if (closed || !started) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
assertNotActive();
|
|
566
|
+
lifecycleActive = true;
|
|
567
|
+
try {
|
|
568
|
+
const errors =
|
|
569
|
+
await stopNames(
|
|
570
|
+
resolvePluginOrder(
|
|
571
|
+
entries
|
|
572
|
+
).reverse()
|
|
573
|
+
);
|
|
574
|
+
started = false;
|
|
575
|
+
if (errors.length > 0) {
|
|
576
|
+
throw new AggregateError(
|
|
577
|
+
errors,
|
|
578
|
+
"BCP Plugins: one or more plugin stop hooks failed."
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
} finally {
|
|
582
|
+
lifecycleActive = false;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
async close() {
|
|
587
|
+
if (closed) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const errors:
|
|
592
|
+
unknown[] = [];
|
|
593
|
+
|
|
594
|
+
if (started) {
|
|
595
|
+
try {
|
|
596
|
+
await host.stop();
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (
|
|
599
|
+
error instanceof AggregateError
|
|
600
|
+
) {
|
|
601
|
+
errors.push(
|
|
602
|
+
...error.errors
|
|
603
|
+
);
|
|
604
|
+
} else {
|
|
605
|
+
errors.push(error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
assertNotActive();
|
|
611
|
+
lifecycleActive = true;
|
|
612
|
+
try {
|
|
613
|
+
const order =
|
|
614
|
+
resolvePluginOrder(
|
|
615
|
+
entries
|
|
616
|
+
).reverse();
|
|
617
|
+
for (const name of order) {
|
|
618
|
+
const entry =
|
|
619
|
+
requireInternal(name);
|
|
620
|
+
if (
|
|
621
|
+
entry.disposed ||
|
|
622
|
+
!entry.context
|
|
623
|
+
) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await entry.definition.dispose?.(
|
|
628
|
+
entry.context
|
|
629
|
+
);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
errors.push(
|
|
632
|
+
new PluginLifecycleError(
|
|
633
|
+
name,
|
|
634
|
+
"dispose",
|
|
635
|
+
error
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
} finally {
|
|
639
|
+
entry.disposed = true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
hooks.clear();
|
|
643
|
+
closed = true;
|
|
644
|
+
} finally {
|
|
645
|
+
lifecycleActive = false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (errors.length > 0) {
|
|
649
|
+
throw new AggregateError(
|
|
650
|
+
errors,
|
|
651
|
+
"BCP Plugins: one or more plugin shutdown hooks failed."
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
plugin(name) {
|
|
657
|
+
const entry =
|
|
658
|
+
entries.get(
|
|
659
|
+
normalizeName(
|
|
660
|
+
name,
|
|
661
|
+
"plugin name"
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
return entry
|
|
665
|
+
? cloneRecord(
|
|
666
|
+
entry.record
|
|
667
|
+
)
|
|
668
|
+
: null;
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
plugins() {
|
|
672
|
+
return [
|
|
673
|
+
...entries.values(),
|
|
674
|
+
]
|
|
675
|
+
.map(entry =>
|
|
676
|
+
cloneRecord(
|
|
677
|
+
entry.record
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
.sort((left, right) =>
|
|
681
|
+
left.registeredAt -
|
|
682
|
+
right.registeredAt ||
|
|
683
|
+
left.name.localeCompare(
|
|
684
|
+
right.name
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
for (const module of options.modules ?? []) {
|
|
691
|
+
host.use(module);
|
|
692
|
+
}
|
|
693
|
+
for (const plugin of options.plugins ?? []) {
|
|
694
|
+
host.use(plugin);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return host;
|
|
698
|
+
|
|
699
|
+
function register(
|
|
700
|
+
definition: PluginDefinition<any>
|
|
701
|
+
): void {
|
|
702
|
+
validatePluginDefinition(
|
|
703
|
+
definition
|
|
704
|
+
);
|
|
705
|
+
const name =
|
|
706
|
+
normalizeName(
|
|
707
|
+
definition.name,
|
|
708
|
+
"plugin name"
|
|
709
|
+
);
|
|
710
|
+
if (entries.has(name)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`BCP Plugins: plugin "${name}" is already registered.`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
const requires =
|
|
716
|
+
normalizeDependencyList(
|
|
717
|
+
definition.requires,
|
|
718
|
+
name,
|
|
719
|
+
"requires"
|
|
720
|
+
);
|
|
721
|
+
const optional =
|
|
722
|
+
normalizeDependencyList(
|
|
723
|
+
definition.optional,
|
|
724
|
+
name,
|
|
725
|
+
"optional"
|
|
726
|
+
);
|
|
727
|
+
entries.set(
|
|
728
|
+
name,
|
|
729
|
+
{
|
|
730
|
+
definition: {
|
|
731
|
+
...definition,
|
|
732
|
+
name,
|
|
733
|
+
requires,
|
|
734
|
+
optional,
|
|
735
|
+
},
|
|
736
|
+
record: {
|
|
737
|
+
name,
|
|
738
|
+
version:
|
|
739
|
+
normalizeOptionalVersion(
|
|
740
|
+
definition.version
|
|
741
|
+
),
|
|
742
|
+
state:
|
|
743
|
+
"registered",
|
|
744
|
+
requires,
|
|
745
|
+
optional,
|
|
746
|
+
registeredAt:
|
|
747
|
+
timestamp(),
|
|
748
|
+
},
|
|
749
|
+
disposed: false,
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function createContext(
|
|
755
|
+
entry: InternalPlugin
|
|
756
|
+
): PluginContext<any> {
|
|
757
|
+
const rawConfig =
|
|
758
|
+
Object.prototype.hasOwnProperty.call(
|
|
759
|
+
configs,
|
|
760
|
+
entry.record.name
|
|
761
|
+
)
|
|
762
|
+
? configs[entry.record.name]
|
|
763
|
+
: entry.definition.config;
|
|
764
|
+
const config =
|
|
765
|
+
parseConfig(
|
|
766
|
+
entry.definition.schema,
|
|
767
|
+
rawConfig,
|
|
768
|
+
entry.record.name
|
|
769
|
+
);
|
|
770
|
+
return {
|
|
771
|
+
name:
|
|
772
|
+
entry.record.name,
|
|
773
|
+
config,
|
|
774
|
+
services,
|
|
775
|
+
hooks,
|
|
776
|
+
host,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function stopNames(
|
|
781
|
+
names: readonly string[]
|
|
782
|
+
): Promise<unknown[]> {
|
|
783
|
+
const errors:
|
|
784
|
+
unknown[] = [];
|
|
785
|
+
for (const name of names) {
|
|
786
|
+
const entry =
|
|
787
|
+
requireInternal(name);
|
|
788
|
+
if (
|
|
789
|
+
entry.record.state !==
|
|
790
|
+
"started"
|
|
791
|
+
) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
entry.record.state =
|
|
795
|
+
"stopping";
|
|
796
|
+
try {
|
|
797
|
+
await entry.definition.stop?.(
|
|
798
|
+
requireContext(entry)
|
|
799
|
+
);
|
|
800
|
+
entry.record.state =
|
|
801
|
+
"stopped";
|
|
802
|
+
entry.record.stoppedAt =
|
|
803
|
+
timestamp();
|
|
804
|
+
} catch (error) {
|
|
805
|
+
markFailed(
|
|
806
|
+
entry,
|
|
807
|
+
error
|
|
808
|
+
);
|
|
809
|
+
errors.push(
|
|
810
|
+
new PluginLifecycleError(
|
|
811
|
+
name,
|
|
812
|
+
"stop",
|
|
813
|
+
error
|
|
814
|
+
)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return errors;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function disposePrepared(
|
|
822
|
+
order: readonly string[],
|
|
823
|
+
failedName: string
|
|
824
|
+
): Promise<void> {
|
|
825
|
+
const index =
|
|
826
|
+
order.indexOf(failedName);
|
|
827
|
+
const names =
|
|
828
|
+
order.slice(
|
|
829
|
+
0,
|
|
830
|
+
Math.max(0, index) + 1
|
|
831
|
+
).reverse();
|
|
832
|
+
|
|
833
|
+
for (const name of names) {
|
|
834
|
+
const entry =
|
|
835
|
+
requireInternal(name);
|
|
836
|
+
if (
|
|
837
|
+
entry.disposed ||
|
|
838
|
+
!entry.context
|
|
839
|
+
) {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
await entry.definition.dispose?.(
|
|
844
|
+
entry.context
|
|
845
|
+
);
|
|
846
|
+
} catch {
|
|
847
|
+
// Preserve the original setup failure.
|
|
848
|
+
} finally {
|
|
849
|
+
entry.disposed = true;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function requireInternal(
|
|
855
|
+
name: string
|
|
856
|
+
): InternalPlugin {
|
|
857
|
+
const entry =
|
|
858
|
+
entries.get(name);
|
|
859
|
+
if (!entry) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`BCP Plugins: plugin "${name}" is not registered.`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return entry;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function timestamp(): number {
|
|
868
|
+
const value = now();
|
|
869
|
+
if (!Number.isFinite(value)) {
|
|
870
|
+
throw new TypeError(
|
|
871
|
+
"BCP Plugins: now() must return a finite number."
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function assertOpen(): void {
|
|
878
|
+
if (closed) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
"BCP Plugins: plugin host is closed."
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function assertHealthy(): void {
|
|
886
|
+
if (fatalError) {
|
|
887
|
+
throw fatalError;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function assertMutable(): void {
|
|
892
|
+
assertOpen();
|
|
893
|
+
if (
|
|
894
|
+
setupCompleted ||
|
|
895
|
+
lifecycleActive ||
|
|
896
|
+
fatalError
|
|
897
|
+
) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
"BCP Plugins: plugins cannot be registered after setup begins or after a lifecycle failure."
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function assertNotActive(): void {
|
|
905
|
+
if (lifecycleActive) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
"BCP Plugins: another lifecycle transition is already running."
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function resolvePluginOrder(
|
|
914
|
+
entries: Map<string, InternalPlugin>
|
|
915
|
+
): string[] {
|
|
916
|
+
for (const entry of entries.values()) {
|
|
917
|
+
for (
|
|
918
|
+
const dependency
|
|
919
|
+
of entry.record.requires
|
|
920
|
+
) {
|
|
921
|
+
if (!entries.has(dependency)) {
|
|
922
|
+
throw new PluginDependencyError(
|
|
923
|
+
`BCP Plugins: plugin "${entry.record.name}" requires missing plugin "${dependency}".`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const visiting =
|
|
930
|
+
new Set<string>();
|
|
931
|
+
const visited =
|
|
932
|
+
new Set<string>();
|
|
933
|
+
const order:
|
|
934
|
+
string[] = [];
|
|
935
|
+
const stack:
|
|
936
|
+
string[] = [];
|
|
937
|
+
|
|
938
|
+
const visit = (
|
|
939
|
+
name: string
|
|
940
|
+
): void => {
|
|
941
|
+
if (visited.has(name)) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (visiting.has(name)) {
|
|
945
|
+
const start =
|
|
946
|
+
stack.indexOf(name);
|
|
947
|
+
const cycle = [
|
|
948
|
+
...stack.slice(
|
|
949
|
+
Math.max(0, start)
|
|
950
|
+
),
|
|
951
|
+
name,
|
|
952
|
+
];
|
|
953
|
+
throw new PluginDependencyError(
|
|
954
|
+
`BCP Plugins: dependency cycle detected: ${cycle.join(" -> ")}.`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const entry =
|
|
959
|
+
entries.get(name);
|
|
960
|
+
if (!entry) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
visiting.add(name);
|
|
965
|
+
stack.push(name);
|
|
966
|
+
|
|
967
|
+
for (const dependency of [
|
|
968
|
+
...entry.record.requires,
|
|
969
|
+
...entry.record.optional.filter(
|
|
970
|
+
candidate =>
|
|
971
|
+
entries.has(candidate)
|
|
972
|
+
),
|
|
973
|
+
]) {
|
|
974
|
+
visit(dependency);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
stack.pop();
|
|
978
|
+
visiting.delete(name);
|
|
979
|
+
visited.add(name);
|
|
980
|
+
order.push(name);
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
for (const name of entries.keys()) {
|
|
984
|
+
visit(name);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return order;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function validatePluginDefinition(
|
|
991
|
+
definition: PluginDefinition<any>
|
|
992
|
+
): void {
|
|
993
|
+
if (
|
|
994
|
+
!definition ||
|
|
995
|
+
typeof definition !== "object"
|
|
996
|
+
) {
|
|
997
|
+
throw new TypeError(
|
|
998
|
+
"BCP Plugins: plugin definition must be an object."
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const name =
|
|
1002
|
+
normalizeName(
|
|
1003
|
+
definition.name,
|
|
1004
|
+
"plugin name"
|
|
1005
|
+
);
|
|
1006
|
+
normalizeDependencyList(
|
|
1007
|
+
definition.requires,
|
|
1008
|
+
name,
|
|
1009
|
+
"requires"
|
|
1010
|
+
);
|
|
1011
|
+
normalizeDependencyList(
|
|
1012
|
+
definition.optional,
|
|
1013
|
+
name,
|
|
1014
|
+
"optional"
|
|
1015
|
+
);
|
|
1016
|
+
for (const hook of [
|
|
1017
|
+
"setup",
|
|
1018
|
+
"start",
|
|
1019
|
+
"stop",
|
|
1020
|
+
"dispose",
|
|
1021
|
+
] as const) {
|
|
1022
|
+
const value =
|
|
1023
|
+
definition[hook];
|
|
1024
|
+
if (
|
|
1025
|
+
value !== undefined &&
|
|
1026
|
+
typeof value !== "function"
|
|
1027
|
+
) {
|
|
1028
|
+
throw new TypeError(
|
|
1029
|
+
`BCP Plugins: plugin "${name}" ${hook} must be a function.`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (
|
|
1034
|
+
definition.schema !== undefined &&
|
|
1035
|
+
typeof definition.schema !== "function" &&
|
|
1036
|
+
(
|
|
1037
|
+
!definition.schema ||
|
|
1038
|
+
typeof definition.schema.parse !==
|
|
1039
|
+
"function"
|
|
1040
|
+
)
|
|
1041
|
+
) {
|
|
1042
|
+
throw new TypeError(
|
|
1043
|
+
`BCP Plugins: plugin "${name}" schema must be a parser function or object with parse().`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function normalizeDependencyList(
|
|
1049
|
+
value: readonly string[] | undefined,
|
|
1050
|
+
plugin: string,
|
|
1051
|
+
field: string
|
|
1052
|
+
): string[] {
|
|
1053
|
+
if (value === undefined) {
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
if (!Array.isArray(value)) {
|
|
1057
|
+
throw new TypeError(
|
|
1058
|
+
`BCP Plugins: plugin "${plugin}" ${field} must be an array.`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const normalized =
|
|
1062
|
+
value.map(item =>
|
|
1063
|
+
normalizeName(
|
|
1064
|
+
item,
|
|
1065
|
+
`${field} dependency`
|
|
1066
|
+
)
|
|
1067
|
+
);
|
|
1068
|
+
if (
|
|
1069
|
+
new Set(normalized).size !==
|
|
1070
|
+
normalized.length
|
|
1071
|
+
) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
`BCP Plugins: plugin "${plugin}" ${field} contains duplicate dependencies.`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (normalized.includes(plugin)) {
|
|
1077
|
+
throw new PluginDependencyError(
|
|
1078
|
+
`BCP Plugins: plugin "${plugin}" cannot depend on itself.`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return normalized;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function parseConfig<TConfig>(
|
|
1085
|
+
parser: PluginConfigParser<TConfig> | undefined,
|
|
1086
|
+
raw: unknown,
|
|
1087
|
+
plugin: string
|
|
1088
|
+
): TConfig {
|
|
1089
|
+
if (!parser) {
|
|
1090
|
+
return raw as TConfig;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
return typeof parser === "function"
|
|
1094
|
+
? parser(raw)
|
|
1095
|
+
: parser.parse(raw);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
throw new PluginLifecycleError(
|
|
1098
|
+
plugin,
|
|
1099
|
+
"config",
|
|
1100
|
+
error
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function requireContext(
|
|
1106
|
+
entry: InternalPlugin
|
|
1107
|
+
): PluginContext<any> {
|
|
1108
|
+
if (!entry.context) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`BCP Plugins: plugin "${entry.record.name}" has not been set up.`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
return entry.context;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function markFailed(
|
|
1117
|
+
entry: InternalPlugin,
|
|
1118
|
+
error: unknown
|
|
1119
|
+
): void {
|
|
1120
|
+
entry.record.state =
|
|
1121
|
+
"failed";
|
|
1122
|
+
entry.record.error =
|
|
1123
|
+
formatError(error);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function cloneRecord(
|
|
1127
|
+
record: PluginRecord
|
|
1128
|
+
): PluginRecord {
|
|
1129
|
+
return {
|
|
1130
|
+
...record,
|
|
1131
|
+
requires: [
|
|
1132
|
+
...record.requires,
|
|
1133
|
+
],
|
|
1134
|
+
optional: [
|
|
1135
|
+
...record.optional,
|
|
1136
|
+
],
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function isPluginModule(
|
|
1141
|
+
value:
|
|
1142
|
+
| PluginDefinition<any>
|
|
1143
|
+
| PluginModule
|
|
1144
|
+
): value is PluginModule {
|
|
1145
|
+
return Boolean(
|
|
1146
|
+
value &&
|
|
1147
|
+
typeof value === "object" &&
|
|
1148
|
+
Array.isArray(
|
|
1149
|
+
(value as PluginModule).plugins
|
|
1150
|
+
)
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function normalizeName(
|
|
1155
|
+
value: unknown,
|
|
1156
|
+
field: string
|
|
1157
|
+
): string {
|
|
1158
|
+
const text =
|
|
1159
|
+
String(value ?? "").trim();
|
|
1160
|
+
if (!text) {
|
|
1161
|
+
throw new TypeError(
|
|
1162
|
+
`BCP Plugins: ${field} must be a non-empty string.`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
if (text.length > 200) {
|
|
1166
|
+
throw new TypeError(
|
|
1167
|
+
`BCP Plugins: ${field} must not exceed 200 characters.`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
return text;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function normalizeOptionalVersion(
|
|
1174
|
+
value: string | undefined
|
|
1175
|
+
): string | undefined {
|
|
1176
|
+
if (value === undefined) {
|
|
1177
|
+
return undefined;
|
|
1178
|
+
}
|
|
1179
|
+
return normalizeName(
|
|
1180
|
+
value,
|
|
1181
|
+
"plugin version"
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function assertServiceKey(
|
|
1186
|
+
key: PluginServiceKey
|
|
1187
|
+
): void {
|
|
1188
|
+
if (typeof key === "string") {
|
|
1189
|
+
normalizeName(
|
|
1190
|
+
key,
|
|
1191
|
+
"service key"
|
|
1192
|
+
);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof key !== "symbol") {
|
|
1196
|
+
throw new TypeError(
|
|
1197
|
+
"BCP Plugins: service key must be a string or symbol."
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function formatServiceKey(
|
|
1203
|
+
key: PluginServiceKey
|
|
1204
|
+
): string {
|
|
1205
|
+
return typeof key === "symbol"
|
|
1206
|
+
? String(key)
|
|
1207
|
+
: `"${key}"`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function formatError(
|
|
1211
|
+
error: unknown
|
|
1212
|
+
): string {
|
|
1213
|
+
if (error instanceof Error) {
|
|
1214
|
+
return error.message || error.name;
|
|
1215
|
+
}
|
|
1216
|
+
if (typeof error === "string") {
|
|
1217
|
+
return error;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
return JSON.stringify(error) ??
|
|
1221
|
+
String(error);
|
|
1222
|
+
} catch {
|
|
1223
|
+
return String(error);
|
|
1224
|
+
}
|
|
1225
|
+
}
|