@chidchanun/bcp 0.3.1 → 0.3.2
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/docs/api-freeze-snapshot.json +13 -2
- package/docs/api-manifest.json +22 -14
- package/docs/docs-web-manifest.json +6 -4
- package/docs/module-system-v2.md +386 -0
- package/docs/platform-manifest.json +19 -4
- package/docs/releases/0.3.2.md +125 -0
- package/package.json +7 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/application.mjs +434 -30
- package/packages/client/src/modules.mjs +392 -0
- package/packages/client/src/modules.ts +17 -0
- package/packages/server/src/application.ts +58 -9
- package/packages/server/src/modules.ts +682 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ServiceContainer,
|
|
3
|
+
type ServiceProvider,
|
|
4
|
+
type ServiceToken,
|
|
5
|
+
} from "./container.js";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type DeploymentResource,
|
|
9
|
+
} from "./deployment.js";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type PluginDefinition,
|
|
13
|
+
type PluginHookBus,
|
|
14
|
+
type PluginServiceKey,
|
|
15
|
+
type PluginServiceRegistry,
|
|
16
|
+
} from "./plugins.js";
|
|
17
|
+
|
|
18
|
+
export const MODULE_V2_KIND =
|
|
19
|
+
"bcp-module-v2" as const;
|
|
20
|
+
|
|
21
|
+
export interface ModuleConfigSchema<TConfig> {
|
|
22
|
+
parse(value: unknown): TConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ModuleConfigParser<TConfig> =
|
|
26
|
+
| ModuleConfigSchema<TConfig>
|
|
27
|
+
| ((value: unknown) => TConfig);
|
|
28
|
+
|
|
29
|
+
export interface ModuleContext<TConfig = unknown> {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly config: TConfig;
|
|
32
|
+
readonly container: ServiceContainer;
|
|
33
|
+
readonly services: PluginServiceRegistry;
|
|
34
|
+
readonly hooks: PluginHookBus;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ModuleDefinition<TConfig = unknown> {
|
|
38
|
+
readonly kind: typeof MODULE_V2_KIND;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly version?: string;
|
|
41
|
+
readonly imports?: readonly ModuleDefinition<any>[];
|
|
42
|
+
readonly providers?: readonly ServiceProvider<any>[];
|
|
43
|
+
readonly exports?: readonly ServiceToken<any>[];
|
|
44
|
+
readonly plugins?: readonly PluginDefinition<any>[];
|
|
45
|
+
readonly services?: Iterable<
|
|
46
|
+
readonly [PluginServiceKey, unknown]
|
|
47
|
+
>;
|
|
48
|
+
readonly resources?: readonly DeploymentResource[];
|
|
49
|
+
readonly config?: unknown;
|
|
50
|
+
readonly schema?: ModuleConfigParser<TConfig>;
|
|
51
|
+
setup?(
|
|
52
|
+
context: ModuleContext<TConfig>
|
|
53
|
+
): void | Promise<void>;
|
|
54
|
+
start?(
|
|
55
|
+
context: ModuleContext<TConfig>
|
|
56
|
+
): void | Promise<void>;
|
|
57
|
+
stop?(
|
|
58
|
+
context: ModuleContext<TConfig>
|
|
59
|
+
): void | Promise<void>;
|
|
60
|
+
dispose?(
|
|
61
|
+
context: ModuleContext<TConfig>
|
|
62
|
+
): void | Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ModuleInput<TConfig = unknown>
|
|
66
|
+
extends Omit<ModuleDefinition<TConfig>, "kind"> {
|
|
67
|
+
kind?: typeof MODULE_V2_KIND;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ModuleRecord {
|
|
71
|
+
name: string;
|
|
72
|
+
version?: string;
|
|
73
|
+
imports: string[];
|
|
74
|
+
providers: string[];
|
|
75
|
+
exports: string[];
|
|
76
|
+
plugins: string[];
|
|
77
|
+
resources: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ModuleComposition {
|
|
81
|
+
readonly modules: readonly ModuleDefinition<any>[];
|
|
82
|
+
readonly providers: readonly ServiceProvider<any>[];
|
|
83
|
+
readonly plugins: readonly PluginDefinition<any>[];
|
|
84
|
+
readonly services: readonly (
|
|
85
|
+
readonly [PluginServiceKey, unknown]
|
|
86
|
+
)[];
|
|
87
|
+
readonly resources: readonly DeploymentResource[];
|
|
88
|
+
records(): ModuleRecord[];
|
|
89
|
+
exportedTokens(moduleName: string): ServiceToken<any>[];
|
|
90
|
+
createLifecycleResources(
|
|
91
|
+
context: {
|
|
92
|
+
container: ServiceContainer;
|
|
93
|
+
services: PluginServiceRegistry;
|
|
94
|
+
hooks: PluginHookBus;
|
|
95
|
+
}
|
|
96
|
+
): DeploymentResource[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ModuleDependencyError
|
|
100
|
+
extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name =
|
|
104
|
+
"ModuleDependencyError";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class ModuleLifecycleError
|
|
109
|
+
extends Error {
|
|
110
|
+
readonly module: string;
|
|
111
|
+
readonly phase: string;
|
|
112
|
+
readonly cause: unknown;
|
|
113
|
+
|
|
114
|
+
constructor(
|
|
115
|
+
moduleName: string,
|
|
116
|
+
phase: string,
|
|
117
|
+
cause: unknown
|
|
118
|
+
) {
|
|
119
|
+
super(
|
|
120
|
+
`BCP Modules: ${phase} failed for module "${moduleName}": ${formatError(cause)}`
|
|
121
|
+
);
|
|
122
|
+
this.name =
|
|
123
|
+
"ModuleLifecycleError";
|
|
124
|
+
this.module = moduleName;
|
|
125
|
+
this.phase = phase;
|
|
126
|
+
this.cause = cause;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function defineModule<TConfig = unknown>(
|
|
131
|
+
input: ModuleInput<TConfig>
|
|
132
|
+
): ModuleDefinition<TConfig> {
|
|
133
|
+
const definition = {
|
|
134
|
+
...input,
|
|
135
|
+
kind:
|
|
136
|
+
MODULE_V2_KIND,
|
|
137
|
+
} satisfies ModuleDefinition<TConfig>;
|
|
138
|
+
|
|
139
|
+
validateModuleDefinition(definition);
|
|
140
|
+
return definition;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isModuleDefinition(
|
|
144
|
+
value: unknown
|
|
145
|
+
): value is ModuleDefinition<any> {
|
|
146
|
+
return Boolean(
|
|
147
|
+
value &&
|
|
148
|
+
typeof value === "object" &&
|
|
149
|
+
(
|
|
150
|
+
value as {
|
|
151
|
+
kind?: unknown;
|
|
152
|
+
}
|
|
153
|
+
).kind === MODULE_V2_KIND
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function composeModules(
|
|
158
|
+
roots: readonly ModuleDefinition<any>[] = []
|
|
159
|
+
): ModuleComposition {
|
|
160
|
+
const ordered =
|
|
161
|
+
resolveModuleOrder(roots);
|
|
162
|
+
const providers:
|
|
163
|
+
ServiceProvider<any>[] = [];
|
|
164
|
+
const plugins:
|
|
165
|
+
PluginDefinition<any>[] = [];
|
|
166
|
+
const services:
|
|
167
|
+
Array<
|
|
168
|
+
readonly [PluginServiceKey, unknown]
|
|
169
|
+
> = [];
|
|
170
|
+
const resources:
|
|
171
|
+
DeploymentResource[] = [];
|
|
172
|
+
const serviceKeys =
|
|
173
|
+
new Set<PluginServiceKey>();
|
|
174
|
+
|
|
175
|
+
for (const module of ordered) {
|
|
176
|
+
providers.push(
|
|
177
|
+
...(module.providers ?? [])
|
|
178
|
+
);
|
|
179
|
+
plugins.push(
|
|
180
|
+
...(module.plugins ?? [])
|
|
181
|
+
);
|
|
182
|
+
resources.push(
|
|
183
|
+
...(module.resources ?? [])
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
for (
|
|
187
|
+
const entry
|
|
188
|
+
of module.services ?? []
|
|
189
|
+
) {
|
|
190
|
+
const [key] = entry;
|
|
191
|
+
if (serviceKeys.has(key)) {
|
|
192
|
+
throw new ModuleDependencyError(
|
|
193
|
+
`BCP Modules: duplicate shared service ${formatServiceKey(key)} while composing module "${module.name}".`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
serviceKeys.add(key);
|
|
197
|
+
services.push(entry);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
validateModuleExports(
|
|
201
|
+
module
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
modules:
|
|
207
|
+
ordered,
|
|
208
|
+
providers,
|
|
209
|
+
plugins,
|
|
210
|
+
services,
|
|
211
|
+
resources,
|
|
212
|
+
|
|
213
|
+
records() {
|
|
214
|
+
return ordered.map(
|
|
215
|
+
module => ({
|
|
216
|
+
name:
|
|
217
|
+
module.name,
|
|
218
|
+
...(module.version
|
|
219
|
+
? {
|
|
220
|
+
version:
|
|
221
|
+
module.version,
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
224
|
+
imports:
|
|
225
|
+
(
|
|
226
|
+
module.imports ?? []
|
|
227
|
+
).map(
|
|
228
|
+
dependency =>
|
|
229
|
+
dependency.name
|
|
230
|
+
),
|
|
231
|
+
providers:
|
|
232
|
+
(
|
|
233
|
+
module.providers ?? []
|
|
234
|
+
).map(
|
|
235
|
+
provider =>
|
|
236
|
+
provider.token.description
|
|
237
|
+
),
|
|
238
|
+
exports:
|
|
239
|
+
(
|
|
240
|
+
module.exports ?? []
|
|
241
|
+
).map(
|
|
242
|
+
token =>
|
|
243
|
+
token.description
|
|
244
|
+
),
|
|
245
|
+
plugins:
|
|
246
|
+
(
|
|
247
|
+
module.plugins ?? []
|
|
248
|
+
).map(
|
|
249
|
+
plugin =>
|
|
250
|
+
plugin.name
|
|
251
|
+
),
|
|
252
|
+
resources:
|
|
253
|
+
(
|
|
254
|
+
module.resources ?? []
|
|
255
|
+
).map(
|
|
256
|
+
resource =>
|
|
257
|
+
resource.name
|
|
258
|
+
),
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
exportedTokens(moduleName) {
|
|
264
|
+
const normalized =
|
|
265
|
+
normalizeName(
|
|
266
|
+
moduleName,
|
|
267
|
+
"module name"
|
|
268
|
+
);
|
|
269
|
+
const module =
|
|
270
|
+
ordered.find(
|
|
271
|
+
entry =>
|
|
272
|
+
entry.name ===
|
|
273
|
+
normalized
|
|
274
|
+
);
|
|
275
|
+
if (!module) {
|
|
276
|
+
throw new ModuleDependencyError(
|
|
277
|
+
`BCP Modules: module "${normalized}" is not part of this composition.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return [
|
|
281
|
+
...(module.exports ?? []),
|
|
282
|
+
];
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
createLifecycleResources(context) {
|
|
286
|
+
return ordered.flatMap(
|
|
287
|
+
module => [
|
|
288
|
+
...(module.resources ?? []),
|
|
289
|
+
createModuleLifecycleResource(
|
|
290
|
+
module,
|
|
291
|
+
context
|
|
292
|
+
),
|
|
293
|
+
]
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function resolveModuleOrder(
|
|
300
|
+
roots: readonly ModuleDefinition<any>[]
|
|
301
|
+
): ModuleDefinition<any>[] {
|
|
302
|
+
const byName =
|
|
303
|
+
new Map<string, ModuleDefinition<any>>();
|
|
304
|
+
const visiting =
|
|
305
|
+
new Set<string>();
|
|
306
|
+
const visited =
|
|
307
|
+
new Set<string>();
|
|
308
|
+
const order:
|
|
309
|
+
ModuleDefinition<any>[] = [];
|
|
310
|
+
|
|
311
|
+
const visit = (
|
|
312
|
+
module: ModuleDefinition<any>,
|
|
313
|
+
path: string[]
|
|
314
|
+
): void => {
|
|
315
|
+
validateModuleDefinition(module);
|
|
316
|
+
const name = module.name;
|
|
317
|
+
const existing =
|
|
318
|
+
byName.get(name);
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
existing &&
|
|
322
|
+
existing !== module
|
|
323
|
+
) {
|
|
324
|
+
throw new ModuleDependencyError(
|
|
325
|
+
`BCP Modules: duplicate module name "${name}" refers to different definitions.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
byName.set(name, module);
|
|
329
|
+
|
|
330
|
+
if (visited.has(name)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (visiting.has(name)) {
|
|
334
|
+
throw new ModuleDependencyError(
|
|
335
|
+
`BCP Modules: circular module dependency detected: ${[
|
|
336
|
+
...path,
|
|
337
|
+
name,
|
|
338
|
+
].join(" -> ")}.`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
visiting.add(name);
|
|
343
|
+
for (
|
|
344
|
+
const dependency
|
|
345
|
+
of module.imports ?? []
|
|
346
|
+
) {
|
|
347
|
+
visit(
|
|
348
|
+
dependency,
|
|
349
|
+
[
|
|
350
|
+
...path,
|
|
351
|
+
name,
|
|
352
|
+
]
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
visiting.delete(name);
|
|
356
|
+
visited.add(name);
|
|
357
|
+
order.push(module);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
for (const root of roots) {
|
|
361
|
+
visit(root, []);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return order;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function createModuleLifecycleResource(
|
|
368
|
+
module: ModuleDefinition<any>,
|
|
369
|
+
shared: {
|
|
370
|
+
container: ServiceContainer;
|
|
371
|
+
services: PluginServiceRegistry;
|
|
372
|
+
hooks: PluginHookBus;
|
|
373
|
+
}
|
|
374
|
+
): DeploymentResource {
|
|
375
|
+
const config =
|
|
376
|
+
parseModuleConfig(
|
|
377
|
+
module.schema,
|
|
378
|
+
module.config
|
|
379
|
+
);
|
|
380
|
+
const context:
|
|
381
|
+
ModuleContext<any> = {
|
|
382
|
+
name:
|
|
383
|
+
module.name,
|
|
384
|
+
config,
|
|
385
|
+
container:
|
|
386
|
+
shared.container,
|
|
387
|
+
services:
|
|
388
|
+
shared.services,
|
|
389
|
+
hooks:
|
|
390
|
+
shared.hooks,
|
|
391
|
+
};
|
|
392
|
+
let setupComplete = false;
|
|
393
|
+
let disposed = false;
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
name:
|
|
397
|
+
`bcp:module:${module.name}`,
|
|
398
|
+
|
|
399
|
+
async start() {
|
|
400
|
+
try {
|
|
401
|
+
if (!setupComplete) {
|
|
402
|
+
await runModuleHook(
|
|
403
|
+
module,
|
|
404
|
+
"setup",
|
|
405
|
+
module.setup,
|
|
406
|
+
context
|
|
407
|
+
);
|
|
408
|
+
setupComplete = true;
|
|
409
|
+
}
|
|
410
|
+
await runModuleHook(
|
|
411
|
+
module,
|
|
412
|
+
"start",
|
|
413
|
+
module.start,
|
|
414
|
+
context
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (!disposed) {
|
|
418
|
+
disposed = true;
|
|
419
|
+
try {
|
|
420
|
+
await runModuleHook(
|
|
421
|
+
module,
|
|
422
|
+
"dispose",
|
|
423
|
+
module.dispose,
|
|
424
|
+
context
|
|
425
|
+
);
|
|
426
|
+
} catch (disposeError) {
|
|
427
|
+
throw new AggregateError(
|
|
428
|
+
[
|
|
429
|
+
error,
|
|
430
|
+
disposeError,
|
|
431
|
+
],
|
|
432
|
+
`BCP Modules: startup and cleanup failed for module "${module.name}".`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
ready() {
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async stop() {
|
|
445
|
+
const errors:
|
|
446
|
+
unknown[] = [];
|
|
447
|
+
try {
|
|
448
|
+
await runModuleHook(
|
|
449
|
+
module,
|
|
450
|
+
"stop",
|
|
451
|
+
module.stop,
|
|
452
|
+
context
|
|
453
|
+
);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
errors.push(error);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!disposed) {
|
|
459
|
+
disposed = true;
|
|
460
|
+
try {
|
|
461
|
+
await runModuleHook(
|
|
462
|
+
module,
|
|
463
|
+
"dispose",
|
|
464
|
+
module.dispose,
|
|
465
|
+
context
|
|
466
|
+
);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
errors.push(error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (errors.length > 0) {
|
|
473
|
+
throw new AggregateError(
|
|
474
|
+
errors,
|
|
475
|
+
`BCP Modules: shutdown failed for module "${module.name}".`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
diagnostics() {
|
|
481
|
+
return {
|
|
482
|
+
name:
|
|
483
|
+
module.name,
|
|
484
|
+
...(module.version
|
|
485
|
+
? {
|
|
486
|
+
version:
|
|
487
|
+
module.version,
|
|
488
|
+
}
|
|
489
|
+
: {}),
|
|
490
|
+
imports:
|
|
491
|
+
(
|
|
492
|
+
module.imports ?? []
|
|
493
|
+
).map(
|
|
494
|
+
dependency =>
|
|
495
|
+
dependency.name
|
|
496
|
+
),
|
|
497
|
+
exports:
|
|
498
|
+
(
|
|
499
|
+
module.exports ?? []
|
|
500
|
+
).map(
|
|
501
|
+
token =>
|
|
502
|
+
token.description
|
|
503
|
+
),
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function runModuleHook<TConfig>(
|
|
510
|
+
module: ModuleDefinition<TConfig>,
|
|
511
|
+
phase: string,
|
|
512
|
+
hook:
|
|
513
|
+
| ((
|
|
514
|
+
context: ModuleContext<TConfig>
|
|
515
|
+
) => void | Promise<void>)
|
|
516
|
+
| undefined,
|
|
517
|
+
context: ModuleContext<TConfig>
|
|
518
|
+
): Promise<void> {
|
|
519
|
+
if (!hook) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
await hook(context);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new ModuleLifecycleError(
|
|
526
|
+
module.name,
|
|
527
|
+
phase,
|
|
528
|
+
error
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function validateModuleDefinition(
|
|
534
|
+
module: ModuleDefinition<any>
|
|
535
|
+
): void {
|
|
536
|
+
if (
|
|
537
|
+
!module ||
|
|
538
|
+
typeof module !== "object"
|
|
539
|
+
) {
|
|
540
|
+
throw new TypeError(
|
|
541
|
+
"BCP Modules: module must be an object."
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (module.kind !== MODULE_V2_KIND) {
|
|
545
|
+
throw new TypeError(
|
|
546
|
+
`BCP Modules: module kind must be "${MODULE_V2_KIND}". Use defineModule().`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
normalizeName(
|
|
550
|
+
module.name,
|
|
551
|
+
"module name"
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
for (
|
|
555
|
+
const [label, value]
|
|
556
|
+
of [
|
|
557
|
+
["imports", module.imports],
|
|
558
|
+
["providers", module.providers],
|
|
559
|
+
["exports", module.exports],
|
|
560
|
+
["plugins", module.plugins],
|
|
561
|
+
["resources", module.resources],
|
|
562
|
+
] as const
|
|
563
|
+
) {
|
|
564
|
+
if (
|
|
565
|
+
value !== undefined &&
|
|
566
|
+
!Array.isArray(value)
|
|
567
|
+
) {
|
|
568
|
+
throw new TypeError(
|
|
569
|
+
`BCP Modules: ${label} must be an array.`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (
|
|
575
|
+
const [phase, hook]
|
|
576
|
+
of [
|
|
577
|
+
["setup", module.setup],
|
|
578
|
+
["start", module.start],
|
|
579
|
+
["stop", module.stop],
|
|
580
|
+
["dispose", module.dispose],
|
|
581
|
+
] as const
|
|
582
|
+
) {
|
|
583
|
+
if (
|
|
584
|
+
hook !== undefined &&
|
|
585
|
+
typeof hook !== "function"
|
|
586
|
+
) {
|
|
587
|
+
throw new TypeError(
|
|
588
|
+
`BCP Modules: ${phase} must be a function.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function validateModuleExports(
|
|
595
|
+
module: ModuleDefinition<any>
|
|
596
|
+
): void {
|
|
597
|
+
const available =
|
|
598
|
+
new Set<symbol>();
|
|
599
|
+
|
|
600
|
+
for (
|
|
601
|
+
const provider
|
|
602
|
+
of module.providers ?? []
|
|
603
|
+
) {
|
|
604
|
+
available.add(
|
|
605
|
+
provider.token.id
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
for (
|
|
609
|
+
const dependency
|
|
610
|
+
of module.imports ?? []
|
|
611
|
+
) {
|
|
612
|
+
for (
|
|
613
|
+
const token
|
|
614
|
+
of dependency.exports ?? []
|
|
615
|
+
) {
|
|
616
|
+
available.add(token.id);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
for (
|
|
621
|
+
const token
|
|
622
|
+
of module.exports ?? []
|
|
623
|
+
) {
|
|
624
|
+
if (!available.has(token.id)) {
|
|
625
|
+
throw new ModuleDependencyError(
|
|
626
|
+
`BCP Modules: module "${module.name}" exports "${token.description}" but does not provide or import it.`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function parseModuleConfig<TConfig>(
|
|
633
|
+
parser:
|
|
634
|
+
ModuleConfigParser<TConfig> |
|
|
635
|
+
undefined,
|
|
636
|
+
value: unknown
|
|
637
|
+
): TConfig {
|
|
638
|
+
if (!parser) {
|
|
639
|
+
return value as TConfig;
|
|
640
|
+
}
|
|
641
|
+
if (typeof parser === "function") {
|
|
642
|
+
return parser(value);
|
|
643
|
+
}
|
|
644
|
+
return parser.parse(value);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function normalizeName(
|
|
648
|
+
value: unknown,
|
|
649
|
+
label: string
|
|
650
|
+
): string {
|
|
651
|
+
if (typeof value !== "string") {
|
|
652
|
+
throw new TypeError(
|
|
653
|
+
`BCP Modules: ${label} must be a string.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const normalized =
|
|
657
|
+
value.trim();
|
|
658
|
+
if (!normalized) {
|
|
659
|
+
throw new TypeError(
|
|
660
|
+
`BCP Modules: ${label} cannot be empty.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return normalized;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function formatServiceKey(
|
|
667
|
+
key: PluginServiceKey
|
|
668
|
+
): string {
|
|
669
|
+
return typeof key === "symbol"
|
|
670
|
+
? key.description
|
|
671
|
+
? `Symbol(${key.description})`
|
|
672
|
+
: key.toString()
|
|
673
|
+
: key;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function formatError(
|
|
677
|
+
value: unknown
|
|
678
|
+
): string {
|
|
679
|
+
return value instanceof Error
|
|
680
|
+
? value.message
|
|
681
|
+
: String(value);
|
|
682
|
+
}
|