frontend-plugin-api: introduce createExtensionInput + refactor

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-13 19:15:34 +02:00
parent 5995614eb0
commit 67b6106dd4
11 changed files with 263 additions and 204 deletions
+89 -78
View File
@@ -22,6 +22,17 @@ export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<any, any>;
};
// @public (undocumented)
export type AnyExtensionInputMap = {
[inputName in string]: ExtensionInput<
AnyExtensionDataMap,
{
optional: boolean;
singleton: boolean;
}
>;
};
// @public (undocumented)
export interface BackstagePlugin {
// (undocumented)
@@ -60,19 +71,14 @@ export const coreExtensionData: {
// @public (undocumented)
export function createApiExtension<
TConfig extends {},
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TInputs extends AnyExtensionInputMap,
>(
options: (
| {
api: AnyApiRef;
factory: (options: {
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => AnyApiFactory;
}
| {
@@ -87,12 +93,7 @@ export function createApiExtension<
// @public (undocumented)
export function createExtension<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
@@ -103,15 +104,28 @@ export function createExtensionDataRef<TData>(
id: string,
): ConfigurableExtensionDataRef<TData>;
// @public (undocumented)
export function createExtensionInput<
TExtensionData extends AnyExtensionDataMap,
TConfig extends {
singleton?: boolean;
optional?: boolean;
},
>(
extensionData: TExtensionData,
config?: TConfig,
): ExtensionInput<
TExtensionData,
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
}
>;
// @public (undocumented)
export interface CreateExtensionOptions<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TInputs extends AnyExtensionInputMap,
TConfig,
> {
// (undocumented)
@@ -123,9 +137,9 @@ export interface CreateExtensionOptions<
// (undocumented)
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TOutput>;
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}): void;
// (undocumented)
id: string;
@@ -150,12 +164,7 @@ export function createPageExtension<
TConfig extends {
path: string;
},
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TInputs extends AnyExtensionInputMap,
>(
options: (
| {
@@ -172,7 +181,7 @@ export function createPageExtension<
routeRef?: RouteRef;
loader: (options: {
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
},
): Extension<TConfig>;
@@ -198,19 +207,17 @@ export interface Extension<TConfig> {
// (undocumented)
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
bind(values: ExtensionInputValues<any>): void;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
inputs: Record<
string,
undefined | Record<string, unknown> | Array<Record<string, unknown>>
>;
}): void;
// (undocumented)
id: string;
// (undocumented)
inputs: Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>;
inputs: AnyExtensionInputMap;
// (undocumented)
output: AnyExtensionDataMap;
}
@@ -228,48 +235,6 @@ export interface ExtensionBoundaryProps {
source?: BackstagePlugin;
}
// @public (undocumented)
export type ExtensionDataBind<TMap extends AnyExtensionDataMap> = (
values: {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TMap[DataName]['T'];
} & {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TMap[DataName]['T'];
},
) => void;
// @public (undocumented)
export type ExtensionDataInputValues<
TInputs extends {
[name in string]: {
extensionData: AnyExtensionDataMap;
};
},
> = {
[InputName in keyof TInputs]: Array<
{
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TInputs[InputName]['extensionData'][DataName]['T'];
} & {
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TInputs[InputName]['extensionData'][DataName]['T'];
}
>;
};
// @public (undocumented)
export type ExtensionDataRef<
TData,
@@ -283,6 +248,52 @@ export type ExtensionDataRef<
$$type: '@backstage/ExtensionDataRef';
};
// @public
export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TExtensionData[DataName]['T'];
} & {
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TExtensionData[DataName]['T'];
};
// @public (undocumented)
export interface ExtensionInput<
TExtensionData extends AnyExtensionDataMap,
TConfig extends {
singleton: boolean;
optional: boolean;
},
> {
// (undocumented)
$$type: '@backstage/ExtensionInput';
// (undocumented)
config: TConfig;
// (undocumented)
extensionData: TExtensionData;
}
// @public
export type ExtensionInputValues<
TInputs extends {
[name in string]: ExtensionInput<any, any>;
},
> = {
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
? Array<Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>>
: false extends TInputs[InputName]['config']['optional']
? Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>
: Expand<
ExtensionDataValues<TInputs[InputName]['extensionData']> | undefined
>;
};
// @public (undocumented)
export type NavTarget = {
title: string;
@@ -17,23 +17,23 @@
import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api';
import { PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
ExtensionDataInputValues,
ExtensionInputValues,
createExtension,
coreExtensionData,
} from '../wiring';
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
/** @public */
export function createApiExtension<
TConfig extends {},
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TInputs extends AnyExtensionInputMap,
>(
options: (
| {
api: AnyApiRef;
factory: (options: {
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => AnyApiFactory;
}
| {
@@ -16,7 +16,7 @@
import React from 'react';
import { PortableSchema } from '../schema';
import { coreExtensionData } from '../wiring';
import { coreExtensionData, createExtensionInput } from '../wiring';
import { createPageExtension } from './createPageExtension';
describe('createPageExtension', () => {
@@ -54,9 +54,9 @@ describe('createPageExtension', () => {
disabled: true,
configSchema,
inputs: {
first: {
extensionData: { element: coreExtensionData.reactElement },
},
first: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
loader: async () => <div />,
}),
@@ -67,9 +67,9 @@ describe('createPageExtension', () => {
configSchema: expect.anything(),
disabled: true,
inputs: {
first: {
extensionData: { element: coreExtensionData.reactElement },
},
first: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
output: {
element: expect.anything(),
@@ -19,12 +19,12 @@ import React from 'react';
import { ExtensionBoundary } from '../components';
import { createSchemaFromZod, PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
coreExtensionData,
createExtension,
Extension,
ExtensionDataInputValues,
ExtensionInputValues,
} from '../wiring';
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
/**
* Helper for creating extensions for a routable React page component.
@@ -33,7 +33,7 @@ import {
*/
export function createPageExtension<
TConfig extends { path: string },
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TInputs extends AnyExtensionInputMap,
>(
options: (
| {
@@ -50,7 +50,7 @@ export function createPageExtension<
routeRef?: RouteRef;
loader: (options: {
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
},
): Extension<TConfig> {
@@ -16,6 +16,7 @@
import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { createExtensionInput } from './createExtensionInput';
const stringData = createExtensionDataRef<string>('string');
@@ -95,22 +96,16 @@ describe('createExtension', () => {
id: 'test',
at: 'root',
inputs: {
mixed: {
extensionData: {
required: stringData,
optional: stringData.optional(),
},
},
onlyRequired: {
extensionData: {
required: stringData,
},
},
onlyOptional: {
extensionData: {
optional: stringData.optional(),
},
},
mixed: createExtensionInput({
required: stringData,
optional: stringData.optional(),
}),
onlyRequired: createExtensionInput({
required: stringData,
}),
onlyOptional: createExtensionInput({
optional: stringData.optional(),
}),
},
output: {
foo: stringData,
@@ -15,51 +15,68 @@
*/
import { PortableSchema } from '../schema';
import { ExtensionDataRef } from './createExtensionDataRef';
import { ExtensionInput } from './createExtensionInput';
import { BackstagePlugin } from './createPlugin';
import { AnyExtensionDataMap, Extension } from './types';
/** @public */
export type ExtensionDataInputValues<
TInputs extends { [name in string]: { extensionData: AnyExtensionDataMap } },
> = {
[InputName in keyof TInputs]: Array<
{
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TInputs[InputName]['extensionData'][DataName]['T'];
} & {
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TInputs[InputName]['extensionData'][DataName]['T'];
}
>;
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<any, any>;
};
/** @public */
export type ExtensionDataBind<TMap extends AnyExtensionDataMap> = (
values: {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TMap[DataName]['T'];
} & {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TMap[DataName]['T'];
},
) => void;
export type AnyExtensionInputMap = {
[inputName in string]: ExtensionInput<
AnyExtensionDataMap,
{ optional: boolean; singleton: boolean }
>;
};
// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types?
/**
* Utility type to expand type aliases into their equivalent type.
* @ignore
*/
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
/**
* Converts an extension data map into the matching concrete data values type.
* @public
*/
export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TExtensionData[DataName]['T'];
} & {
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TExtensionData[DataName]['T'];
};
/**
* Converts an extension input map into the matching concrete input values type.
* @public
*/
export type ExtensionInputValues<
TInputs extends { [name in string]: ExtensionInput<any, any> },
> = {
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
? Array<Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>>
: false extends TInputs[InputName]['config']['optional']
? Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>
: Expand<
ExtensionDataValues<TInputs[InputName]['extensionData']> | undefined
>;
};
/** @public */
export interface CreateExtensionOptions<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TInputs extends AnyExtensionInputMap,
TConfig,
> {
id: string;
@@ -70,16 +87,36 @@ export interface CreateExtensionOptions<
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TOutput>;
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
inputs: Expand<ExtensionInputValues<TInputs>>;
}): void;
}
/** @public */
export interface Extension<TConfig> {
$$type: '@backstage/Extension';
id: string;
at: string;
disabled: boolean;
inputs: AnyExtensionInputMap;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind(values: ExtensionInputValues<any>): void;
config: TConfig;
inputs: Record<
string,
undefined | Record<string, unknown> | Array<Record<string, unknown>>
>;
}): void;
}
/** @public */
export function createExtension<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
@@ -94,7 +131,7 @@ export function createExtension<
return options.factory({
bind,
config,
inputs: inputs as ExtensionDataInputValues<TInputs>,
inputs: inputs as Expand<ExtensionInputValues<TInputs>>,
});
},
};
@@ -0,0 +1,55 @@
/*
* 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.
*/
import { AnyExtensionDataMap } from './createExtension';
/** @public */
export interface ExtensionInput<
TExtensionData extends AnyExtensionDataMap,
TConfig extends { singleton: boolean; optional: boolean },
> {
$$type: '@backstage/ExtensionInput';
extensionData: TExtensionData;
config: TConfig;
}
/** @public */
export function createExtensionInput<
TExtensionData extends AnyExtensionDataMap,
TConfig extends { singleton?: boolean; optional?: boolean },
>(
extensionData: TExtensionData,
config?: TConfig,
): ExtensionInput<
TExtensionData,
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
}
> {
return {
$$type: '@backstage/ExtensionInput',
extensionData,
config: {
singleton: Boolean(config?.singleton) as TConfig['singleton'] extends true
? true
: false,
optional: Boolean(config?.optional) as TConfig['optional'] extends true
? true
: false,
},
};
}
@@ -24,6 +24,7 @@ import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { coreExtensionData } from './coreExtensionData';
import { MockConfigApi } from '@backstage/test-utils';
import { createExtensionInput } from './createExtensionInput';
const nameExtensionDataRef = createExtensionDataRef<string>('name');
@@ -70,11 +71,9 @@ const TechDocsPage = createExtension({
id: 'plugin.techdocs.page',
at: 'test.output/names',
inputs: {
addons: {
extensionData: {
name: nameExtensionDataRef,
},
},
addons: createExtensionInput({
name: nameExtensionDataRef,
}),
},
output: {
name: nameExtensionDataRef,
@@ -88,11 +87,9 @@ const outputExtension = createExtension({
id: 'test.output',
at: 'root',
inputs: {
names: {
extensionData: {
name: nameExtensionDataRef,
},
},
names: createExtensionInput({
name: nameExtensionDataRef,
}),
},
output: {
element: coreExtensionData.reactElement,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Extension } from './types';
import { Extension } from './createExtension';
/** @public */
export interface PluginOptions {
@@ -17,10 +17,17 @@
export { coreExtensionData, type NavTarget } from './coreExtensionData';
export {
createExtension,
type Extension,
type CreateExtensionOptions,
type ExtensionDataBind,
type ExtensionDataInputValues,
type ExtensionDataValues,
type ExtensionInputValues,
type AnyExtensionInputMap,
type AnyExtensionDataMap,
} from './createExtension';
export {
createExtensionInput,
type ExtensionInput,
} from './createExtensionInput';
export {
createExtensionDataRef,
type ExtensionDataRef,
@@ -31,4 +38,3 @@ export {
type BackstagePlugin,
type PluginOptions,
} from './createPlugin';
export type { AnyExtensionDataMap, Extension } from './types';
@@ -1,42 +0,0 @@
/*
* 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.
*/
import { PortableSchema } from '../schema';
import { ExtensionDataBind } from './createExtension';
import { ExtensionDataRef } from './createExtensionDataRef';
import { BackstagePlugin } from './createPlugin';
/** @public */
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<any, any>;
};
/** @public */
export interface Extension<TConfig> {
$$type: '@backstage/Extension';
id: string;
at: string;
disabled: boolean;
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
}