Merge pull request #25903 from backstage/rugvip/presets
backend-*-api: add support for feature loaders
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features.
|
||||
|
||||
A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported.
|
||||
|
||||
Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated.
|
||||
|
||||
```ts
|
||||
const searchLoader = createBackendFeatureLoader({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
*loader({ config }) {
|
||||
// Example of a custom config flag to enable search
|
||||
if (config.getOptionalString('customFeatureToggle.search')) {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -193,7 +193,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
|
||||
backend.add(customAuth);
|
||||
```
|
||||
|
||||
Check out [the naming patterns article](../backend-system/architecture/07-naming-patterns.md) for what rules
|
||||
Check out [the naming patterns article](../backend-system/architecture/08-naming-patterns.md) for what rules
|
||||
apply regarding how to form valid IDs. In this example we also put the module
|
||||
declaration directly in `packages/backend/src/index.ts` but that's just for
|
||||
simplicity. You can place it anywhere you like, including in other packages, and
|
||||
|
||||
@@ -38,7 +38,7 @@ export const fooServiceRef = createServiceRef<FooService>({
|
||||
|
||||
The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime.
|
||||
|
||||
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page.
|
||||
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patterns surrounding services, see the [naming patterns](./08-naming-patterns.md#services) page.
|
||||
|
||||
A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Plugins provide the actual base features of a Backstage backend. Each plugin ope
|
||||
|
||||
## Defining a Plugin
|
||||
|
||||
Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns.
|
||||
Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./08-naming-patterns.md) about proper naming patterns.
|
||||
|
||||
```ts
|
||||
// plugins/example-backend/src/plugin.ts
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
id: feature-loaders
|
||||
title: Backend Feature Loaders
|
||||
sidebar_label: Feature Loaders
|
||||
# prettier-ignore
|
||||
description: Backend feature loaders
|
||||
---
|
||||
|
||||
Backend feature loaders are used to programmatically select and install features in a Backstage backend. They can service a wide range of use cases, such as enabling or disabling features based on static configuration, dynamically load features at runtime, or conditionally load features based on the state of a system.
|
||||
|
||||
Feature loaders are defined using the `createBackendFeatureLoader` function, exported by `@backstage/backend-plugin-api`. It accepts a `loader` function, as well as an optional `deps` object for declaring service dependencies. Unlike plugins and modules, feature loaders are limited to only depending on root-scoped services, but that still allows access to for example the [root config](../core-services/root-config.md) and [root logger](../core-services/root-logger.md) services.
|
||||
|
||||
The `loader` function can be defined in many different ways, with the main requirement being that it returns a list of `BackendFeature`s in some form. A backend feature is the kind of object that you can pass to `backend.add(...)`, for example services factories, plugins, modules, or even other feature loaders. The `loader` function can be synchronous or asynchronous, and can be defined as a generator function to allow for more complex logic.
|
||||
|
||||
## Examples
|
||||
|
||||
The following are a few example of how feature loaders can be used:
|
||||
|
||||
### Simple list of features
|
||||
|
||||
A feature loader can simply return a list of features to be installed:
|
||||
|
||||
```ts
|
||||
export default createBackendFeatureLoader({
|
||||
loader() {
|
||||
return [
|
||||
import('@backstage/plugin-search-backend/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-catalog/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-explore/alpha'),
|
||||
import('@backstage/plugin-search-backend-module-techdocs/alpha'),
|
||||
];
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
It can also encapsulate a collection of custom features:
|
||||
|
||||
```ts
|
||||
export default createBackendFeatureLoader({
|
||||
// Async loader is fine too
|
||||
async loader() {
|
||||
return [
|
||||
createBackendPlugin({
|
||||
...
|
||||
}),
|
||||
createBackendModule({
|
||||
...
|
||||
}),
|
||||
]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Conditional loading
|
||||
|
||||
A feature loader can access root-scoped services, such as the config service. This allows for conditional loading of features based on configuration. It is often convenient to use a generator function for this purpose:
|
||||
|
||||
```ts
|
||||
export default createBackendFeatureLoader({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
// The `*` in front of the function name makes it a generator function
|
||||
*loader({ config }) {
|
||||
// Example of a custom config flag to enable search
|
||||
if (config.getOptionalString('customFeatureToggle.search')) {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Dynamic logic
|
||||
|
||||
A feature loader can also be asynchronous, and for example fetch data from an external source to determine which features to load:
|
||||
|
||||
```ts
|
||||
export default createBackendFeatureLoader({
|
||||
// The `async *` in front of the function name makes it an async generator function.
|
||||
async *loader() {
|
||||
const localMetadata = await readMetadataFromDisk();
|
||||
|
||||
if (localMetadata.enableSearch) {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
|
||||
const remoteMetadata = await fetchMetadata();
|
||||
|
||||
if (remoteMetadata.enableExplore) {
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
}
|
||||
if (remoteMetadata.enableTechDocs) {
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -68,7 +68,7 @@ that's specific to your plugin. In the example above, the logger might tag
|
||||
messages with your plugin ID, and the HTTP router might prefix API routes with
|
||||
your plugin ID, depending on the implementation used.
|
||||
|
||||
See [the article on naming patterns](../architecture/07-naming-patterns.md) for
|
||||
See [the article on naming patterns](../architecture/08-naming-patterns.md) for
|
||||
details on how to best choose names/IDs for plugins and related backend system
|
||||
items.
|
||||
|
||||
@@ -124,7 +124,7 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({
|
||||
export { catalogModuleExampleCustomProcessor as default } from './module';
|
||||
```
|
||||
|
||||
See [the article on naming patterns](../architecture/07-naming-patterns.md) for
|
||||
See [the article on naming patterns](../architecture/08-naming-patterns.md) for
|
||||
details on how to best choose names/IDs for modules and related backend system
|
||||
items.
|
||||
|
||||
|
||||
@@ -394,6 +394,7 @@
|
||||
"backend-system/architecture/plugins",
|
||||
"backend-system/architecture/extension-points",
|
||||
"backend-system/architecture/modules",
|
||||
"backend-system/architecture/feature-loaders",
|
||||
"backend-system/architecture/naming-patterns"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
createBackendPlugin,
|
||||
createBackendModule,
|
||||
createExtensionPoint,
|
||||
createBackendFeatureLoader,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BackendInitializer } from './BackendInitializer';
|
||||
|
||||
@@ -105,6 +106,128 @@ describe('BackendInitializer', () => {
|
||||
expect(factory3).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should discover features from feature loader', async () => {
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
scope: 'root',
|
||||
});
|
||||
const ref2 = createServiceRef<{ x: number }>({
|
||||
id: '2',
|
||||
scope: 'plugin',
|
||||
});
|
||||
const factory1 = jest.fn();
|
||||
const factory2 = jest.fn();
|
||||
|
||||
const pluginInit = jest.fn(async () => {});
|
||||
const moduleInit = jest.fn(async () => {});
|
||||
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
init.add(
|
||||
createBackendFeatureLoader({
|
||||
*loader() {
|
||||
yield createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: factory1,
|
||||
});
|
||||
yield createServiceFactory({
|
||||
service: ref2,
|
||||
initialization: 'always',
|
||||
deps: {},
|
||||
factory: factory2,
|
||||
});
|
||||
yield createBackendPlugin({
|
||||
pluginId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
init: pluginInit,
|
||||
});
|
||||
},
|
||||
});
|
||||
yield createBackendModule({
|
||||
pluginId: 'test',
|
||||
moduleId: 'tester',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
init: moduleInit,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await init.start();
|
||||
|
||||
expect(factory1).toHaveBeenCalled();
|
||||
expect(factory2).toHaveBeenCalled();
|
||||
expect(pluginInit).toHaveBeenCalled();
|
||||
expect(moduleInit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should refuse to override already initialized services through loaded features', async () => {
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
scope: 'root',
|
||||
});
|
||||
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: () => ({ x: 1 }),
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendFeatureLoader({
|
||||
deps: { service1: ref1 },
|
||||
*loader() {
|
||||
yield createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: jest.fn(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(init.start()).rejects.toThrow(
|
||||
'Unable to set service factory with id 1, service has already been instantiated',
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse feature loader that depends on a plugin scoped service', async () => {
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
});
|
||||
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: () => ({ x: 1 }),
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendFeatureLoader({
|
||||
// @ts-expect-error
|
||||
deps: { service1: ref1 },
|
||||
*loader() {
|
||||
yield createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: jest.fn(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(init.start()).rejects.toThrow(
|
||||
/^Feature loaders can only depend on root scoped services, but 'service1' is scoped to 'plugin'. Offending loader is created at '.*'$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize plugin scoped services with eager initialization', async () => {
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
|
||||
@@ -25,8 +25,14 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServiceOrExtensionPoint } from './types';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type {
|
||||
InternalBackendFeature,
|
||||
InternalBackendFeatureLoader,
|
||||
InternalBackendRegistrations,
|
||||
} from '../../../backend-plugin-api/src/wiring/types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types';
|
||||
import { ForwardedError, ConflictError } from '@backstage/errors';
|
||||
import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import { DependencyGraph } from '../lib/DependencyGraph';
|
||||
@@ -44,10 +50,11 @@ export interface BackendRegisterInit {
|
||||
|
||||
export class BackendInitializer {
|
||||
#startPromise?: Promise<void>;
|
||||
#features = new Array<InternalBackendFeature>();
|
||||
#registrations = new Array<InternalBackendRegistrations>();
|
||||
#extensionPoints = new Map<string, { impl: unknown; pluginId: string }>();
|
||||
#serviceRegistry: ServiceRegistry;
|
||||
#registeredFeatures = new Array<Promise<BackendFeature>>();
|
||||
#registeredFeatureLoaders = new Array<InternalBackendFeatureLoader>();
|
||||
|
||||
constructor(defaultApiFactories: ServiceFactory[]) {
|
||||
this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
|
||||
@@ -101,21 +108,12 @@ export class BackendInitializer {
|
||||
}
|
||||
|
||||
#addFeature(feature: BackendFeature) {
|
||||
if (feature.$$type !== '@backstage/BackendFeature') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid type '${feature.$$type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isServiceFactory(feature)) {
|
||||
this.#serviceRegistry.add(feature);
|
||||
} else if (isInternalBackendFeature(feature)) {
|
||||
if (feature.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid version '${feature.version}'`,
|
||||
);
|
||||
}
|
||||
this.#features.push(feature);
|
||||
} else if (isBackendFeatureLoader(feature)) {
|
||||
this.#registeredFeatureLoaders.push(feature);
|
||||
} else if (isBackendRegistrations(feature)) {
|
||||
this.#registrations.push(feature);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid feature ${JSON.stringify(feature)}`,
|
||||
@@ -170,14 +168,16 @@ export class BackendInitializer {
|
||||
this.#serviceRegistry.checkForCircularDeps();
|
||||
}
|
||||
|
||||
await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
|
||||
|
||||
// Initialize all root scoped services
|
||||
await this.#serviceRegistry.initializeEagerServicesWithScope('root');
|
||||
|
||||
const pluginInits = new Map<string, BackendRegisterInit>();
|
||||
const moduleInits = new Map<string, Map<string, BackendRegisterInit>>();
|
||||
|
||||
// Enumerate all features
|
||||
for (const feature of this.#features) {
|
||||
// Enumerate all registrations
|
||||
for (const feature of this.#registrations) {
|
||||
for (const r of feature.getRegistrations()) {
|
||||
const provides = new Set<ExtensionPoint<unknown>>();
|
||||
|
||||
@@ -205,7 +205,7 @@ export class BackendInitializer {
|
||||
consumes: new Set(Object.values(r.init.deps)),
|
||||
init: r.init,
|
||||
});
|
||||
} else {
|
||||
} else if (r.type === 'module') {
|
||||
let modules = moduleInits.get(r.pluginId);
|
||||
if (!modules) {
|
||||
modules = new Map();
|
||||
@@ -221,6 +221,8 @@ export class BackendInitializer {
|
||||
consumes: new Set(Object.values(r.init.deps)),
|
||||
init: r.init,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Invalid registration type '${(r as any).type}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,16 +385,109 @@ export class BackendInitializer {
|
||||
|
||||
throw new Error('Unexpected plugin lifecycle service implementation');
|
||||
}
|
||||
|
||||
async #applyBackendFeatureLoaders(loaders: InternalBackendFeatureLoader[]) {
|
||||
for (const loader of loaders) {
|
||||
const deps = new Map<string, unknown>();
|
||||
const missingRefs = new Set<ServiceOrExtensionPoint>();
|
||||
|
||||
for (const [name, ref] of Object.entries(loader.deps ?? {})) {
|
||||
if (ref.scope !== 'root') {
|
||||
throw new Error(
|
||||
`Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`,
|
||||
);
|
||||
}
|
||||
const impl = await this.#serviceRegistry.get(
|
||||
ref as ServiceRef<unknown>,
|
||||
'root',
|
||||
);
|
||||
if (impl) {
|
||||
deps.set(name, impl);
|
||||
} else {
|
||||
missingRefs.add(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRefs.size > 0) {
|
||||
const missing = Array.from(missingRefs).join(', ');
|
||||
throw new Error(
|
||||
`No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await loader
|
||||
.loader(Object.fromEntries(deps))
|
||||
.catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Feature loader ${loader.description} failed`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
let didAddServiceFactory = false;
|
||||
const newLoaders = new Array<InternalBackendFeatureLoader>();
|
||||
|
||||
for await (const feature of result) {
|
||||
if (isBackendFeatureLoader(feature)) {
|
||||
newLoaders.push(feature);
|
||||
} else {
|
||||
didAddServiceFactory ||= isServiceFactory(feature);
|
||||
this.#addFeature(feature);
|
||||
}
|
||||
}
|
||||
|
||||
// Every time we add a new service factory we need to make sure that we don't have circular dependencies
|
||||
if (didAddServiceFactory) {
|
||||
this.#serviceRegistry.checkForCircularDeps();
|
||||
}
|
||||
|
||||
// Apply loaders recursively, depth-first
|
||||
if (newLoaders.length > 0) {
|
||||
await this.#applyBackendFeatureLoaders(newLoaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isServiceFactory(feature: BackendFeature): feature is ServiceFactory {
|
||||
return !!(feature as ServiceFactory).service;
|
||||
}
|
||||
|
||||
function isInternalBackendFeature(
|
||||
function toInternalBackendFeature(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalBackendFeature {
|
||||
return (
|
||||
typeof (feature as InternalBackendFeature).getRegistrations === 'function'
|
||||
);
|
||||
): InternalBackendFeature {
|
||||
if (feature.$$type !== '@backstage/BackendFeature') {
|
||||
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
|
||||
}
|
||||
const internal = feature as InternalBackendFeature;
|
||||
if (internal.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Invalid BackendFeature, bad version '${internal.version}'`,
|
||||
);
|
||||
}
|
||||
return internal;
|
||||
}
|
||||
|
||||
function isServiceFactory(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalServiceFactory {
|
||||
const internal = toInternalBackendFeature(feature);
|
||||
if (internal.featureType === 'service') {
|
||||
return true;
|
||||
}
|
||||
// Backwards compatibility for v1 registrations that use duck typing
|
||||
return 'service' in internal;
|
||||
}
|
||||
|
||||
function isBackendRegistrations(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalBackendRegistrations {
|
||||
const internal = toInternalBackendFeature(feature);
|
||||
if (internal.featureType === 'registrations') {
|
||||
return true;
|
||||
}
|
||||
// Backwards compatibility for v1 registrations that use duck typing
|
||||
return 'getRegistrations' in internal;
|
||||
}
|
||||
|
||||
function isBackendFeatureLoader(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalBackendFeatureLoader {
|
||||
return toInternalBackendFeature(feature).featureType === 'loader';
|
||||
}
|
||||
|
||||
@@ -213,6 +213,47 @@ export namespace coreServices {
|
||||
identity: ServiceRef<IdentityService, 'plugin'>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function createBackendFeatureLoader<
|
||||
TDeps extends {
|
||||
[name in string]: unknown;
|
||||
},
|
||||
>(options: CreateBackendFeatureLoaderOptions<TDeps>): BackendFeature;
|
||||
|
||||
// @public
|
||||
export interface CreateBackendFeatureLoaderOptions<
|
||||
TDeps extends {
|
||||
[name in string]: unknown;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
deps?: {
|
||||
[name in keyof TDeps]: ServiceRef<TDeps[name], 'root'>;
|
||||
};
|
||||
// (undocumented)
|
||||
loader(deps: TDeps):
|
||||
| Iterable<
|
||||
| BackendFeature
|
||||
| Promise<{
|
||||
default: BackendFeature;
|
||||
}>
|
||||
>
|
||||
| Promise<
|
||||
Iterable<
|
||||
| BackendFeature
|
||||
| Promise<{
|
||||
default: BackendFeature;
|
||||
}>
|
||||
>
|
||||
>
|
||||
| AsyncIterable<
|
||||
| BackendFeature
|
||||
| {
|
||||
default: BackendFeature;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function createBackendModule(
|
||||
options: CreateBackendModuleOptions,
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createServiceFactory, createServiceRef } from './types';
|
||||
import {
|
||||
InternalServiceFactory,
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
} from './types';
|
||||
|
||||
const ref = createServiceRef<string>({ id: 'x' });
|
||||
const rootDep = createServiceRef<number>({ id: 'y', scope: 'root' });
|
||||
@@ -36,6 +40,10 @@ describe('createServiceFactory', () => {
|
||||
},
|
||||
});
|
||||
expect(metaFactory).toEqual(expect.any(Function));
|
||||
expect(metaFactory().$$type).toBe('@backstage/BackendFeature');
|
||||
expect((metaFactory() as InternalServiceFactory).featureType).toBe(
|
||||
'service',
|
||||
);
|
||||
expect(metaFactory().service).toBe(ref);
|
||||
|
||||
// @ts-expect-error
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface InternalServiceFactory<
|
||||
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
|
||||
> extends ServiceFactory<TService, TScope> {
|
||||
version: 'v1';
|
||||
featureType: 'service';
|
||||
initialization?: 'always' | 'lazy';
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
createRootContext?(deps: { [key in string]: unknown }): Promise<unknown>;
|
||||
@@ -307,6 +308,7 @@ export function createServiceFactory<
|
||||
return {
|
||||
$$type: '@backstage/BackendFeature',
|
||||
version: 'v1',
|
||||
featureType: 'service',
|
||||
service: c.service,
|
||||
initialization: c.initialization,
|
||||
deps: c.deps,
|
||||
@@ -322,6 +324,7 @@ export function createServiceFactory<
|
||||
return {
|
||||
$$type: '@backstage/BackendFeature',
|
||||
version: 'v1',
|
||||
featureType: 'service',
|
||||
service: c.service,
|
||||
initialization: c.initialization,
|
||||
...('createRootContext' in c
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { coreServices, createServiceFactory } from '../services';
|
||||
import { InternalServiceFactory } from '../services/system/types';
|
||||
import { BackendFeature } from '../types';
|
||||
import { createBackendFeatureLoader } from './createBackendFeatureLoader';
|
||||
import { createBackendPlugin } from './createBackendPlugin';
|
||||
import { InternalBackendFeatureLoader } from './types';
|
||||
|
||||
describe('createBackendFeatureLoader', () => {
|
||||
it('should create an empty feature loader', () => {
|
||||
const result = createBackendFeatureLoader({
|
||||
deps: {},
|
||||
loader: () => [],
|
||||
}) as InternalBackendFeatureLoader;
|
||||
|
||||
expect(result.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(result.version).toEqual('v1');
|
||||
expect(result.featureType).toEqual('loader');
|
||||
expect(result.deps).toEqual({});
|
||||
expect(result.loader).toEqual(expect.any(Function));
|
||||
expect(result.description).toMatch(/^created at '.*'$/);
|
||||
});
|
||||
|
||||
it('should create a feature loader that loads a few features', async () => {
|
||||
const result = createBackendFeatureLoader({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
loader({ config: _unused }) {
|
||||
return [
|
||||
createBackendPlugin({
|
||||
pluginId: 'x',
|
||||
register() {},
|
||||
})(),
|
||||
createServiceFactory({
|
||||
service: coreServices.pluginMetadata,
|
||||
deps: {},
|
||||
factory: () => ({ getId: () => 'fake-id' }),
|
||||
})(),
|
||||
// Dynamic import format
|
||||
Promise.resolve({
|
||||
default: createBackendPlugin({
|
||||
pluginId: 'y',
|
||||
register() {},
|
||||
})(),
|
||||
}),
|
||||
];
|
||||
},
|
||||
}) as InternalBackendFeatureLoader;
|
||||
|
||||
expect(result.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(result.version).toEqual('v1');
|
||||
expect(result.featureType).toEqual('loader');
|
||||
|
||||
const results = await result.loader({ config: {} });
|
||||
expect(results.length).toBe(3);
|
||||
const [pluginX, serviceFactory, pluginY] = results;
|
||||
expect(pluginX.$$type).toBe('@backstage/BackendFeature');
|
||||
expect(serviceFactory.$$type).toBe('@backstage/BackendFeature');
|
||||
expect(pluginY.$$type).toBe('@backstage/BackendFeature');
|
||||
expect((serviceFactory as InternalServiceFactory).service.id).toBe(
|
||||
coreServices.pluginMetadata.id,
|
||||
);
|
||||
});
|
||||
|
||||
it('should support multiple output formats', async () => {
|
||||
const feature = createBackendPlugin({ pluginId: 'x', register() {} })();
|
||||
const dynamicFeature = Promise.resolve({ default: feature });
|
||||
|
||||
async function extractResult(f: BackendFeature) {
|
||||
const internal = f as InternalBackendFeatureLoader;
|
||||
return internal.loader({});
|
||||
}
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
loader() {
|
||||
return [feature];
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
async loader() {
|
||||
return [feature];
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
*loader() {
|
||||
yield feature;
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
async *loader() {
|
||||
yield feature;
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
loader() {
|
||||
return [dynamicFeature];
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
async loader() {
|
||||
return [dynamicFeature];
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
*loader() {
|
||||
yield dynamicFeature;
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
|
||||
await expect(
|
||||
extractResult(
|
||||
createBackendFeatureLoader({
|
||||
async *loader() {
|
||||
yield dynamicFeature;
|
||||
},
|
||||
}),
|
||||
),
|
||||
).resolves.toEqual([feature]);
|
||||
});
|
||||
|
||||
it('should only allow dependencies on root scoped services', () => {
|
||||
createBackendFeatureLoader({
|
||||
deps: {
|
||||
rootLogger: coreServices.rootLogger,
|
||||
},
|
||||
loader: () => [],
|
||||
});
|
||||
createBackendFeatureLoader({
|
||||
deps: {
|
||||
// @ts-expect-error
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
loader: () => [],
|
||||
});
|
||||
createBackendFeatureLoader({
|
||||
deps: {
|
||||
rootLogger: coreServices.rootLogger,
|
||||
// @ts-expect-error
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
loader: () => [],
|
||||
});
|
||||
expect('test').toBe('test');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServiceRef } from '../services';
|
||||
import { BackendFeature } from '../types';
|
||||
import { describeParentCallSite } from './describeParentCallSite';
|
||||
import { InternalBackendFeatureLoader } from './types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Options for creating a new backend feature loader.
|
||||
*/
|
||||
export interface CreateBackendFeatureLoaderOptions<
|
||||
TDeps extends { [name in string]: unknown },
|
||||
> {
|
||||
deps?: {
|
||||
[name in keyof TDeps]: ServiceRef<TDeps[name], 'root'>;
|
||||
};
|
||||
loader(
|
||||
deps: TDeps,
|
||||
):
|
||||
| Iterable<BackendFeature | Promise<{ default: BackendFeature }>>
|
||||
| Promise<Iterable<BackendFeature | Promise<{ default: BackendFeature }>>>
|
||||
| AsyncIterable<BackendFeature | { default: BackendFeature }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Creates a new backend feature loader.
|
||||
*/
|
||||
export function createBackendFeatureLoader<
|
||||
TDeps extends { [name in string]: unknown },
|
||||
>(options: CreateBackendFeatureLoaderOptions<TDeps>): BackendFeature {
|
||||
return {
|
||||
$$type: '@backstage/BackendFeature',
|
||||
version: 'v1',
|
||||
featureType: 'loader',
|
||||
description: `created at '${describeParentCallSite()}'`,
|
||||
deps: options.deps,
|
||||
async loader(deps: TDeps) {
|
||||
const it = await options.loader(deps);
|
||||
const result = new Array<BackendFeature>();
|
||||
for await (const item of it) {
|
||||
if ('$$type' in item && item.$$type === '@backstage/BackendFeature') {
|
||||
result.push(item);
|
||||
} else if ('default' in item) {
|
||||
result.push(item.default);
|
||||
} else {
|
||||
throw new Error(`Invalid item "${item}"`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
} as InternalBackendFeatureLoader;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createBackendModule } from './createBackendModule';
|
||||
import { InternalBackendRegistrations } from './types';
|
||||
|
||||
describe('createBackendModule', () => {
|
||||
it('should create a BackendModule', () => {
|
||||
const result = createBackendModule({
|
||||
pluginId: 'x',
|
||||
moduleId: 'y',
|
||||
register(r) {
|
||||
r.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
});
|
||||
|
||||
// legacy form
|
||||
const legacy = result() as unknown as InternalBackendRegistrations;
|
||||
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(legacy.version).toEqual('v1');
|
||||
expect(legacy.getRegistrations).toEqual(expect.any(Function));
|
||||
|
||||
// new form
|
||||
const module = result as unknown as InternalBackendRegistrations;
|
||||
expect(module.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(module.version).toEqual('v1');
|
||||
expect(module.getRegistrations).toEqual(expect.any(Function));
|
||||
expect(module.getRegistrations()).toEqual([
|
||||
{
|
||||
type: 'module',
|
||||
pluginId: 'x',
|
||||
moduleId: 'y',
|
||||
extensionPoints: [],
|
||||
init: {
|
||||
deps: expect.any(Object),
|
||||
func: expect.any(Function),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(module({ a: 'a' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackendFeatureCompat } from '../types';
|
||||
import {
|
||||
BackendModuleRegistrationPoints,
|
||||
InternalBackendModuleRegistration,
|
||||
InternalBackendPluginRegistration,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createBackendModule}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateBackendModuleOptions {
|
||||
/**
|
||||
* Should exactly match the `id` of the plugin that the module extends.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
pluginId: string;
|
||||
|
||||
/**
|
||||
* The ID of this module, used to identify the module and ensure that it is not installed twice.
|
||||
*/
|
||||
moduleId: string;
|
||||
register(reg: BackendModuleRegistrationPoints): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend module for a given plugin.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export function createBackendModule(
|
||||
options: CreateBackendModuleOptions,
|
||||
): BackendFeatureCompat {
|
||||
function getRegistrations() {
|
||||
const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
|
||||
[];
|
||||
let init: InternalBackendModuleRegistration['init'] | undefined = undefined;
|
||||
|
||||
options.register({
|
||||
registerExtensionPoint(ext, impl) {
|
||||
if (init) {
|
||||
throw new Error('registerExtensionPoint called after registerInit');
|
||||
}
|
||||
extensionPoints.push([ext, impl]);
|
||||
},
|
||||
registerInit(regInit) {
|
||||
if (init) {
|
||||
throw new Error('registerInit must only be called once');
|
||||
}
|
||||
init = {
|
||||
deps: regInit.deps,
|
||||
func: regInit.init,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (!init) {
|
||||
throw new Error(
|
||||
`registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'module',
|
||||
pluginId: options.pluginId,
|
||||
moduleId: options.moduleId,
|
||||
extensionPoints,
|
||||
init,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function backendFeatureCompatWrapper() {
|
||||
return backendFeatureCompatWrapper;
|
||||
}
|
||||
|
||||
Object.assign(backendFeatureCompatWrapper, {
|
||||
$$type: '@backstage/BackendFeature' as const,
|
||||
version: 'v1',
|
||||
getRegistrations,
|
||||
});
|
||||
|
||||
return backendFeatureCompatWrapper as BackendFeatureCompat;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createBackendPlugin } from './createBackendPlugin';
|
||||
import { InternalBackendRegistrations } from './types';
|
||||
|
||||
describe('createBackendPlugin', () => {
|
||||
it('should create a BackendPlugin', () => {
|
||||
const result = createBackendPlugin({
|
||||
pluginId: 'x',
|
||||
register(r) {
|
||||
r.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
});
|
||||
|
||||
// legacy form
|
||||
const legacy = result() as unknown as InternalBackendRegistrations;
|
||||
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(legacy.version).toEqual('v1');
|
||||
expect(legacy.getRegistrations).toEqual(expect.any(Function));
|
||||
|
||||
// new form
|
||||
const plugin = result as unknown as InternalBackendRegistrations;
|
||||
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(plugin.version).toEqual('v1');
|
||||
expect(plugin.getRegistrations).toEqual(expect.any(Function));
|
||||
expect(plugin.getRegistrations()).toEqual([
|
||||
{
|
||||
type: 'plugin',
|
||||
pluginId: 'x',
|
||||
extensionPoints: [],
|
||||
init: {
|
||||
deps: expect.any(Object),
|
||||
func: expect.any(Function),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(plugin({ a: 'a' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackendFeatureCompat } from '../types';
|
||||
import {
|
||||
BackendPluginRegistrationPoints,
|
||||
InternalBackendPluginRegistration,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createBackendPlugin}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateBackendPluginOptions {
|
||||
/**
|
||||
* The ID of this plugin.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
pluginId: string;
|
||||
register(reg: BackendPluginRegistrationPoints): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend plugin.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export function createBackendPlugin(
|
||||
options: CreateBackendPluginOptions,
|
||||
): BackendFeatureCompat {
|
||||
function getRegistrations() {
|
||||
const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
|
||||
[];
|
||||
let init: InternalBackendPluginRegistration['init'] | undefined = undefined;
|
||||
|
||||
options.register({
|
||||
registerExtensionPoint(ext, impl) {
|
||||
if (init) {
|
||||
throw new Error('registerExtensionPoint called after registerInit');
|
||||
}
|
||||
extensionPoints.push([ext, impl]);
|
||||
},
|
||||
registerInit(regInit) {
|
||||
if (init) {
|
||||
throw new Error('registerInit must only be called once');
|
||||
}
|
||||
init = {
|
||||
deps: regInit.deps,
|
||||
func: regInit.init,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (!init) {
|
||||
throw new Error(
|
||||
`registerInit was not called by register in ${options.pluginId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'plugin',
|
||||
pluginId: options.pluginId,
|
||||
extensionPoints,
|
||||
init,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function backendFeatureCompatWrapper() {
|
||||
return backendFeatureCompatWrapper;
|
||||
}
|
||||
|
||||
Object.assign(backendFeatureCompatWrapper, {
|
||||
$$type: '@backstage/BackendFeature' as const,
|
||||
version: 'v1',
|
||||
getRegistrations,
|
||||
});
|
||||
|
||||
return backendFeatureCompatWrapper as BackendFeatureCompat;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createExtensionPoint } from './createExtensionPoint';
|
||||
|
||||
describe('createExtensionPoint', () => {
|
||||
it('should create an ExtensionPoint', () => {
|
||||
const extensionPoint = createExtensionPoint({ id: 'x' });
|
||||
expect(extensionPoint).toBeDefined();
|
||||
expect(extensionPoint.id).toBe('x');
|
||||
expect(() => extensionPoint.T).not.toThrow();
|
||||
expect(String(extensionPoint)).toBe('extensionPoint{x}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExtensionPoint } from './types';
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createExtensionPoint}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateExtensionPointOptions {
|
||||
/**
|
||||
* The ID of this extension point.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend extension point.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points}
|
||||
*/
|
||||
export function createExtensionPoint<T>(
|
||||
options: CreateExtensionPointOptions,
|
||||
): ExtensionPoint<T> {
|
||||
return {
|
||||
id: options.id,
|
||||
get T(): T {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// Avoid throwing errors so tests asserting extensions' properties cannot be easily broken
|
||||
return null as T;
|
||||
}
|
||||
throw new Error(`tried to read ExtensionPoint.T of ${this}`);
|
||||
},
|
||||
toString() {
|
||||
return `extensionPoint{${options.id}}`;
|
||||
},
|
||||
$$type: '@backstage/ExtensionPoint',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Single re-export to avoid doing this import in multiple places, but still
|
||||
// avoid duplicate declarations because this one is a bit tricky.
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite';
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createBackendModule,
|
||||
createBackendPlugin,
|
||||
createExtensionPoint,
|
||||
} from './factories';
|
||||
import { InternalBackendFeature } from './types';
|
||||
|
||||
describe('createExtensionPoint', () => {
|
||||
it('should create an ExtensionPoint', () => {
|
||||
const extensionPoint = createExtensionPoint({ id: 'x' });
|
||||
expect(extensionPoint).toBeDefined();
|
||||
expect(extensionPoint.id).toBe('x');
|
||||
expect(() => extensionPoint.T).not.toThrow();
|
||||
expect(String(extensionPoint)).toBe('extensionPoint{x}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBackendPlugin', () => {
|
||||
it('should create a BackendPlugin', () => {
|
||||
const result = createBackendPlugin({
|
||||
pluginId: 'x',
|
||||
register(r) {
|
||||
r.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
});
|
||||
|
||||
// legacy form
|
||||
const legacy = result() as unknown as InternalBackendFeature;
|
||||
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(legacy.version).toEqual('v1');
|
||||
expect(legacy.getRegistrations).toEqual(expect.any(Function));
|
||||
|
||||
// new form
|
||||
const plugin = result as unknown as InternalBackendFeature;
|
||||
expect(plugin.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(plugin.version).toEqual('v1');
|
||||
expect(plugin.getRegistrations).toEqual(expect.any(Function));
|
||||
expect(plugin.getRegistrations()).toEqual([
|
||||
{
|
||||
type: 'plugin',
|
||||
pluginId: 'x',
|
||||
extensionPoints: [],
|
||||
init: {
|
||||
deps: expect.any(Object),
|
||||
func: expect.any(Function),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(plugin({ a: 'a' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBackendModule', () => {
|
||||
it('should create a BackendModule', () => {
|
||||
const result = createBackendModule({
|
||||
pluginId: 'x',
|
||||
moduleId: 'y',
|
||||
register(r) {
|
||||
r.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
});
|
||||
|
||||
// legacy form
|
||||
const legacy = result() as unknown as InternalBackendFeature;
|
||||
expect(legacy.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(legacy.version).toEqual('v1');
|
||||
expect(legacy.getRegistrations).toEqual(expect.any(Function));
|
||||
|
||||
// new form
|
||||
const module = result as unknown as InternalBackendFeature;
|
||||
expect(module.$$type).toEqual('@backstage/BackendFeature');
|
||||
expect(module.version).toEqual('v1');
|
||||
expect(module.getRegistrations).toEqual(expect.any(Function));
|
||||
expect(module.getRegistrations()).toEqual([
|
||||
{
|
||||
type: 'module',
|
||||
pluginId: 'x',
|
||||
moduleId: 'y',
|
||||
extensionPoints: [],
|
||||
init: {
|
||||
deps: expect.any(Object),
|
||||
func: expect.any(Function),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(module({ a: 'a' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackendFeatureCompat } from '../types';
|
||||
import {
|
||||
BackendModuleRegistrationPoints,
|
||||
BackendPluginRegistrationPoints,
|
||||
ExtensionPoint,
|
||||
InternalBackendModuleRegistration,
|
||||
InternalBackendPluginRegistration,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createExtensionPoint}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateExtensionPointOptions {
|
||||
/**
|
||||
* The ID of this extension point.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend extension point.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points}
|
||||
*/
|
||||
export function createExtensionPoint<T>(
|
||||
options: CreateExtensionPointOptions,
|
||||
): ExtensionPoint<T> {
|
||||
return {
|
||||
id: options.id,
|
||||
get T(): T {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// Avoid throwing errors so tests asserting extensions' properties cannot be easily broken
|
||||
return null as T;
|
||||
}
|
||||
throw new Error(`tried to read ExtensionPoint.T of ${this}`);
|
||||
},
|
||||
toString() {
|
||||
return `extensionPoint{${options.id}}`;
|
||||
},
|
||||
$$type: '@backstage/ExtensionPoint',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createBackendPlugin}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateBackendPluginOptions {
|
||||
/**
|
||||
* The ID of this plugin.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
pluginId: string;
|
||||
register(reg: BackendPluginRegistrationPoints): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend plugin.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export function createBackendPlugin(
|
||||
options: CreateBackendPluginOptions,
|
||||
): BackendFeatureCompat {
|
||||
function getRegistrations() {
|
||||
const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
|
||||
[];
|
||||
let init: InternalBackendPluginRegistration['init'] | undefined = undefined;
|
||||
|
||||
options.register({
|
||||
registerExtensionPoint(ext, impl) {
|
||||
if (init) {
|
||||
throw new Error('registerExtensionPoint called after registerInit');
|
||||
}
|
||||
extensionPoints.push([ext, impl]);
|
||||
},
|
||||
registerInit(regInit) {
|
||||
if (init) {
|
||||
throw new Error('registerInit must only be called once');
|
||||
}
|
||||
init = {
|
||||
deps: regInit.deps,
|
||||
func: regInit.init,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (!init) {
|
||||
throw new Error(
|
||||
`registerInit was not called by register in ${options.pluginId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'plugin',
|
||||
pluginId: options.pluginId,
|
||||
extensionPoints,
|
||||
init,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function backendFeatureCompatWrapper() {
|
||||
return backendFeatureCompatWrapper;
|
||||
}
|
||||
|
||||
Object.assign(backendFeatureCompatWrapper, {
|
||||
$$type: '@backstage/BackendFeature' as const,
|
||||
version: 'v1',
|
||||
getRegistrations,
|
||||
});
|
||||
|
||||
return backendFeatureCompatWrapper as BackendFeatureCompat;
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration options passed to {@link createBackendModule}.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export interface CreateBackendModuleOptions {
|
||||
/**
|
||||
* Should exactly match the `id` of the plugin that the module extends.
|
||||
*
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
pluginId: string;
|
||||
|
||||
/**
|
||||
* The ID of this module, used to identify the module and ensure that it is not installed twice.
|
||||
*/
|
||||
moduleId: string;
|
||||
register(reg: BackendModuleRegistrationPoints): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new backend module for a given plugin.
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules}
|
||||
* @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns}
|
||||
*/
|
||||
export function createBackendModule(
|
||||
options: CreateBackendModuleOptions,
|
||||
): BackendFeatureCompat {
|
||||
function getRegistrations() {
|
||||
const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] =
|
||||
[];
|
||||
let init: InternalBackendModuleRegistration['init'] | undefined = undefined;
|
||||
|
||||
options.register({
|
||||
registerExtensionPoint(ext, impl) {
|
||||
if (init) {
|
||||
throw new Error('registerExtensionPoint called after registerInit');
|
||||
}
|
||||
extensionPoints.push([ext, impl]);
|
||||
},
|
||||
registerInit(regInit) {
|
||||
if (init) {
|
||||
throw new Error('registerInit must only be called once');
|
||||
}
|
||||
init = {
|
||||
deps: regInit.deps,
|
||||
func: regInit.init,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (!init) {
|
||||
throw new Error(
|
||||
`registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'module',
|
||||
pluginId: options.pluginId,
|
||||
moduleId: options.moduleId,
|
||||
extensionPoints,
|
||||
init,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function backendFeatureCompatWrapper() {
|
||||
return backendFeatureCompatWrapper;
|
||||
}
|
||||
|
||||
Object.assign(backendFeatureCompatWrapper, {
|
||||
$$type: '@backstage/BackendFeature' as const,
|
||||
version: 'v1',
|
||||
getRegistrations,
|
||||
});
|
||||
|
||||
return backendFeatureCompatWrapper as BackendFeatureCompat;
|
||||
}
|
||||
@@ -14,17 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateBackendPluginOptions,
|
||||
CreateBackendModuleOptions,
|
||||
CreateExtensionPointOptions,
|
||||
} from './factories';
|
||||
import { type CreateBackendModuleOptions } from './createBackendModule';
|
||||
import { type CreateBackendPluginOptions } from './createBackendPlugin';
|
||||
import { type CreateExtensionPointOptions } from './createExtensionPoint';
|
||||
|
||||
export { createBackendModule } from './createBackendModule';
|
||||
export { createBackendPlugin } from './createBackendPlugin';
|
||||
export { createExtensionPoint } from './createExtensionPoint';
|
||||
export {
|
||||
createBackendModule,
|
||||
createBackendPlugin,
|
||||
createExtensionPoint,
|
||||
} from './factories';
|
||||
createBackendFeatureLoader,
|
||||
type CreateBackendFeatureLoaderOptions,
|
||||
} from './createBackendFeatureLoader';
|
||||
|
||||
export type {
|
||||
BackendModuleRegistrationPoints,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServiceRef } from '../services/system/types';
|
||||
import { InternalServiceFactory, ServiceRef } from '../services/system/types';
|
||||
import { BackendFeature } from '../types';
|
||||
|
||||
/**
|
||||
@@ -73,8 +73,9 @@ export interface BackendModuleRegistrationPoints {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalBackendFeature extends BackendFeature {
|
||||
export interface InternalBackendRegistrations extends BackendFeature {
|
||||
version: 'v1';
|
||||
featureType: 'registrations';
|
||||
getRegistrations(): Array<
|
||||
InternalBackendPluginRegistration | InternalBackendModuleRegistration
|
||||
>;
|
||||
@@ -102,3 +103,20 @@ export interface InternalBackendModuleRegistration {
|
||||
func(deps: Record<string, unknown>): Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface InternalBackendFeatureLoader extends BackendFeature {
|
||||
version: 'v1';
|
||||
featureType: 'loader';
|
||||
description: string;
|
||||
deps: Record<string, ServiceRef<unknown>>;
|
||||
loader(deps: Record<string, unknown>): Promise<BackendFeature[]>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalBackendFeature =
|
||||
| InternalBackendRegistrations
|
||||
| InternalBackendFeatureLoader
|
||||
| InternalServiceFactory;
|
||||
|
||||
@@ -36,7 +36,10 @@ import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
|
||||
import {
|
||||
InternalBackendFeature,
|
||||
InternalBackendRegistrations,
|
||||
} from '@backstage/backend-plugin-api/src/wiring/types';
|
||||
import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
|
||||
/** @public */
|
||||
@@ -94,7 +97,7 @@ function createPluginsForOrphanModules(features: Array<BackendFeature>) {
|
||||
const modulePluginIds = new Set<string>();
|
||||
|
||||
for (const feature of features) {
|
||||
if (isInternalBackendFeature(feature)) {
|
||||
if (isInternalBackendRegistrations(feature)) {
|
||||
const registrations = feature.getRegistrations();
|
||||
for (const registration of registrations) {
|
||||
if (registration.type === 'plugin') {
|
||||
@@ -137,18 +140,7 @@ function createExtensionPointTestModules(
|
||||
}
|
||||
|
||||
const registrations = features.flatMap(feature => {
|
||||
if (feature.$$type !== '@backstage/BackendFeature') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid type '${feature.$$type}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isInternalBackendFeature(feature)) {
|
||||
if (feature.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid version '${feature.version}'`,
|
||||
);
|
||||
}
|
||||
if (isInternalBackendRegistrations(feature)) {
|
||||
return feature.getRegistrations();
|
||||
}
|
||||
return [];
|
||||
@@ -361,10 +353,28 @@ function registerTestHooks() {
|
||||
|
||||
registerTestHooks();
|
||||
|
||||
function isInternalBackendFeature(
|
||||
function toInternalBackendFeature(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalBackendFeature {
|
||||
return (
|
||||
typeof (feature as InternalBackendFeature).getRegistrations === 'function'
|
||||
);
|
||||
): InternalBackendFeature {
|
||||
if (feature.$$type !== '@backstage/BackendFeature') {
|
||||
throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`);
|
||||
}
|
||||
const internal = feature as InternalBackendFeature;
|
||||
if (internal.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Invalid BackendFeature, bad version '${internal.version}'`,
|
||||
);
|
||||
}
|
||||
return internal;
|
||||
}
|
||||
|
||||
function isInternalBackendRegistrations(
|
||||
feature: BackendFeature,
|
||||
): feature is InternalBackendRegistrations {
|
||||
const internal = toInternalBackendFeature(feature);
|
||||
if (internal.featureType === 'registrations') {
|
||||
return true;
|
||||
}
|
||||
// Backwards compatibility for v1 registrations that use duck typing
|
||||
return 'getRegistrations' in internal;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,21 @@
|
||||
*/
|
||||
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { createBackendFeatureLoader } from '@backstage/backend-plugin-api';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
// An example of how to group together and load multiple features. You can also
|
||||
// access root-scoped services by adding `deps`.
|
||||
const searchLoader = createBackendFeatureLoader({
|
||||
*loader() {
|
||||
yield import('@backstage/plugin-search-backend/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-explore/alpha');
|
||||
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
|
||||
},
|
||||
});
|
||||
|
||||
backend.add(import('@backstage/plugin-auth-backend'));
|
||||
backend.add(import('./authModuleGithubProvider'));
|
||||
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
|
||||
@@ -36,13 +48,10 @@ backend.add(import('@backstage/plugin-permission-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-explore/alpha'));
|
||||
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
|
||||
backend.add(
|
||||
import('@backstage/plugin-catalog-backend-module-backstage-openapi'),
|
||||
);
|
||||
backend.add(import('@backstage/plugin-search-backend/alpha'));
|
||||
backend.add(searchLoader);
|
||||
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
|
||||
backend.add(import('@backstage/plugin-signals-backend'));
|
||||
backend.add(import('@backstage/plugin-notifications-backend'));
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo';
|
||||
|
||||
// NOTE: This function is also imported and used in backend code
|
||||
|
||||
/**
|
||||
* Internal helper that describes the location of the parent caller.
|
||||
* @internal
|
||||
|
||||
Reference in New Issue
Block a user