@backstage/backend-app-api 0.8.1-next.1 → 0.8.1-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @backstage/backend-app-api
2
2
 
3
+ ## 0.8.1-next.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders.
8
+ - 93095ee: Make sure node-fetch is version 2.7.0 or greater
9
+ - 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref.
10
+ - 80a0737: Added configuration for the `packages` options to config schema
11
+ - Updated dependencies
12
+ - @backstage/backend-plugin-api@0.8.0-next.2
13
+ - @backstage/backend-common@0.23.4-next.2
14
+ - @backstage/config-loader@1.9.0-next.2
15
+ - @backstage/plugin-auth-node@0.5.0-next.2
16
+ - @backstage/plugin-permission-node@0.8.1-next.2
17
+ - @backstage/backend-tasks@0.5.28-next.2
18
+ - @backstage/cli-node@0.2.7
19
+ - @backstage/cli-common@0.1.14
20
+ - @backstage/config@1.2.0
21
+ - @backstage/errors@1.2.4
22
+ - @backstage/types@1.1.1
23
+
3
24
  ## 0.8.1-next.1
4
25
 
5
26
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/backend-app-api__alpha",
3
- "version": "0.8.1-next.1",
3
+ "version": "0.8.1-next.2",
4
4
  "main": "../dist/alpha.cjs.js",
5
5
  "types": "../dist/alpha.d.ts"
6
6
  }
package/config.d.ts CHANGED
@@ -287,6 +287,7 @@ export interface Config {
287
287
  }
288
288
  >;
289
289
  };
290
+ packages?: 'all' | { include?: string[]; exclude?: string[] };
290
291
  };
291
292
 
292
293
  /** Discovery options. */
package/dist/alpha.d.ts CHANGED
@@ -2,6 +2,6 @@ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
2
  import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha';
3
3
 
4
4
  /** @alpha */
5
- declare const featureDiscoveryServiceFactory: _backstage_backend_plugin_api.ServiceFactoryCompat<FeatureDiscoveryService, "root", undefined>;
5
+ declare const featureDiscoveryServiceFactory: _backstage_backend_plugin_api.ServiceFactoryCompat<FeatureDiscoveryService, "root", "singleton", undefined>;
6
6
 
7
7
  export { featureDiscoveryServiceFactory };
package/dist/index.cjs.js CHANGED
@@ -1243,7 +1243,19 @@ function createPluginMetadataServiceFactory(pluginId) {
1243
1243
  }
1244
1244
  class ServiceRegistry {
1245
1245
  static create(factories) {
1246
- const registry = new ServiceRegistry(factories);
1246
+ const factoryMap = /* @__PURE__ */ new Map();
1247
+ for (const factory of factories) {
1248
+ if (factory.service.multiton) {
1249
+ const existing = factoryMap.get(factory.service.id) ?? [];
1250
+ factoryMap.set(
1251
+ factory.service.id,
1252
+ existing.concat(toInternalServiceFactory(factory))
1253
+ );
1254
+ } else {
1255
+ factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
1256
+ }
1257
+ }
1258
+ const registry = new ServiceRegistry(factoryMap);
1247
1259
  registry.checkForCircularDeps();
1248
1260
  return registry;
1249
1261
  }
@@ -1254,17 +1266,15 @@ class ServiceRegistry {
1254
1266
  #addedFactoryIds = /* @__PURE__ */ new Set();
1255
1267
  #instantiatedFactories = /* @__PURE__ */ new Set();
1256
1268
  constructor(factories) {
1257
- this.#providedFactories = new Map(
1258
- factories.map((sf) => [sf.service.id, toInternalServiceFactory(sf)])
1259
- );
1269
+ this.#providedFactories = factories;
1260
1270
  this.#loadedDefaultFactories = /* @__PURE__ */ new Map();
1261
1271
  this.#implementations = /* @__PURE__ */ new Map();
1262
1272
  }
1263
1273
  #resolveFactory(ref, pluginId) {
1264
1274
  if (ref.id === backendPluginApi.coreServices.pluginMetadata.id) {
1265
- return Promise.resolve(
1275
+ return Promise.resolve([
1266
1276
  toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId))
1267
- );
1277
+ ]);
1268
1278
  }
1269
1279
  let resolvedFactory = this.#providedFactories.get(ref.id);
1270
1280
  const { __defaultFactory: defaultFactory } = ref;
@@ -1279,13 +1289,16 @@ class ServiceRegistry {
1279
1289
  );
1280
1290
  this.#loadedDefaultFactories.set(defaultFactory, loadedFactory);
1281
1291
  }
1282
- resolvedFactory = loadedFactory.catch((error) => {
1283
- throw new Error(
1284
- `Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
1285
- error
1286
- )}`
1287
- );
1288
- });
1292
+ resolvedFactory = loadedFactory.then(
1293
+ (factory) => [factory],
1294
+ (error) => {
1295
+ throw new Error(
1296
+ `Failed to instantiate service '${ref.id}' because the default factory loader threw an error, ${errors.stringifyError(
1297
+ error
1298
+ )}`
1299
+ );
1300
+ }
1301
+ );
1289
1302
  }
1290
1303
  return Promise.resolve(resolvedFactory);
1291
1304
  }
@@ -1297,6 +1310,9 @@ class ServiceRegistry {
1297
1310
  if (this.#providedFactories.get(ref.id)) {
1298
1311
  return false;
1299
1312
  }
1313
+ if (ref.multiton) {
1314
+ return false;
1315
+ }
1300
1316
  return !ref.__defaultFactory;
1301
1317
  });
1302
1318
  if (missingDeps.length) {
@@ -1308,13 +1324,13 @@ class ServiceRegistry {
1308
1324
  }
1309
1325
  checkForCircularDeps() {
1310
1326
  const graph = DependencyGraph.fromIterable(
1311
- Array.from(this.#providedFactories).map(
1312
- ([serviceId, serviceFactory]) => ({
1313
- value: serviceId,
1314
- provides: [serviceId],
1315
- consumes: Object.values(serviceFactory.deps).map((d) => d.id)
1316
- })
1317
- )
1327
+ Array.from(this.#providedFactories).map(([serviceId, factories]) => ({
1328
+ value: serviceId,
1329
+ provides: [serviceId],
1330
+ consumes: factories.flatMap(
1331
+ (factory) => Object.values(factory.deps).map((d) => d.id)
1332
+ )
1333
+ }))
1318
1334
  );
1319
1335
  const circularDependencies = Array.from(graph.detectCircularDependencies());
1320
1336
  if (circularDependencies.length) {
@@ -1330,21 +1346,28 @@ class ServiceRegistry {
1330
1346
  `The ${backendPluginApi.coreServices.pluginMetadata.id} service cannot be overridden`
1331
1347
  );
1332
1348
  }
1333
- if (this.#addedFactoryIds.has(factoryId)) {
1334
- throw new Error(
1335
- `Duplicate service implementations provided for ${factoryId}`
1336
- );
1337
- }
1338
1349
  if (this.#instantiatedFactories.has(factoryId)) {
1339
1350
  throw new Error(
1340
1351
  `Unable to set service factory with id ${factoryId}, service has already been instantiated`
1341
1352
  );
1342
1353
  }
1343
- this.#addedFactoryIds.add(factoryId);
1344
- this.#providedFactories.set(factoryId, toInternalServiceFactory(factory));
1354
+ if (factory.service.multiton) {
1355
+ const newFactories = (this.#providedFactories.get(factoryId) ?? []).concat(toInternalServiceFactory(factory));
1356
+ this.#providedFactories.set(factoryId, newFactories);
1357
+ } else {
1358
+ if (this.#addedFactoryIds.has(factoryId)) {
1359
+ throw new Error(
1360
+ `Duplicate service implementations provided for ${factoryId}`
1361
+ );
1362
+ }
1363
+ this.#addedFactoryIds.add(factoryId);
1364
+ this.#providedFactories.set(factoryId, [
1365
+ toInternalServiceFactory(factory)
1366
+ ]);
1367
+ }
1345
1368
  }
1346
1369
  async initializeEagerServicesWithScope(scope, pluginId = "root") {
1347
- for (const factory of this.#providedFactories.values()) {
1370
+ for (const [factory] of this.#providedFactories.values()) {
1348
1371
  if (factory.service.scope === scope) {
1349
1372
  if (scope === "root" && factory.initialization !== "lazy") {
1350
1373
  await this.get(factory.service, pluginId);
@@ -1356,72 +1379,80 @@ class ServiceRegistry {
1356
1379
  }
1357
1380
  get(ref, pluginId) {
1358
1381
  this.#instantiatedFactories.add(ref.id);
1359
- return this.#resolveFactory(ref, pluginId)?.then((factory) => {
1360
- if (factory.service.scope === "root") {
1361
- let existing = this.#rootServiceImplementations.get(factory);
1362
- if (!existing) {
1363
- this.#checkForMissingDeps(factory, pluginId);
1364
- const rootDeps = new Array();
1365
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
1366
- if (serviceRef.scope !== "root") {
1367
- throw new Error(
1368
- `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`
1382
+ const resolvedFactory = this.#resolveFactory(ref, pluginId);
1383
+ if (!resolvedFactory) {
1384
+ return ref.multiton ? Promise.resolve([]) : void 0;
1385
+ }
1386
+ return resolvedFactory.then((factories) => {
1387
+ return Promise.all(
1388
+ factories.map((factory) => {
1389
+ if (factory.service.scope === "root") {
1390
+ let existing = this.#rootServiceImplementations.get(factory);
1391
+ if (!existing) {
1392
+ this.#checkForMissingDeps(factory, pluginId);
1393
+ const rootDeps = new Array();
1394
+ for (const [name, serviceRef] of Object.entries(factory.deps)) {
1395
+ if (serviceRef.scope !== "root") {
1396
+ throw new Error(
1397
+ `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`
1398
+ );
1399
+ }
1400
+ const target = this.get(serviceRef, pluginId);
1401
+ rootDeps.push(target.then((impl) => [name, impl]));
1402
+ }
1403
+ existing = Promise.all(rootDeps).then(
1404
+ (entries) => factory.factory(Object.fromEntries(entries), void 0)
1369
1405
  );
1406
+ this.#rootServiceImplementations.set(factory, existing);
1370
1407
  }
1371
- const target = this.get(serviceRef, pluginId);
1372
- rootDeps.push(target.then((impl) => [name, impl]));
1408
+ return existing;
1373
1409
  }
1374
- existing = Promise.all(rootDeps).then(
1375
- (entries) => factory.factory(Object.fromEntries(entries), void 0)
1376
- );
1377
- this.#rootServiceImplementations.set(factory, existing);
1378
- }
1379
- return existing;
1380
- }
1381
- let implementation = this.#implementations.get(factory);
1382
- if (!implementation) {
1383
- this.#checkForMissingDeps(factory, pluginId);
1384
- const rootDeps = new Array();
1385
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
1386
- if (serviceRef.scope === "root") {
1387
- const target = this.get(serviceRef, pluginId);
1388
- rootDeps.push(target.then((impl) => [name, impl]));
1410
+ let implementation = this.#implementations.get(factory);
1411
+ if (!implementation) {
1412
+ this.#checkForMissingDeps(factory, pluginId);
1413
+ const rootDeps = new Array();
1414
+ for (const [name, serviceRef] of Object.entries(factory.deps)) {
1415
+ if (serviceRef.scope === "root") {
1416
+ const target = this.get(serviceRef, pluginId);
1417
+ rootDeps.push(target.then((impl) => [name, impl]));
1418
+ }
1419
+ }
1420
+ implementation = {
1421
+ context: Promise.all(rootDeps).then(
1422
+ (entries) => factory.createRootContext?.(Object.fromEntries(entries))
1423
+ ).catch((error) => {
1424
+ const cause = errors.stringifyError(error);
1425
+ throw new Error(
1426
+ `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
1427
+ );
1428
+ }),
1429
+ byPlugin: /* @__PURE__ */ new Map()
1430
+ };
1431
+ this.#implementations.set(factory, implementation);
1389
1432
  }
1390
- }
1391
- implementation = {
1392
- context: Promise.all(rootDeps).then(
1393
- (entries) => factory.createRootContext?.(Object.fromEntries(entries))
1394
- ).catch((error) => {
1395
- const cause = errors.stringifyError(error);
1396
- throw new Error(
1397
- `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`
1398
- );
1399
- }),
1400
- byPlugin: /* @__PURE__ */ new Map()
1401
- };
1402
- this.#implementations.set(factory, implementation);
1403
- }
1404
- let result = implementation.byPlugin.get(pluginId);
1405
- if (!result) {
1406
- const allDeps = new Array();
1407
- for (const [name, serviceRef] of Object.entries(factory.deps)) {
1408
- const target = this.get(serviceRef, pluginId);
1409
- allDeps.push(target.then((impl) => [name, impl]));
1410
- }
1411
- result = implementation.context.then(
1412
- (context) => Promise.all(allDeps).then(
1413
- (entries) => factory.factory(Object.fromEntries(entries), context)
1414
- )
1415
- ).catch((error) => {
1416
- const cause = errors.stringifyError(error);
1417
- throw new Error(
1418
- `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`
1419
- );
1420
- });
1421
- implementation.byPlugin.set(pluginId, result);
1422
- }
1423
- return result;
1424
- });
1433
+ let result = implementation.byPlugin.get(pluginId);
1434
+ if (!result) {
1435
+ const allDeps = new Array();
1436
+ for (const [name, serviceRef] of Object.entries(factory.deps)) {
1437
+ const target = this.get(serviceRef, pluginId);
1438
+ allDeps.push(target.then((impl) => [name, impl]));
1439
+ }
1440
+ result = implementation.context.then(
1441
+ (context) => Promise.all(allDeps).then(
1442
+ (entries) => factory.factory(Object.fromEntries(entries), context)
1443
+ )
1444
+ ).catch((error) => {
1445
+ const cause = errors.stringifyError(error);
1446
+ throw new Error(
1447
+ `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`
1448
+ );
1449
+ });
1450
+ implementation.byPlugin.set(pluginId, result);
1451
+ }
1452
+ return result;
1453
+ })
1454
+ );
1455
+ }).then((results) => ref.multiton ? results : results[0]);
1425
1456
  }
1426
1457
  }
1427
1458
 
@@ -1473,10 +1504,11 @@ function createInitializationLogger(pluginIds, rootLogger) {
1473
1504
 
1474
1505
  class BackendInitializer {
1475
1506
  #startPromise;
1476
- #features = new Array();
1507
+ #registrations = new Array();
1477
1508
  #extensionPoints = /* @__PURE__ */ new Map();
1478
1509
  #serviceRegistry;
1479
1510
  #registeredFeatures = new Array();
1511
+ #registeredFeatureLoaders = new Array();
1480
1512
  constructor(defaultApiFactories) {
1481
1513
  this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
1482
1514
  }
@@ -1519,20 +1551,12 @@ class BackendInitializer {
1519
1551
  this.#registeredFeatures.push(Promise.resolve(feature));
1520
1552
  }
1521
1553
  #addFeature(feature) {
1522
- if (feature.$$type !== "@backstage/BackendFeature") {
1523
- throw new Error(
1524
- `Failed to add feature, invalid type '${feature.$$type}'`
1525
- );
1526
- }
1527
1554
  if (isServiceFactory(feature)) {
1528
1555
  this.#serviceRegistry.add(feature);
1529
- } else if (isInternalBackendFeature(feature)) {
1530
- if (feature.version !== "v1") {
1531
- throw new Error(
1532
- `Failed to add feature, invalid version '${feature.version}'`
1533
- );
1534
- }
1535
- this.#features.push(feature);
1556
+ } else if (isBackendFeatureLoader(feature)) {
1557
+ this.#registeredFeatureLoaders.push(feature);
1558
+ } else if (isBackendRegistrations(feature)) {
1559
+ this.#registrations.push(feature);
1536
1560
  } else {
1537
1561
  throw new Error(
1538
1562
  `Failed to add feature, invalid feature ${JSON.stringify(feature)}`
@@ -1577,10 +1601,11 @@ class BackendInitializer {
1577
1601
  }
1578
1602
  this.#serviceRegistry.checkForCircularDeps();
1579
1603
  }
1604
+ await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
1580
1605
  await this.#serviceRegistry.initializeEagerServicesWithScope("root");
1581
1606
  const pluginInits = /* @__PURE__ */ new Map();
1582
1607
  const moduleInits = /* @__PURE__ */ new Map();
1583
- for (const feature of this.#features) {
1608
+ for (const feature of this.#registrations) {
1584
1609
  for (const r of feature.getRegistrations()) {
1585
1610
  const provides = /* @__PURE__ */ new Set();
1586
1611
  if (r.type === "plugin" || r.type === "module") {
@@ -1606,7 +1631,7 @@ class BackendInitializer {
1606
1631
  consumes: new Set(Object.values(r.init.deps)),
1607
1632
  init: r.init
1608
1633
  });
1609
- } else {
1634
+ } else if (r.type === "module") {
1610
1635
  let modules = moduleInits.get(r.pluginId);
1611
1636
  if (!modules) {
1612
1637
  modules = /* @__PURE__ */ new Map();
@@ -1622,6 +1647,8 @@ class BackendInitializer {
1622
1647
  consumes: new Set(Object.values(r.init.deps)),
1623
1648
  init: r.init
1624
1649
  });
1650
+ } else {
1651
+ throw new Error(`Invalid registration type '${r.type}'`);
1625
1652
  }
1626
1653
  }
1627
1654
  }
@@ -1738,12 +1765,85 @@ class BackendInitializer {
1738
1765
  }
1739
1766
  throw new Error("Unexpected plugin lifecycle service implementation");
1740
1767
  }
1768
+ async #applyBackendFeatureLoaders(loaders) {
1769
+ for (const loader of loaders) {
1770
+ const deps = /* @__PURE__ */ new Map();
1771
+ const missingRefs = /* @__PURE__ */ new Set();
1772
+ for (const [name, ref] of Object.entries(loader.deps ?? {})) {
1773
+ if (ref.scope !== "root") {
1774
+ throw new Error(
1775
+ `Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`
1776
+ );
1777
+ }
1778
+ const impl = await this.#serviceRegistry.get(
1779
+ ref,
1780
+ "root"
1781
+ );
1782
+ if (impl) {
1783
+ deps.set(name, impl);
1784
+ } else {
1785
+ missingRefs.add(ref);
1786
+ }
1787
+ }
1788
+ if (missingRefs.size > 0) {
1789
+ const missing = Array.from(missingRefs).join(", ");
1790
+ throw new Error(
1791
+ `No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`
1792
+ );
1793
+ }
1794
+ const result = await loader.loader(Object.fromEntries(deps)).catch((error) => {
1795
+ throw new errors.ForwardedError(
1796
+ `Feature loader ${loader.description} failed`,
1797
+ error
1798
+ );
1799
+ });
1800
+ let didAddServiceFactory = false;
1801
+ const newLoaders = new Array();
1802
+ for await (const feature of result) {
1803
+ if (isBackendFeatureLoader(feature)) {
1804
+ newLoaders.push(feature);
1805
+ } else {
1806
+ didAddServiceFactory ||= isServiceFactory(feature);
1807
+ this.#addFeature(feature);
1808
+ }
1809
+ }
1810
+ if (didAddServiceFactory) {
1811
+ this.#serviceRegistry.checkForCircularDeps();
1812
+ }
1813
+ if (newLoaders.length > 0) {
1814
+ await this.#applyBackendFeatureLoaders(newLoaders);
1815
+ }
1816
+ }
1817
+ }
1818
+ }
1819
+ function toInternalBackendFeature(feature) {
1820
+ if (feature.$$type !== "@backstage/BackendFeature") {
1821
+ throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
1822
+ }
1823
+ const internal = feature;
1824
+ if (internal.version !== "v1") {
1825
+ throw new Error(
1826
+ `Invalid BackendFeature, bad version '${internal.version}'`
1827
+ );
1828
+ }
1829
+ return internal;
1741
1830
  }
1742
1831
  function isServiceFactory(feature) {
1743
- return !!feature.service;
1832
+ const internal = toInternalBackendFeature(feature);
1833
+ if (internal.featureType === "service") {
1834
+ return true;
1835
+ }
1836
+ return "service" in internal;
1837
+ }
1838
+ function isBackendRegistrations(feature) {
1839
+ const internal = toInternalBackendFeature(feature);
1840
+ if (internal.featureType === "registrations") {
1841
+ return true;
1842
+ }
1843
+ return "getRegistrations" in internal;
1744
1844
  }
1745
- function isInternalBackendFeature(feature) {
1746
- return typeof feature.getRegistrations === "function";
1845
+ function isBackendFeatureLoader(feature) {
1846
+ return toInternalBackendFeature(feature).featureType === "loader";
1747
1847
  }
1748
1848
 
1749
1849
  class BackstageBackend {