frontend-plugin-api: update extension data ref declaration to allow embedding of ID in type

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-07-20 12:47:09 +02:00
parent 2ab03460d5
commit 31bfc4417f
27 changed files with 204 additions and 77 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-search-react': patch
'@backstage/plugin-home': patch
---
Updated alpha definitions of extension data references.
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/frontend-plugin-api': patch
---
Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release.
For example, the following code:
```ts
export const myExtension = createExtensionDataRef<MyType>('my-plugin.my-data');
```
Should be updated to the following:
```ts
export const myExtension = createExtensionDataRef<MyType>().with({
id: 'my-plugin.my-data',
});
```
@@ -91,7 +91,9 @@ To create a new extension data reference to represent a type of shared extension
```ts
export const reactElementExtensionDataRef =
createExtensionDataRef<React.JSX.Element>('my-plugin.reactElement');
createExtensionDataRef<React.JSX.Element>().with({
id: 'my-plugin.reactElement',
});
```
The `ExtensionDataRef` can then be used to describe an output property of the extension. This will enforce typing on the return value of the extension factory:
@@ -98,9 +98,9 @@ export interface SearchResultItemExtensionData {
}
export const searchResultItemExtensionDataRef =
createExtensionDataRef<SearchResultItemExtensionData>(
'search.search-result-item',
);
createExtensionDataRef<SearchResultItemExtensionData>().with({
id: 'search.search-result-item',
});
```
#### Grouped Extension Data
@@ -109,8 +109,12 @@ This way of defining extension data is similar to the standalone way, but it use
```ts
export const coreExtensionData = {
reactElement: createExtensionDataRef<ReactElement>('core.react-element'),
routePath: createExtensionDataRef<string>('core.route-path'),
reactElement: createExtensionDataRef<ReactElement>().with({
id: 'core.react-element',
}),
routePath: createExtensionDataRef<string>().with({
id: 'core.route-path',
}),
};
```
@@ -125,9 +129,9 @@ export function createGraphiQLEndpointExtension(options) {
// Use a TypeScript namespace to merge the extension data references with the extension creator
export namespace createGraphiQLEndpointExtension {
export const endpointDataRef = createExtensionDataRef</* ... */>(
'graphiql.graphiql-endpoint.endpoint',
);
export const endpointDataRef = createExtensionDataRef</* ... */>().with({
id: 'graphiql.graphiql-endpoint.endpoint',
});
}
```
@@ -32,9 +32,11 @@ import { resolveAppTree } from './resolveAppTree';
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
import { withLogCollector } from '@backstage/test-utils';
const testDataRef = createExtensionDataRef<string>('test');
const otherDataRef = createExtensionDataRef<number>('other');
const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror');
const testDataRef = createExtensionDataRef<string>().with({ id: 'test' });
const otherDataRef = createExtensionDataRef<number>().with({ id: 'other' });
const inputMirrorDataRef = createExtensionDataRef<unknown>().with({
id: 'mirror',
});
const simpleExtension = resolveExtensionDefinition(
createExtension({
+43 -9
View File
@@ -151,6 +151,7 @@ export { AnyApiRef };
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<
unknown,
string,
{
optional?: true;
}
@@ -317,13 +318,15 @@ export { configApiRef };
// @public (undocumented)
export interface ConfigurableExtensionDataRef<
TId extends string,
TData,
TConfig extends {
optional?: true;
} = {},
> extends ExtensionDataRef<TData, TConfig> {
> extends ExtensionDataRef<TData, TId, TConfig> {
// (undocumented)
optional(): ConfigurableExtensionDataRef<
TId,
TData,
TData & {
optional: true;
@@ -347,9 +350,17 @@ export type CoreErrorBoundaryFallbackProps = {
// @public (undocumented)
export const coreExtensionData: {
reactElement: ConfigurableExtensionDataRef<JSX_2.Element, {}>;
routePath: ConfigurableExtensionDataRef<string, {}>;
routeRef: ConfigurableExtensionDataRef<RouteRef<AnyRouteRefParams>, {}>;
reactElement: ConfigurableExtensionDataRef<
'core.reactElement',
JSX_2.Element,
{}
>;
routePath: ConfigurableExtensionDataRef<'core.routing.path', string, {}>;
routeRef: ConfigurableExtensionDataRef<
'core.routing.ref',
RouteRef<AnyRouteRefParams>,
{}
>;
};
// @public (undocumented)
@@ -385,7 +396,11 @@ export function createApiExtension<
// @public (undocumented)
export namespace createApiExtension {
const // (undocumented)
factoryDataRef: ConfigurableExtensionDataRef<AnyApiFactory, {}>;
factoryDataRef: ConfigurableExtensionDataRef<
'core.api.factory',
AnyApiFactory,
{}
>;
}
export { createApiFactory };
@@ -440,6 +455,7 @@ export function createAppRootWrapperExtension<
export namespace createAppRootWrapperExtension {
const // (undocumented)
componentDataRef: ConfigurableExtensionDataRef<
'app.root.wrapper',
React_2.ComponentType<{
children?: React_2.ReactNode;
}>,
@@ -477,6 +493,7 @@ export function createComponentExtension<
export namespace createComponentExtension {
const // (undocumented)
componentDataRef: ConfigurableExtensionDataRef<
'core.component.component',
{
ref: ComponentRef;
impl: ComponentType;
@@ -544,10 +561,17 @@ export interface CreateExtensionBlueprintOptions<
output: TOutput;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export function createExtensionDataRef<TData>(
id: string,
): ConfigurableExtensionDataRef<TData>;
): ConfigurableExtensionDataRef<string, TData>;
// @public (undocumented)
export function createExtensionDataRef<TData>(): {
with<TId extends string>(options: {
id: TId;
}): ConfigurableExtensionDataRef<TId, TData>;
};
// @public (undocumented)
export function createExtensionInput<
@@ -646,6 +670,7 @@ export function createNavItemExtension(options: {
export namespace createNavItemExtension {
const // (undocumented)
targetDataRef: ConfigurableExtensionDataRef<
'core.nav-item.target',
{
title: string;
icon: IconComponent_2;
@@ -667,6 +692,7 @@ export function createNavLogoExtension(options: {
export namespace createNavLogoExtension {
const // (undocumented)
logoElementsDataRef: ConfigurableExtensionDataRef<
'core.nav-logo.logo-elements',
{
logoIcon?: JSX.Element | undefined;
logoFull?: JSX.Element | undefined;
@@ -760,6 +786,7 @@ export function createRouterExtension<
export namespace createRouterExtension {
const // (undocumented)
componentDataRef: ConfigurableExtensionDataRef<
'app.router.wrapper',
React_2.ComponentType<{
children?: React_2.ReactNode;
}>,
@@ -796,6 +823,7 @@ export function createSignInPageExtension<
export namespace createSignInPageExtension {
const // (undocumented)
componentDataRef: ConfigurableExtensionDataRef<
'core.sign-in-page.component',
React_2.ComponentType<SignInPageProps>,
{}
>;
@@ -818,7 +846,11 @@ export function createThemeExtension(
// @public (undocumented)
export namespace createThemeExtension {
const // (undocumented)
themeDataRef: ConfigurableExtensionDataRef<AppTheme, {}>;
themeDataRef: ConfigurableExtensionDataRef<
'core.theme.theme',
AppTheme,
{}
>;
}
// @public (undocumented)
@@ -831,6 +863,7 @@ export function createTranslationExtension(options: {
export namespace createTranslationExtension {
const // (undocumented)
translationDataRef: ConfigurableExtensionDataRef<
'core.translation.translation',
| TranslationResource<string>
| TranslationMessages<
string,
@@ -934,11 +967,12 @@ export interface ExtensionBoundaryProps {
// @public (undocumented)
export type ExtensionDataRef<
TData,
TId extends string = string,
TConfig extends {
optional?: true;
} = {},
> = {
id: string;
id: TId;
T: TData;
config: TConfig;
$$type: '@backstage/ExtensionDataRef';
@@ -72,6 +72,7 @@ export function createApiExtension<
/** @public */
export namespace createApiExtension {
export const factoryDataRef =
createExtensionDataRef<AnyApiFactory>('core.api.factory');
export const factoryDataRef = createExtensionDataRef<AnyApiFactory>().with({
id: 'core.api.factory',
});
}
@@ -77,8 +77,7 @@ export function createAppRootWrapperExtension<
/** @public */
export namespace createAppRootWrapperExtension {
export const componentDataRef =
createExtensionDataRef<ComponentType<PropsWithChildren<{}>>>(
'app.root.wrapper',
);
export const componentDataRef = createExtensionDataRef<
ComponentType<PropsWithChildren<{}>>
>().with({ id: 'app.root.wrapper' });
}
@@ -92,5 +92,5 @@ export namespace createComponentExtension {
export const componentDataRef = createExtensionDataRef<{
ref: ComponentRef;
impl: ComponentType;
}>('core.component.component');
}>().with({ id: 'core.component.component' });
}
@@ -61,5 +61,5 @@ export namespace createNavItemExtension {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
}>('core.nav-item.target');
}>().with({ id: 'core.nav-item.target' });
}
@@ -51,5 +51,5 @@ export namespace createNavLogoExtension {
export const logoElementsDataRef = createExtensionDataRef<{
logoIcon?: JSX.Element;
logoFull?: JSX.Element;
}>('core.nav-logo.logo-elements');
}>().with({ id: 'core.nav-logo.logo-elements' });
}
@@ -77,8 +77,7 @@ export function createRouterExtension<
/** @public */
export namespace createRouterExtension {
export const componentDataRef =
createExtensionDataRef<ComponentType<PropsWithChildren<{}>>>(
'app.router.wrapper',
);
export const componentDataRef = createExtensionDataRef<
ComponentType<PropsWithChildren<{}>>
>().with({ id: 'app.router.wrapper' });
}
@@ -79,5 +79,5 @@ export function createSignInPageExtension<
export namespace createSignInPageExtension {
export const componentDataRef = createExtensionDataRef<
ComponentType<SignInPageProps>
>('core.sign-in-page.component');
>().with({ id: 'core.sign-in-page.component' });
}
@@ -33,6 +33,7 @@ export function createThemeExtension(theme: AppTheme) {
/** @public */
export namespace createThemeExtension {
export const themeDataRef =
createExtensionDataRef<AppTheme>('core.theme.theme');
export const themeDataRef = createExtensionDataRef<AppTheme>().with({
id: 'core.theme.theme',
});
}
@@ -38,5 +38,5 @@ export function createTranslationExtension(options: {
export namespace createTranslationExtension {
export const translationDataRef = createExtensionDataRef<
TranslationResource | TranslationMessages
>('core.translation.translation');
>().with({ id: 'core.translation.translation' });
}
@@ -20,7 +20,9 @@ import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
export const coreExtensionData = {
reactElement: createExtensionDataRef<JSX.Element>('core.reactElement'),
routePath: createExtensionDataRef<string>('core.routing.path'),
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
reactElement: createExtensionDataRef<JSX.Element>().with({
id: 'core.reactElement',
}),
routePath: createExtensionDataRef<string>().with({ id: 'core.routing.path' }),
routeRef: createExtensionDataRef<RouteRef>().with({ id: 'core.routing.ref' }),
};
@@ -18,7 +18,7 @@ import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { createExtensionInput } from './createExtensionInput';
const stringData = createExtensionDataRef<string>('string');
const stringData = createExtensionDataRef<string>().with({ id: 'string' });
function unused(..._any: any[]) {}
@@ -22,7 +22,7 @@ import { ExtensionInput } from './createExtensionInput';
/** @public */
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<unknown, { optional?: true }>;
[name in string]: ExtensionDataRef<unknown, string, { optional?: true }>;
};
/** @public */
@@ -18,6 +18,15 @@ import { createExtensionDataRef } from './createExtensionDataRef';
describe('createExtensionDataRef', () => {
it('can be created and read', () => {
const ref = createExtensionDataRef().with({ id: 'foo' });
expect(ref.id).toBe('foo');
expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}');
const refOptional = ref.optional();
expect(refOptional.id).toBe('foo');
expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}');
});
it('can be created and read in the deprecated way', () => {
const ref = createExtensionDataRef('foo');
expect(ref.id).toBe('foo');
expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}');
@@ -17,9 +17,10 @@
/** @public */
export type ExtensionDataRef<
TData,
TId extends string = string,
TConfig extends { optional?: true } = {},
> = {
id: string;
id: TId;
T: TData;
config: TConfig;
$$type: '@backstage/ExtensionDataRef';
@@ -27,30 +28,59 @@ export type ExtensionDataRef<
/** @public */
export interface ConfigurableExtensionDataRef<
TId extends string,
TData,
TConfig extends { optional?: true } = {},
> extends ExtensionDataRef<TData, TConfig> {
optional(): ConfigurableExtensionDataRef<TData, TData & { optional: true }>;
> extends ExtensionDataRef<TData, TId, TConfig> {
optional(): ConfigurableExtensionDataRef<
TId,
TData,
TData & { optional: true }
>;
}
// TODO: change to options object with ID.
/** @public */
/**
* @public
* @deprecated Use the following form instead: `createExtensionDataRef<Type>().with({ id: 'core.foo' })`
*/
export function createExtensionDataRef<TData>(
id: string,
): ConfigurableExtensionDataRef<TData> {
): ConfigurableExtensionDataRef<string, TData>;
/** @public */
export function createExtensionDataRef<TData>(): {
with<TId extends string>(options: {
id: TId;
}): ConfigurableExtensionDataRef<TId, TData>;
};
export function createExtensionDataRef<TData>(id?: string):
| ConfigurableExtensionDataRef<string, TData>
| {
with<TId extends string>(options: {
id: TId;
}): ConfigurableExtensionDataRef<TId, TData>;
} {
const createRef = <TId extends string>(refId: TId) =>
({
id: refId,
$$type: '@backstage/ExtensionDataRef',
config: {},
optional() {
return {
...this,
config: { ...this.config, optional: true },
};
},
toString() {
const optional = Boolean(this.config.optional);
return `ExtensionDataRef{id=${refId},optional=${optional}}`;
},
} as ConfigurableExtensionDataRef<TId, TData, { optional?: true }>);
if (id) {
return createRef(id);
}
return {
id,
$$type: '@backstage/ExtensionDataRef',
config: {},
optional() {
return {
...this,
config: { ...this.config, optional: true },
};
with<TId extends string>(options: { id: TId }) {
return createRef(options.id);
},
toString() {
const optional = Boolean(this.config.optional);
return `ExtensionDataRef{id=${id},optional=${optional}}`;
},
} as ConfigurableExtensionDataRef<TData, { optional?: true }>;
};
}
@@ -27,7 +27,9 @@ import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import { createExtensionInput } from './createExtensionInput';
import { BackstagePlugin } from './types';
const nameExtensionDataRef = createExtensionDataRef<string>('name');
const nameExtensionDataRef = createExtensionDataRef<string>().with({
id: 'name',
});
const Extension1 = createExtension({
name: '1',
+11 -2
View File
@@ -17,12 +17,21 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export const catalogExtensionData: {
entityContentTitle: ConfigurableExtensionDataRef<string, {}>;
entityContentTitle: ConfigurableExtensionDataRef<
'catalog.entity-content-title',
string,
{}
>;
entityFilterFunction: ConfigurableExtensionDataRef<
'catalog.entity-filter-function',
(entity: Entity) => boolean,
{}
>;
entityFilterExpression: ConfigurableExtensionDataRef<string, {}>;
entityFilterExpression: ConfigurableExtensionDataRef<
'catalog.entity-filter-expression',
string,
{}
>;
};
// @alpha (undocumented)
+9 -9
View File
@@ -36,15 +36,15 @@ export * from './translation';
/** @alpha */
export const catalogExtensionData = {
entityContentTitle: createExtensionDataRef<string>(
'catalog.entity-content-title',
),
entityFilterFunction: createExtensionDataRef<(entity: Entity) => boolean>(
'catalog.entity-filter-function',
),
entityFilterExpression: createExtensionDataRef<string>(
'catalog.entity-filter-expression',
),
entityContentTitle: createExtensionDataRef<string>().with({
id: 'catalog.entity-content-title',
}),
entityFilterFunction: createExtensionDataRef<
(entity: Entity) => boolean
>().with({ id: 'catalog.entity-filter-function' }),
entityFilterExpression: createExtensionDataRef<string>().with({
id: 'catalog.entity-filter-expression',
}),
};
// TODO: Figure out how to merge with provided config schema
+5 -1
View File
@@ -11,7 +11,11 @@ const _default: BackstagePlugin<{}, {}>;
export default _default;
// @alpha (undocumented)
export const titleExtensionDataRef: ConfigurableExtensionDataRef<string, {}>;
export const titleExtensionDataRef: ConfigurableExtensionDataRef<
'title',
string,
{}
>;
// (No @packageDocumentation comment for this package)
```
+3 -1
View File
@@ -31,7 +31,9 @@ const rootRouteRef = createRouteRef();
/**
* @alpha
*/
export const titleExtensionDataRef = createExtensionDataRef<string>('title');
export const titleExtensionDataRef = createExtensionDataRef<string>().with({
id: 'title',
});
const homePage = createPageExtension({
defaultPath: '/home',
+1
View File
@@ -31,6 +31,7 @@ export function createSearchResultListItemExtension<
export namespace createSearchResultListItemExtension {
const // (undocumented)
itemDataRef: ConfigurableExtensionDataRef<
'search.search-result-list-item.item',
{
predicate?: SearchResultItemExtensionPredicate | undefined;
component: SearchResultItemExtensionComponent;
+1 -1
View File
@@ -137,5 +137,5 @@ export namespace createSearchResultListItemExtension {
export const itemDataRef = createExtensionDataRef<{
predicate?: SearchResultItemExtensionPredicate;
component: SearchResultItemExtensionComponent;
}>('search.search-result-list-item.item');
}>().with({ id: 'search.search-result-list-item.item' });
}