@backstage/backend-app-api 1.0.1-next.0 → 1.0.1

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/dist/index.cjs.js CHANGED
@@ -1,920 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var backendPluginApi = require('@backstage/backend-plugin-api');
4
- var errors = require('@backstage/errors');
5
- var alpha = require('@backstage/backend-plugin-api/alpha');
3
+ var createSpecializedBackend = require('./wiring/createSpecializedBackend.cjs.js');
6
4
 
7
- class Node {
8
- constructor(value, consumes, provides) {
9
- this.value = value;
10
- this.consumes = consumes;
11
- this.provides = provides;
12
- }
13
- static from(input) {
14
- return new Node(
15
- input.value,
16
- input.consumes ? new Set(input.consumes) : /* @__PURE__ */ new Set(),
17
- input.provides ? new Set(input.provides) : /* @__PURE__ */ new Set()
18
- );
19
- }
20
- }
21
- class CycleKeySet {
22
- static from(nodes) {
23
- return new CycleKeySet(nodes);
24
- }
25
- #nodeIds;
26
- #cycleKeys;
27
- constructor(nodes) {
28
- this.#nodeIds = new Map(nodes.map((n, i) => [n.value, i]));
29
- this.#cycleKeys = /* @__PURE__ */ new Set();
30
- }
31
- tryAdd(path) {
32
- const cycleKey = this.#getCycleKey(path);
33
- if (this.#cycleKeys.has(cycleKey)) {
34
- return false;
35
- }
36
- this.#cycleKeys.add(cycleKey);
37
- return true;
38
- }
39
- #getCycleKey(path) {
40
- return path.map((n) => this.#nodeIds.get(n)).sort().join(",");
41
- }
42
- }
43
- class DependencyGraph {
44
- static fromMap(nodes) {
45
- return this.fromIterable(
46
- Object.entries(nodes).map(([key, node]) => ({
47
- value: String(key),
48
- ...node
49
- }))
50
- );
51
- }
52
- static fromIterable(nodeInputs) {
53
- const nodes = new Array();
54
- for (const nodeInput of nodeInputs) {
55
- nodes.push(Node.from(nodeInput));
56
- }
57
- return new DependencyGraph(nodes);
58
- }
59
- #nodes;
60
- #allProvided;
61
- constructor(nodes) {
62
- this.#nodes = nodes;
63
- this.#allProvided = /* @__PURE__ */ new Set();
64
- for (const node of this.#nodes.values()) {
65
- for (const produced of node.provides) {
66
- this.#allProvided.add(produced);
67
- }
68
- }
69
- }
70
- /**
71
- * Find all nodes that consume dependencies that are not provided by any other node.
72
- */
73
- findUnsatisfiedDeps() {
74
- const unsatisfiedDependencies = [];
75
- for (const node of this.#nodes.values()) {
76
- const unsatisfied = Array.from(node.consumes).filter(
77
- (id) => !this.#allProvided.has(id)
78
- );
79
- if (unsatisfied.length > 0) {
80
- unsatisfiedDependencies.push({ value: node.value, unsatisfied });
81
- }
82
- }
83
- return unsatisfiedDependencies;
84
- }
85
- /**
86
- * Detect the first circular dependency within the graph, returning the path of nodes that
87
- * form a cycle, with the same node as the first and last element of the array.
88
- */
89
- detectCircularDependency() {
90
- return this.detectCircularDependencies().next().value;
91
- }
92
- /**
93
- * Detect circular dependencies within the graph, returning the path of nodes that
94
- * form a cycle, with the same node as the first and last element of the array.
95
- */
96
- *detectCircularDependencies() {
97
- const cycleKeys = CycleKeySet.from(this.#nodes);
98
- for (const startNode of this.#nodes) {
99
- const visited = /* @__PURE__ */ new Set();
100
- const stack = new Array([
101
- startNode,
102
- [startNode.value]
103
- ]);
104
- while (stack.length > 0) {
105
- const [node, path] = stack.pop();
106
- if (visited.has(node)) {
107
- continue;
108
- }
109
- visited.add(node);
110
- for (const consumed of node.consumes) {
111
- const providerNodes = this.#nodes.filter(
112
- (other) => other.provides.has(consumed)
113
- );
114
- for (const provider of providerNodes) {
115
- if (provider === startNode) {
116
- if (cycleKeys.tryAdd(path)) {
117
- yield [...path, startNode.value];
118
- }
119
- break;
120
- }
121
- if (!visited.has(provider)) {
122
- stack.push([provider, [...path, provider.value]]);
123
- }
124
- }
125
- }
126
- }
127
- }
128
- return void 0;
129
- }
130
- /**
131
- * Traverses the dependency graph in topological order, calling the provided
132
- * function for each node and waiting for it to resolve.
133
- *
134
- * The nodes are traversed in parallel, but in such a way that no node is
135
- * visited before all of its dependencies.
136
- *
137
- * Dependencies of nodes that are not produced by any other nodes will be ignored.
138
- */
139
- async parallelTopologicalTraversal(fn) {
140
- const allProvided = this.#allProvided;
141
- const producedSoFar = /* @__PURE__ */ new Set();
142
- const waiting = new Set(this.#nodes.values());
143
- const visited = /* @__PURE__ */ new Set();
144
- const results = new Array();
145
- let inFlight = 0;
146
- async function processMoreNodes() {
147
- if (waiting.size === 0) {
148
- return;
149
- }
150
- const nodesToProcess = [];
151
- for (const node of waiting) {
152
- let ready = true;
153
- for (const consumed of node.consumes) {
154
- if (allProvided.has(consumed) && !producedSoFar.has(consumed)) {
155
- ready = false;
156
- continue;
157
- }
158
- }
159
- if (ready) {
160
- nodesToProcess.push(node);
161
- }
162
- }
163
- for (const node of nodesToProcess) {
164
- waiting.delete(node);
165
- }
166
- if (nodesToProcess.length === 0 && inFlight === 0) {
167
- throw new Error("Circular dependency detected");
168
- }
169
- await Promise.all(nodesToProcess.map(processNode));
170
- }
171
- async function processNode(node) {
172
- visited.add(node);
173
- inFlight += 1;
174
- const result = await fn(node.value);
175
- results.push(result);
176
- node.provides.forEach((produced) => producedSoFar.add(produced));
177
- inFlight -= 1;
178
- await processMoreNodes();
179
- }
180
- await processMoreNodes();
181
- return results;
182
- }
183
- }
184
5
 
185
- function toInternalServiceFactory(factory) {
186
- const f = factory;
187
- if (f.$$type !== "@backstage/BackendFeature") {
188
- throw new Error(`Invalid service factory, bad type '${f.$$type}'`);
189
- }
190
- if (f.version !== "v1") {
191
- throw new Error(`Invalid service factory, bad version '${f.version}'`);
192
- }
193
- return f;
194
- }
195
- function createPluginMetadataServiceFactory(pluginId) {
196
- return backendPluginApi.createServiceFactory({
197
- service: backendPluginApi.coreServices.pluginMetadata,
198
- deps: {},
199
- factory: async () => ({ getId: () => pluginId })
200
- });
201
- }
202
- class ServiceRegistry {
203
- static create(factories) {
204
- const factoryMap = /* @__PURE__ */ new Map();
205
- for (const factory of factories) {
206
- if (factory.service.multiton) {
207
- const existing = factoryMap.get(factory.service.id) ?? [];
208
- factoryMap.set(
209
- factory.service.id,
210
- existing.concat(toInternalServiceFactory(factory))
211
- );
212
- } else {
213
- factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
214
- }
215
- }
216
- const registry = new ServiceRegistry(factoryMap);
217
- registry.checkForCircularDeps();
218
- return registry;
219
- }
220
- #providedFactories;
221
- #loadedDefaultFactories;
222
- #implementations;
223
- #rootServiceImplementations = /* @__PURE__ */ new Map();
224
- #addedFactoryIds = /* @__PURE__ */ new Set();
225
- #instantiatedFactories = /* @__PURE__ */ new Set();
226
- constructor(factories) {
227
- this.#providedFactories = factories;
228
- this.#loadedDefaultFactories = /* @__PURE__ */ new Map();
229
- this.#implementations = /* @__PURE__ */ new Map();
230
- }
231
- #resolveFactory(ref, pluginId) {
232
- if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
233
- return Promise.resolve([
234
- toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId))
235
- ]);
236
- }
237
- let resolvedFactory = this.#providedFactories.get(ref.id);
238
- const { __defaultFactory: defaultFactory } = ref;
239
- if (!resolvedFactory && !defaultFactory) {
240
- return void 0;
241
- }
242
- if (!resolvedFactory) {
243
- let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory);
244
- if (!loadedFactory) {
245
- loadedFactory = Promise.resolve().then(() => defaultFactory(ref)).then(
246
- (f) => toInternalServiceFactory(typeof f === "function" ? f() : f)
247
- );
248
- this.#loadedDefaultFactories.set(defaultFactory, loadedFactory);
249
- }
250
- resolvedFactory = loadedFactory.then(
251
- (factory) => [factory],
252
- (error) => {
253
- throw new Error(
254
- `Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
255
- error
256
- )}`
257
- );
258
- }
259
- );
260
- }
261
- return Promise.resolve(resolvedFactory);
262
- }
263
- #checkForMissingDeps(factory, pluginId) {
264
- const missingDeps = Object.values(factory.deps).filter((ref) => {
265
- if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
266
- return false;
267
- }
268
- if (this.#providedFactories.get(ref.id)) {
269
- return false;
270
- }
271
- if (ref.multiton) {
272
- return false;
273
- }
274
- return !ref.__defaultFactory;
275
- });
276
- if (missingDeps.length) {
277
- const missing = missingDeps.map((r) => `'${r.id}'`).join(", ");
278
- throw new Error(
279
- `Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`
280
- );
281
- }
282
- }
283
- checkForCircularDeps() {
284
- const graph = DependencyGraph.fromIterable(
285
- Array.from(this.#providedFactories).map(([serviceId, factories]) => ({
286
- value: serviceId,
287
- provides: [serviceId],
288
- consumes: factories.flatMap(
289
- (factory) => Object.values(factory.deps).map((d) => d.id)
290
- )
291
- }))
292
- );
293
- const circularDependencies = Array.from(graph.detectCircularDependencies());
294
- if (circularDependencies.length) {
295
- const cycles = circularDependencies.map((c) => c.map((id) => `'${id}'`).join(" -> ")).join("\n ");
296
- throw new errors.ConflictError(`Circular dependencies detected:
297
- ${cycles}`);
298
- }
299
- }
300
- add(factory) {
301
- const factoryId = factory.service.id;
302
- if (factoryId === backendPluginApi.coreServices.pluginMetadata.id) {
303
- throw new Error(
304
- `The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
305
- );
306
- }
307
- if (this.#instantiatedFactories.has(factoryId)) {
308
- throw new Error(
309
- `Unable to set service factory with id ${factoryId}, service has already been instantiated`
310
- );
311
- }
312
- if (factory.service.multiton) {
313
- const newFactories = (this.#providedFactories.get(factoryId) ?? []).concat(toInternalServiceFactory(factory));
314
- this.#providedFactories.set(factoryId, newFactories);
315
- } else {
316
- if (this.#addedFactoryIds.has(factoryId)) {
317
- throw new Error(
318
- `Duplicate service implementations provided for ${factoryId}`
319
- );
320
- }
321
- this.#addedFactoryIds.add(factoryId);
322
- this.#providedFactories.set(factoryId, [
323
- toInternalServiceFactory(factory)
324
- ]);
325
- }
326
- }
327
- async initializeEagerServicesWithScope(scope, pluginId = "root") {
328
- for (const [factory] of this.#providedFactories.values()) {
329
- if (factory.service.scope === scope) {
330
- if (scope === "root" && factory.initialization !== "lazy") {
331
- await this.get(factory.service, pluginId);
332
- } else if (scope === "plugin" && factory.initialization === "always") {
333
- await this.get(factory.service, pluginId);
334
- }
335
- }
336
- }
337
- }
338
- get(ref, pluginId) {
339
- this.#instantiatedFactories.add(ref.id);
340
- const resolvedFactory = this.#resolveFactory(ref, pluginId);
341
- if (!resolvedFactory) {
342
- return ref.multiton ? Promise.resolve([]) : void 0;
343
- }
344
- return resolvedFactory.then((factories) => {
345
- return Promise.all(
346
- factories.map((factory) => {
347
- if (factory.service.scope === "root") {
348
- let existing = this.#rootServiceImplementations.get(factory);
349
- if (!existing) {
350
- this.#checkForMissingDeps(factory, pluginId);
351
- const rootDeps = new Array();
352
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
353
- if (serviceRef.scope !== "root") {
354
- throw new Error(
355
- `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`
356
- );
357
- }
358
- const target = this.get(serviceRef, pluginId);
359
- rootDeps.push(target.then((impl) => [name, impl]));
360
- }
361
- existing = Promise.all(rootDeps).then(
362
- (entries) => factory.factory(Object.fromEntries(entries), void 0)
363
- );
364
- this.#rootServiceImplementations.set(factory, existing);
365
- }
366
- return existing;
367
- }
368
- let implementation = this.#implementations.get(factory);
369
- if (!implementation) {
370
- this.#checkForMissingDeps(factory, pluginId);
371
- const rootDeps = new Array();
372
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
373
- if (serviceRef.scope === "root") {
374
- const target = this.get(serviceRef, pluginId);
375
- rootDeps.push(target.then((impl) => [name, impl]));
376
- }
377
- }
378
- implementation = {
379
- context: Promise.all(rootDeps).then(
380
- (entries) => factory.createRootContext?.(Object.fromEntries(entries))
381
- ).catch((error) => {
382
- const cause = errors.stringifyError(error);
383
- throw new Error(
384
- `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
385
- );
386
- }),
387
- byPlugin: /* @__PURE__ */ new Map()
388
- };
389
- this.#implementations.set(factory, implementation);
390
- }
391
- let result = implementation.byPlugin.get(pluginId);
392
- if (!result) {
393
- const allDeps = new Array();
394
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
395
- const target = this.get(serviceRef, pluginId);
396
- allDeps.push(target.then((impl) => [name, impl]));
397
- }
398
- result = implementation.context.then(
399
- (context) => Promise.all(allDeps).then(
400
- (entries) => factory.factory(Object.fromEntries(entries), context)
401
- )
402
- ).catch((error) => {
403
- const cause = errors.stringifyError(error);
404
- throw new Error(
405
- `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`
406
- );
407
- });
408
- implementation.byPlugin.set(pluginId, result);
409
- }
410
- return result;
411
- })
412
- );
413
- }).then((results) => ref.multiton ? results : results[0]);
414
- }
415
- }
416
6
 
417
- const LOGGER_INTERVAL_MAX = 6e4;
418
- function joinIds(ids) {
419
- return [...ids].map((id) => `'${id}'`).join(", ");
420
- }
421
- function createInitializationLogger(pluginIds, rootLogger) {
422
- const logger = rootLogger?.child({ type: "initialization" });
423
- const starting = new Set(pluginIds);
424
- const started = /* @__PURE__ */ new Set();
425
- logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`);
426
- const getInitStatus = () => {
427
- let status = "";
428
- if (started.size > 0) {
429
- status = `, newly initialized: ${joinIds(started)}`;
430
- started.clear();
431
- }
432
- if (starting.size > 0) {
433
- status += `, still initializing: ${joinIds(starting)}`;
434
- }
435
- return status;
436
- };
437
- let interval = 1e3;
438
- let prevInterval = 0;
439
- let timeout;
440
- const onTimeout = () => {
441
- logger?.info(`Plugin initialization in progress${getInitStatus()}`);
442
- const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
443
- prevInterval = interval;
444
- interval = nextInterval;
445
- timeout = setTimeout(onTimeout, nextInterval);
446
- };
447
- timeout = setTimeout(onTimeout, interval);
448
- return {
449
- onPluginStarted(pluginId) {
450
- starting.delete(pluginId);
451
- started.add(pluginId);
452
- },
453
- onPluginFailed(pluginId) {
454
- starting.delete(pluginId);
455
- const status = starting.size > 0 ? `, waiting for ${starting.size} other plugins to finish before shutting down the process` : "";
456
- logger?.error(
457
- `Plugin '${pluginId}' thew an error during startup${status}`
458
- );
459
- },
460
- onAllStarted() {
461
- logger?.info(`Plugin initialization complete${getInitStatus()}`);
462
- if (timeout) {
463
- clearTimeout(timeout);
464
- timeout = void 0;
465
- }
466
- }
467
- };
468
- }
469
-
470
- function unwrapFeature(feature) {
471
- if ("$$type" in feature) {
472
- return feature;
473
- }
474
- if ("default" in feature) {
475
- return feature.default;
476
- }
477
- return feature;
478
- }
479
-
480
- const instanceRegistry = new class InstanceRegistry {
481
- #registered = false;
482
- #instances = /* @__PURE__ */ new Set();
483
- register(instance) {
484
- if (!this.#registered) {
485
- this.#registered = true;
486
- process.addListener("SIGTERM", this.#exitHandler);
487
- process.addListener("SIGINT", this.#exitHandler);
488
- process.addListener("beforeExit", this.#exitHandler);
489
- }
490
- this.#instances.add(instance);
491
- }
492
- unregister(instance) {
493
- this.#instances.delete(instance);
494
- }
495
- #exitHandler = async () => {
496
- try {
497
- const results = await Promise.allSettled(
498
- Array.from(this.#instances).map((b) => b.stop())
499
- );
500
- const errors = results.flatMap(
501
- (r) => r.status === "rejected" ? [r.reason] : []
502
- );
503
- if (errors.length > 0) {
504
- for (const error of errors) {
505
- console.error(error);
506
- }
507
- process.exit(1);
508
- } else {
509
- process.exit(0);
510
- }
511
- } catch (error) {
512
- console.error(error);
513
- process.exit(1);
514
- }
515
- };
516
- }();
517
- class BackendInitializer {
518
- #startPromise;
519
- #stopPromise;
520
- #registrations = new Array();
521
- #extensionPoints = /* @__PURE__ */ new Map();
522
- #serviceRegistry;
523
- #registeredFeatures = new Array();
524
- #registeredFeatureLoaders = new Array();
525
- constructor(defaultApiFactories) {
526
- this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
527
- }
528
- async #getInitDeps(deps, pluginId, moduleId) {
529
- const result = /* @__PURE__ */ new Map();
530
- const missingRefs = /* @__PURE__ */ new Set();
531
- for (const [name, ref] of Object.entries(deps)) {
532
- const ep = this.#extensionPoints.get(ref.id);
533
- if (ep) {
534
- if (ep.pluginId !== pluginId) {
535
- throw new Error(
536
- `Illegal dependency: Module '${moduleId}' for plugin '${pluginId}' attempted to depend on extension point '${ref.id}' for plugin '${ep.pluginId}'. Extension points can only be used within their plugin's scope.`
537
- );
538
- }
539
- result.set(name, ep.impl);
540
- } else {
541
- const impl = await this.#serviceRegistry.get(
542
- ref,
543
- pluginId
544
- );
545
- if (impl) {
546
- result.set(name, impl);
547
- } else {
548
- missingRefs.add(ref);
549
- }
550
- }
551
- }
552
- if (missingRefs.size > 0) {
553
- const missing = Array.from(missingRefs).join(", ");
554
- const target = moduleId ? `module '${moduleId}' for plugin '${pluginId}'` : `plugin '${pluginId}'`;
555
- throw new Error(
556
- `Service or extension point dependencies of ${target} are missing for the following ref(s): ${missing}`
557
- );
558
- }
559
- return Object.fromEntries(result);
560
- }
561
- add(feature) {
562
- if (this.#startPromise) {
563
- throw new Error("feature can not be added after the backend has started");
564
- }
565
- this.#registeredFeatures.push(Promise.resolve(feature));
566
- }
567
- #addFeature(feature) {
568
- if (isServiceFactory(feature)) {
569
- this.#serviceRegistry.add(feature);
570
- } else if (isBackendFeatureLoader(feature)) {
571
- this.#registeredFeatureLoaders.push(feature);
572
- } else if (isBackendRegistrations(feature)) {
573
- this.#registrations.push(feature);
574
- } else {
575
- throw new Error(
576
- `Failed to add feature, invalid feature ${JSON.stringify(feature)}`
577
- );
578
- }
579
- }
580
- async start() {
581
- if (this.#startPromise) {
582
- throw new Error("Backend has already started");
583
- }
584
- if (this.#stopPromise) {
585
- throw new Error("Backend has already stopped");
586
- }
587
- instanceRegistry.register(this);
588
- this.#startPromise = this.#doStart();
589
- await this.#startPromise;
590
- }
591
- async #doStart() {
592
- this.#serviceRegistry.checkForCircularDeps();
593
- for (const feature of this.#registeredFeatures) {
594
- this.#addFeature(await feature);
595
- }
596
- const featureDiscovery = await this.#serviceRegistry.get(
597
- // TODO: Let's leave this in place and remove it once the deprecated service is removed. We can do that post-1.0 since it's alpha
598
- alpha.featureDiscoveryServiceRef,
599
- "root"
600
- );
601
- if (featureDiscovery) {
602
- const { features } = await featureDiscovery.getBackendFeatures();
603
- for (const feature of features) {
604
- this.#addFeature(unwrapFeature(feature));
605
- }
606
- this.#serviceRegistry.checkForCircularDeps();
607
- }
608
- await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
609
- await this.#serviceRegistry.initializeEagerServicesWithScope("root");
610
- const pluginInits = /* @__PURE__ */ new Map();
611
- const moduleInits = /* @__PURE__ */ new Map();
612
- for (const feature of this.#registrations) {
613
- for (const r of feature.getRegistrations()) {
614
- const provides = /* @__PURE__ */ new Set();
615
- if (r.type === "plugin" || r.type === "module") {
616
- for (const [extRef, extImpl] of r.extensionPoints) {
617
- if (this.#extensionPoints.has(extRef.id)) {
618
- throw new Error(
619
- `ExtensionPoint with ID '${extRef.id}' is already registered`
620
- );
621
- }
622
- this.#extensionPoints.set(extRef.id, {
623
- impl: extImpl,
624
- pluginId: r.pluginId
625
- });
626
- provides.add(extRef);
627
- }
628
- }
629
- if (r.type === "plugin") {
630
- if (pluginInits.has(r.pluginId)) {
631
- throw new Error(`Plugin '${r.pluginId}' is already registered`);
632
- }
633
- pluginInits.set(r.pluginId, {
634
- provides,
635
- consumes: new Set(Object.values(r.init.deps)),
636
- init: r.init
637
- });
638
- } else if (r.type === "module") {
639
- let modules = moduleInits.get(r.pluginId);
640
- if (!modules) {
641
- modules = /* @__PURE__ */ new Map();
642
- moduleInits.set(r.pluginId, modules);
643
- }
644
- if (modules.has(r.moduleId)) {
645
- throw new Error(
646
- `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`
647
- );
648
- }
649
- modules.set(r.moduleId, {
650
- provides,
651
- consumes: new Set(Object.values(r.init.deps)),
652
- init: r.init
653
- });
654
- } else {
655
- throw new Error(`Invalid registration type '${r.type}'`);
656
- }
657
- }
658
- }
659
- const allPluginIds = [...pluginInits.keys()];
660
- const initLogger = createInitializationLogger(
661
- allPluginIds,
662
- await this.#serviceRegistry.get(backendPluginApi.coreServices.rootLogger, "root")
663
- );
664
- const results = await Promise.allSettled(
665
- allPluginIds.map(async (pluginId) => {
666
- try {
667
- await this.#serviceRegistry.initializeEagerServicesWithScope(
668
- "plugin",
669
- pluginId
670
- );
671
- const modules = moduleInits.get(pluginId);
672
- if (modules) {
673
- const tree = DependencyGraph.fromIterable(
674
- Array.from(modules).map(([moduleId, moduleInit]) => ({
675
- value: { moduleId, moduleInit },
676
- // Relationships are reversed at this point since we're only interested in the extension points.
677
- // If a modules provides extension point A we want it to be initialized AFTER all modules
678
- // that depend on extension point A, so that they can provide their extensions.
679
- consumes: Array.from(moduleInit.provides).map((p) => p.id),
680
- provides: Array.from(moduleInit.consumes).map((c) => c.id)
681
- }))
682
- );
683
- const circular = tree.detectCircularDependency();
684
- if (circular) {
685
- throw new errors.ConflictError(
686
- `Circular dependency detected for modules of plugin '${pluginId}', ${circular.map(({ moduleId }) => `'${moduleId}'`).join(" -> ")}`
687
- );
688
- }
689
- await tree.parallelTopologicalTraversal(
690
- async ({ moduleId, moduleInit }) => {
691
- const moduleDeps = await this.#getInitDeps(
692
- moduleInit.init.deps,
693
- pluginId,
694
- moduleId
695
- );
696
- await moduleInit.init.func(moduleDeps).catch((error) => {
697
- throw new errors.ForwardedError(
698
- `Module '${moduleId}' for plugin '${pluginId}' startup failed`,
699
- error
700
- );
701
- });
702
- }
703
- );
704
- }
705
- const pluginInit = pluginInits.get(pluginId);
706
- if (pluginInit) {
707
- const pluginDeps = await this.#getInitDeps(
708
- pluginInit.init.deps,
709
- pluginId
710
- );
711
- await pluginInit.init.func(pluginDeps).catch((error) => {
712
- throw new errors.ForwardedError(
713
- `Plugin '${pluginId}' startup failed`,
714
- error
715
- );
716
- });
717
- }
718
- initLogger.onPluginStarted(pluginId);
719
- const lifecycleService2 = await this.#getPluginLifecycleImpl(pluginId);
720
- await lifecycleService2.startup();
721
- } catch (error) {
722
- initLogger.onPluginFailed(pluginId);
723
- throw error;
724
- }
725
- })
726
- );
727
- const initErrors = results.flatMap(
728
- (r) => r.status === "rejected" ? [r.reason] : []
729
- );
730
- if (initErrors.length === 1) {
731
- throw initErrors[0];
732
- } else if (initErrors.length > 1) {
733
- throw new AggregateError(initErrors, "Backend startup failed");
734
- }
735
- const lifecycleService = await this.#getRootLifecycleImpl();
736
- await lifecycleService.startup();
737
- initLogger.onAllStarted();
738
- if (process.env.NODE_ENV !== "test") {
739
- const rootLogger = await this.#serviceRegistry.get(
740
- backendPluginApi.coreServices.rootLogger,
741
- "root"
742
- );
743
- process.on("unhandledRejection", (reason) => {
744
- rootLogger?.child({ type: "unhandledRejection" })?.error("Unhandled rejection", reason);
745
- });
746
- process.on("uncaughtException", (error) => {
747
- rootLogger?.child({ type: "uncaughtException" })?.error("Uncaught exception", error);
748
- });
749
- }
750
- }
751
- // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit
752
- async stop() {
753
- instanceRegistry.unregister(this);
754
- if (!this.#stopPromise) {
755
- this.#stopPromise = this.#doStop();
756
- }
757
- await this.#stopPromise;
758
- }
759
- async #doStop() {
760
- if (!this.#startPromise) {
761
- return;
762
- }
763
- try {
764
- await this.#startPromise;
765
- } catch (error) {
766
- }
767
- const lifecycleService = await this.#getRootLifecycleImpl();
768
- await lifecycleService.shutdown();
769
- }
770
- // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
771
- async #getRootLifecycleImpl() {
772
- const lifecycleService = await this.#serviceRegistry.get(
773
- backendPluginApi.coreServices.rootLifecycle,
774
- "root"
775
- );
776
- const service = lifecycleService;
777
- if (service && typeof service.startup === "function" && typeof service.shutdown === "function") {
778
- return service;
779
- }
780
- throw new Error("Unexpected root lifecycle service implementation");
781
- }
782
- async #getPluginLifecycleImpl(pluginId) {
783
- const lifecycleService = await this.#serviceRegistry.get(
784
- backendPluginApi.coreServices.lifecycle,
785
- pluginId
786
- );
787
- const service = lifecycleService;
788
- if (service && typeof service.startup === "function") {
789
- return service;
790
- }
791
- throw new Error("Unexpected plugin lifecycle service implementation");
792
- }
793
- async #applyBackendFeatureLoaders(loaders) {
794
- for (const loader of loaders) {
795
- const deps = /* @__PURE__ */ new Map();
796
- const missingRefs = /* @__PURE__ */ new Set();
797
- for (const [name, ref] of Object.entries(loader.deps ?? {})) {
798
- if (ref.scope !== "root") {
799
- throw new Error(
800
- `Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`
801
- );
802
- }
803
- const impl = await this.#serviceRegistry.get(
804
- ref,
805
- "root"
806
- );
807
- if (impl) {
808
- deps.set(name, impl);
809
- } else {
810
- missingRefs.add(ref);
811
- }
812
- }
813
- if (missingRefs.size > 0) {
814
- const missing = Array.from(missingRefs).join(", ");
815
- throw new Error(
816
- `No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`
817
- );
818
- }
819
- const result = await loader.loader(Object.fromEntries(deps)).then((features) => features.map(unwrapFeature)).catch((error) => {
820
- throw new errors.ForwardedError(
821
- `Feature loader ${loader.description} failed`,
822
- error
823
- );
824
- });
825
- let didAddServiceFactory = false;
826
- const newLoaders = new Array();
827
- for await (const feature of result) {
828
- if (isBackendFeatureLoader(feature)) {
829
- newLoaders.push(feature);
830
- } else {
831
- didAddServiceFactory ||= isServiceFactory(feature);
832
- this.#addFeature(feature);
833
- }
834
- }
835
- if (didAddServiceFactory) {
836
- this.#serviceRegistry.checkForCircularDeps();
837
- }
838
- if (newLoaders.length > 0) {
839
- await this.#applyBackendFeatureLoaders(newLoaders);
840
- }
841
- }
842
- }
843
- }
844
- function toInternalBackendFeature(feature) {
845
- if (feature.$$type !== "@backstage/BackendFeature") {
846
- throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
847
- }
848
- const internal = feature;
849
- if (internal.version !== "v1") {
850
- throw new Error(
851
- `Invalid BackendFeature, bad version '${internal.version}'`
852
- );
853
- }
854
- return internal;
855
- }
856
- function isServiceFactory(feature) {
857
- const internal = toInternalBackendFeature(feature);
858
- if (internal.featureType === "service") {
859
- return true;
860
- }
861
- return "service" in internal;
862
- }
863
- function isBackendRegistrations(feature) {
864
- const internal = toInternalBackendFeature(feature);
865
- if (internal.featureType === "registrations") {
866
- return true;
867
- }
868
- return "getRegistrations" in internal;
869
- }
870
- function isBackendFeatureLoader(feature) {
871
- return toInternalBackendFeature(feature).featureType === "loader";
872
- }
873
-
874
- class BackstageBackend {
875
- #initializer;
876
- constructor(defaultServiceFactories) {
877
- this.#initializer = new BackendInitializer(defaultServiceFactories);
878
- }
879
- add(feature) {
880
- if (isPromise(feature)) {
881
- this.#initializer.add(feature.then((f) => unwrapFeature(f.default)));
882
- } else {
883
- this.#initializer.add(unwrapFeature(feature));
884
- }
885
- }
886
- async start() {
887
- await this.#initializer.start();
888
- }
889
- async stop() {
890
- await this.#initializer.stop();
891
- }
892
- }
893
- function isPromise(value) {
894
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
895
- }
896
-
897
- function createSpecializedBackend(options) {
898
- const exists = /* @__PURE__ */ new Set();
899
- const duplicates = /* @__PURE__ */ new Set();
900
- for (const { service } of options.defaultServiceFactories) {
901
- if (exists.has(service.id)) {
902
- duplicates.add(service.id);
903
- } else {
904
- exists.add(service.id);
905
- }
906
- }
907
- if (duplicates.size > 0) {
908
- const ids = Array.from(duplicates).join(", ");
909
- throw new Error(`Duplicate service implementations provided for ${ids}`);
910
- }
911
- if (exists.has(backendPluginApi.coreServices.pluginMetadata.id)) {
912
- throw new Error(
913
- `The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
914
- );
915
- }
916
- return new BackstageBackend(options.defaultServiceFactories);
917
- }
918
-
919
- exports.createSpecializedBackend = createSpecializedBackend;
7
+ exports.createSpecializedBackend = createSpecializedBackend.createSpecializedBackend;
920
8
  //# sourceMappingURL=index.cjs.js.map