frontend-plugin-api: add support for declaring plugin info loaders
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added a new optional `info` option to `createFrontendPlugin` that lets you provide a loaders for different sources of metadata information about the plugin.
|
||||
|
||||
There are two available loaders. The first one is `info.packageJson`, which can be used to point to a `package.json` file for the plugin.This is recommended for any plugin that is defined within its own package, especially all plugins that are published to a package registry. Typical usage looks like this:
|
||||
|
||||
```ts
|
||||
export default createFrontendPlugin({
|
||||
pluginId: '...',
|
||||
info: {
|
||||
packageJson: () => import('../package.json'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The second loader is `info.manifest`, which can be used to point to an opaque plugin manifest. This **MUST ONLY** be used by plugins that intended for use within a single organization. Plugins that are published to an open package registry should **NOT** use this loader. The loader is useful to add additional internal metadata associated with the plugin, and it is up to the Backstage app to decide how these manifests are parsed and used. The default manifest parser in an app created with `createApp` from `@backstage/frontend-defaults` is able to parse the default `catalog-info.yaml` format and built-in fields such as `spec.owner`.
|
||||
|
||||
Typical usage looks like this:
|
||||
|
||||
```ts
|
||||
export default createFrontendPlugin({
|
||||
pluginId: '...',
|
||||
info: {
|
||||
manifest: () => import('../catalog-info.yaml'),
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -41,6 +41,8 @@ describe('convertLegacyPlugin', () => {
|
||||
"featureFlags": [],
|
||||
"getExtension": [Function],
|
||||
"id": "test",
|
||||
"info": [Function],
|
||||
"infoOptions": undefined,
|
||||
"routes": {},
|
||||
"toString": [Function],
|
||||
"version": "v1",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
FeatureFlagConfig,
|
||||
FrontendPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { OpaqueType } from '@internal/opaque';
|
||||
|
||||
export const OpaqueFrontendPlugin = OpaqueType.create<{
|
||||
@@ -27,6 +28,10 @@ export const OpaqueFrontendPlugin = OpaqueType.create<{
|
||||
readonly version: 'v1';
|
||||
readonly extensions: Extension<unknown>[];
|
||||
readonly featureFlags: FeatureFlagConfig[];
|
||||
readonly infoOptions?: {
|
||||
packageJson?: () => Promise<JsonObject>;
|
||||
manifest?: () => Promise<JsonObject>;
|
||||
};
|
||||
};
|
||||
}>({
|
||||
type: '@backstage/FrontendPlugin',
|
||||
|
||||
@@ -1322,14 +1322,38 @@ export interface FrontendPlugin<
|
||||
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
// (undocumented)
|
||||
readonly id: string;
|
||||
info(): Promise<FrontendPluginInfo>;
|
||||
// (undocumented)
|
||||
readonly routes: TRoutes;
|
||||
// (undocumented)
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition>;
|
||||
info?: FrontendPluginInfoOptions;
|
||||
}): FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface FrontendPluginInfo {
|
||||
description?: string;
|
||||
links?: Array<{
|
||||
title: string;
|
||||
url: string;
|
||||
}>;
|
||||
ownerEntityRefs?: string[];
|
||||
packageName?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FrontendPluginInfoOptions = {
|
||||
packageJson?: () => Promise<
|
||||
{
|
||||
name: string;
|
||||
} & JsonObject
|
||||
>;
|
||||
manifest?: () => Promise<JsonObject>;
|
||||
};
|
||||
|
||||
export { githubAuthApiRef };
|
||||
|
||||
export { gitlabAuthApiRef };
|
||||
@@ -1514,6 +1538,8 @@ export interface PluginOptions<
|
||||
// (undocumented)
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
// (undocumented)
|
||||
info?: FrontendPluginInfoOptions;
|
||||
// (undocumented)
|
||||
pluginId: TId;
|
||||
// (undocumented)
|
||||
routes?: TRoutes;
|
||||
|
||||
@@ -281,6 +281,60 @@ describe('createFrontendPlugin', () => {
|
||||
).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3");
|
||||
});
|
||||
|
||||
describe('info', () => {
|
||||
it('should support reading info from package.json', async () => {
|
||||
const plugin = createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
info: { packageJson: () => Promise.resolve({ name: '@test/test' }) },
|
||||
});
|
||||
|
||||
await expect((plugin as any).infoOptions?.packageJson()).resolves.toEqual(
|
||||
{ name: '@test/test' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should support reading info from actual package.json', async () => {
|
||||
const plugin = createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
info: { packageJson: () => import('../../package.json') },
|
||||
});
|
||||
|
||||
await expect(
|
||||
(plugin as any).infoOptions?.packageJson(),
|
||||
).resolves.toMatchObject({ name: '@backstage/frontend-plugin-api' });
|
||||
});
|
||||
|
||||
it('should support reading info from opaque manifest', async () => {
|
||||
const plugin = createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
info: { manifest: () => Promise.resolve({ owner: 'me' }) },
|
||||
});
|
||||
|
||||
await expect((plugin as any).infoOptions?.manifest()).resolves.toEqual({
|
||||
owner: 'me',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when trying to load info without installing in an app', async () => {
|
||||
await expect(
|
||||
createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
}).info(),
|
||||
).rejects.toThrow(
|
||||
"Attempted to load plugin info for plugin 'test', but the plugin instance is not installed in an app",
|
||||
);
|
||||
|
||||
await expect(
|
||||
createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
info: { packageJson: () => Promise.resolve({ name: '@test/test' }) },
|
||||
}).info(),
|
||||
).rejects.toThrow(
|
||||
"Attempted to load plugin info for plugin 'test', but the plugin instance is not installed in an app",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overrides', () => {
|
||||
it('should return a plugin instance with the correct namespace', () => {
|
||||
const plugin = createFrontendPlugin({
|
||||
|
||||
@@ -25,6 +25,67 @@ import {
|
||||
} from './resolveExtensionDefinition';
|
||||
import { AnyExternalRoutes, AnyRoutes, FeatureFlagConfig } from './types';
|
||||
import { MakeSortedExtensionsMap } from './MakeSortedExtensionsMap';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* Information about the plugin.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* This interface is intended to be extended via [module
|
||||
* augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
|
||||
* in order to add fields that are specific to each project.
|
||||
*
|
||||
* For example, one might add a `slackChannel` field that is read from the
|
||||
* opaque manifest file.
|
||||
*
|
||||
* See the options for `createApp` for more information about how to
|
||||
* customize the parsing of manifest files.
|
||||
*/
|
||||
export interface FrontendPluginInfo {
|
||||
/**
|
||||
* The name of the package that implements the plugin.
|
||||
*/
|
||||
packageName?: string;
|
||||
|
||||
/**
|
||||
* The version of the plugin, typically the version of the package.json file.
|
||||
*/
|
||||
version?: string;
|
||||
|
||||
/**
|
||||
* As short description of the plugin, typically the description field in
|
||||
* package.json.
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The owner entity references of the plugin.
|
||||
*/
|
||||
ownerEntityRefs?: string[];
|
||||
|
||||
/**
|
||||
* Links related to the plugin.
|
||||
*/
|
||||
links?: Array<{ title: string; url: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for providing information for a plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type FrontendPluginInfoOptions = {
|
||||
/**
|
||||
* A loader function for the package.json file for the plugin.
|
||||
*/
|
||||
packageJson?: () => Promise<{ name: string } & JsonObject>;
|
||||
/**
|
||||
* A loader function for an opaque manifest file for the plugin.
|
||||
*/
|
||||
manifest?: () => Promise<JsonObject>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface FrontendPlugin<
|
||||
@@ -38,9 +99,21 @@ export interface FrontendPlugin<
|
||||
readonly id: string;
|
||||
readonly routes: TRoutes;
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
|
||||
/**
|
||||
* Loads the plugin info.
|
||||
*/
|
||||
info(): Promise<FrontendPluginInfo>;
|
||||
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition>;
|
||||
|
||||
/**
|
||||
* Overrides to merge with the original plugin info. The merging is done for
|
||||
* each top-level field. Setting a field explicitly to `undefined` will
|
||||
* remove it.
|
||||
*/
|
||||
info?: FrontendPluginInfoOptions;
|
||||
}): FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
@@ -56,6 +129,7 @@ export interface PluginOptions<
|
||||
externalRoutes?: TExternalRoutes;
|
||||
extensions?: TExtensions;
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
info?: FrontendPluginInfoOptions;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -150,6 +224,14 @@ export function createFrontendPlugin<
|
||||
externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),
|
||||
featureFlags: options.featureFlags ?? [],
|
||||
extensions: extensions,
|
||||
infoOptions: options.info,
|
||||
|
||||
// This method is overridden when the plugin instance is installed in an app
|
||||
async info() {
|
||||
throw new Error(
|
||||
`Attempted to load plugin info for plugin '${pluginId}', but the plugin instance is not installed in an app`,
|
||||
);
|
||||
},
|
||||
getExtension(id) {
|
||||
const ext = extensionDefinitionsById.get(id);
|
||||
if (!ext) {
|
||||
@@ -178,6 +260,10 @@ export function createFrontendPlugin<
|
||||
...options,
|
||||
pluginId,
|
||||
extensions: [...nonOverriddenExtensions, ...overrides.extensions],
|
||||
info: {
|
||||
...options.info,
|
||||
...overrides.info,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,6 +40,8 @@ export {
|
||||
createFrontendPlugin,
|
||||
type FrontendPlugin,
|
||||
type PluginOptions,
|
||||
type FrontendPluginInfo,
|
||||
type FrontendPluginInfoOptions,
|
||||
} from './createFrontendPlugin';
|
||||
export {
|
||||
createFrontendModule,
|
||||
|
||||
Reference in New Issue
Block a user