diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts
index a7c49d48cb..d767f21d58 100644
--- a/packages/frontend-app-api/src/createExtensionInstance.ts
+++ b/packages/frontend-app-api/src/createExtensionInstance.ts
@@ -54,9 +54,17 @@ export function createExtensionInstance(options: {
extension.factory({
source,
config: parsedConfig,
- bind: mapValues(extension.output, ref => {
- return (value: unknown) => extensionData.set(ref.id, value);
- }),
+ bind: namedOutputs => {
+ for (const [name, output] of Object.entries(namedOutputs)) {
+ const ref = extension.output[name];
+ if (!ref) {
+ throw new Error(
+ `Extension instance '${extension.id}' tried to bind unknown output '${name}'`,
+ );
+ }
+ extensionData.set(ref.id, output);
+ }
+ },
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
index f2a760d25e..05f84aba6d 100644
--- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
@@ -47,10 +47,12 @@ export const CoreRouter = createExtension({
return element;
};
- bind.component(() => (
-
-
-
- ));
+ bind({
+ component: () => (
+
+
+
+ ),
+ });
},
});
diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
index a27875bcf8..2959dcb92e 100644
--- a/packages/frontend-plugin-api/api-report.md
+++ b/packages/frontend-plugin-api/api-report.md
@@ -17,7 +17,9 @@ import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
// @public (undocumented)
-export type AnyExtensionDataMap = Record>;
+export type AnyExtensionDataMap = {
+ [name in string]: ExtensionDataRef;
+};
// @public (undocumented)
export interface BackstagePlugin {
@@ -29,12 +31,28 @@ export interface BackstagePlugin {
id: string;
}
+// @public (undocumented)
+export interface ConfigurableExtensionDataRef<
+ TData,
+ TConfig extends {
+ optional?: true;
+ } = {},
+> extends ExtensionDataRef {
+ // (undocumented)
+ optional(): ConfigurableExtensionDataRef<
+ TData,
+ TData & {
+ optional: true;
+ }
+ >;
+}
+
// @public (undocumented)
export const coreExtensionData: {
- reactComponent: ExtensionDataRef>;
- routePath: ExtensionDataRef;
- apiFactory: ExtensionDataRef;
- routeRef: ExtensionDataRef>;
+ reactComponent: ConfigurableExtensionDataRef, {}>;
+ routePath: ConfigurableExtensionDataRef;
+ apiFactory: ConfigurableExtensionDataRef;
+ routeRef: ConfigurableExtensionDataRef, {}>;
};
// @public (undocumented)
@@ -52,11 +70,7 @@ export function createApiExtension<
api: AnyApiRef;
factory: (options: {
config: TConfig;
- inputs: {
- [pointName in keyof TInputs]: ExtensionDataValue<
- TInputs[pointName]['extensionData']
- >[];
- };
+ inputs: ExtensionDataInputValues;
}) => AnyApiFactory;
}
| {
@@ -70,20 +84,27 @@ export function createApiExtension<
// @public (undocumented)
export function createExtension<
- TData extends AnyExtensionDataMap,
- TPoint extends Record<
+ TOutput extends AnyExtensionDataMap,
+ TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TConfig = never,
->(options: CreateExtensionOptions): Extension;
+>(
+ options: CreateExtensionOptions,
+): Extension;
+
+// @public (undocumented)
+export function createExtensionDataRef(
+ id: string,
+): ConfigurableExtensionDataRef;
// @public (undocumented)
export interface CreateExtensionOptions<
- TData extends AnyExtensionDataMap,
- TPoint extends Record<
+ TOutput extends AnyExtensionDataMap,
+ TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
@@ -100,20 +121,16 @@ export interface CreateExtensionOptions<
// (undocumented)
factory(options: {
source?: BackstagePlugin;
- bind: ExtensionDataBind;
+ bind: ExtensionDataBind;
config: TConfig;
- inputs: {
- [pointName in keyof TPoint]: ExtensionDataValue<
- TPoint[pointName]['extensionData']
- >[];
- };
+ inputs: ExtensionDataInputValues;
}): void;
// (undocumented)
id: string;
// (undocumented)
- inputs?: TPoint;
+ inputs?: TInputs;
// (undocumented)
- output: TData;
+ output: TOutput;
}
// @public
@@ -143,11 +160,7 @@ export function createPageExtension<
routeRef?: RouteRef;
component: (props: {
config: TConfig;
- inputs: {
- [pointName in keyof TInputs]: ExtensionDataValue<
- TInputs[pointName]['extensionData']
- >[];
- };
+ inputs: ExtensionDataInputValues;
}) => Promise;
},
): Extension;
@@ -204,22 +217,60 @@ export interface ExtensionBoundaryProps {
}
// @public (undocumented)
-export type ExtensionDataBind = {
- [K in keyof TData]: (value: TData[K]['T']) => void;
+export type ExtensionDataBind = (
+ 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 = {
+export type ExtensionDataRef<
+ TData,
+ TConfig extends {
+ optional?: true;
+ } = {},
+> = {
id: string;
- T: T;
+ T: TData;
+ config: TConfig;
$$type: 'extension-data';
};
-// @public (undocumented)
-export type ExtensionDataValue = {
- [K in keyof TData]: TData[K]['T'];
-};
-
// @public (undocumented)
export interface PluginOptions {
// (undocumented)
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
index a3662e5ef1..fadb814d4f 100644
--- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
@@ -38,10 +38,11 @@ describe('createApiExtension', () => {
configSchema: undefined,
inputs: {},
output: {
- api: {
+ api: expect.objectContaining({
$$type: 'extension-data',
id: 'core.api.factory',
- },
+ config: {},
+ }),
},
factory: expect.any(Function),
});
@@ -71,10 +72,11 @@ describe('createApiExtension', () => {
configSchema: undefined,
inputs: {},
output: {
- api: {
+ api: expect.objectContaining({
$$type: 'extension-data',
id: 'core.api.factory',
- },
+ config: {},
+ }),
},
factory: expect.any(Function),
});
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
index 5dab38aec8..f81f07ca72 100644
--- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
@@ -15,13 +15,13 @@
*/
import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api';
-import { PortableSchema } from '../createSchemaFromZod';
+import { PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
- coreExtensionData,
+ ExtensionDataInputValues,
createExtension,
- ExtensionDataValue,
-} from '../types';
+ coreExtensionData,
+} from '../wiring';
/** @public */
export function createApiExtension<
@@ -33,11 +33,7 @@ export function createApiExtension<
api: AnyApiRef;
factory: (options: {
config: TConfig;
- inputs: {
- [pointName in keyof TInputs]: ExtensionDataValue<
- TInputs[pointName]['extensionData']
- >[];
- };
+ inputs: ExtensionDataInputValues;
}) => AnyApiFactory;
}
| {
@@ -63,9 +59,9 @@ export function createApiExtension<
},
factory({ bind, config, inputs }) {
if (typeof factory === 'function') {
- bind.api(factory({ config, inputs }));
+ bind({ api: factory({ config, inputs }) });
} else {
- bind.api(factory);
+ bind({ api: factory });
}
},
});
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
index f1caaf7fdf..db389ae4b1 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
@@ -15,8 +15,8 @@
*/
import React from 'react';
-import { PortableSchema } from '../createSchemaFromZod';
-import { coreExtensionData } from '../types';
+import { PortableSchema } from '../schema';
+import { coreExtensionData } from '../wiring';
import { createPageExtension } from './createPageExtension';
describe('createPageExtension', () => {
@@ -42,6 +42,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
+ routeRef: expect.anything(),
},
factory: expect.any(Function),
});
@@ -73,6 +74,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
+ routeRef: expect.anything(),
},
factory: expect.any(Function),
});
@@ -93,6 +95,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
+ routeRef: expect.anything(),
},
factory: expect.any(Function),
});
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
index fb2d930844..167dd397dd 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
@@ -17,14 +17,14 @@
import { RouteRef } from '@backstage/core-plugin-api';
import React from 'react';
import { ExtensionBoundary } from '../components';
-import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod';
+import { createSchemaFromZod, PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
coreExtensionData,
createExtension,
Extension,
- ExtensionDataValue,
-} from '../types';
+ ExtensionDataInputValues,
+} from '../wiring';
/**
* Helper for creating extensions for a routable React page component.
@@ -50,11 +50,7 @@ export function createPageExtension<
routeRef?: RouteRef;
component: (props: {
config: TConfig;
- inputs: {
- [pointName in keyof TInputs]: ExtensionDataValue<
- TInputs[pointName]['extensionData']
- >[];
- };
+ inputs: ExtensionDataInputValues;
}) => Promise;
},
): Extension {
@@ -72,7 +68,7 @@ export function createPageExtension<
output: {
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
- ...(options.routeRef && { routeRef: coreExtensionData.routeRef }),
+ routeRef: coreExtensionData.routeRef.optional(),
},
inputs: options.inputs,
configSchema,
@@ -82,17 +78,18 @@ export function createPageExtension<
.component({ config, inputs })
.then(element => ({ default: () => element })),
);
- bind.path(config.path);
- bind.component(() => (
-
-
-
-
-
- ));
- if (options.routeRef) {
- bind.routeRef!(options.routeRef);
- }
+
+ bind({
+ path: config.path,
+ component: () => (
+
+
+
+
+
+ ),
+ routeRef: options.routeRef,
+ });
},
});
}
diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts
index 16f226c483..4e1a383899 100644
--- a/packages/frontend-plugin-api/src/index.ts
+++ b/packages/frontend-plugin-api/src/index.ts
@@ -20,21 +20,8 @@
* @packageDocumentation
*/
-export {
- createSchemaFromZod,
- type PortableSchema,
-} from './createSchemaFromZod';
export * from './components';
export * from './extensions';
-export {
- coreExtensionData,
- createExtension,
- type AnyExtensionDataMap,
- type CreateExtensionOptions,
- type Extension,
- type ExtensionDataBind,
- type ExtensionDataRef,
- type ExtensionDataValue,
-} from './types';
+export * from './schema';
export * from './wiring';
export * from './routing';
diff --git a/packages/frontend-plugin-api/src/createSchemaFromZod.test.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts
similarity index 100%
rename from packages/frontend-plugin-api/src/createSchemaFromZod.test.ts
rename to packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts
diff --git a/packages/frontend-plugin-api/src/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts
similarity index 93%
rename from packages/frontend-plugin-api/src/createSchemaFromZod.ts
rename to packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts
index 447cea8bbd..bfaf57f522 100644
--- a/packages/frontend-plugin-api/src/createSchemaFromZod.ts
+++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts
@@ -17,12 +17,7 @@
import { JsonObject } from '@backstage/types';
import { z, ZodSchema, ZodTypeDef } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
-
-/** @public */
-export type PortableSchema = {
- parse: (input: unknown) => TOutput;
- schema: JsonObject;
-};
+import { PortableSchema } from './types';
/** @public */
export function createSchemaFromZod(
diff --git a/packages/frontend-plugin-api/src/schema/index.ts b/packages/frontend-plugin-api/src/schema/index.ts
new file mode 100644
index 0000000000..7f21c4e07d
--- /dev/null
+++ b/packages/frontend-plugin-api/src/schema/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+export { createSchemaFromZod } from './createSchemaFromZod';
+export { type PortableSchema } from './types';
diff --git a/packages/frontend-plugin-api/src/schema/types.ts b/packages/frontend-plugin-api/src/schema/types.ts
new file mode 100644
index 0000000000..9636d3b896
--- /dev/null
+++ b/packages/frontend-plugin-api/src/schema/types.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 { JsonObject } from '@backstage/types';
+
+/** @public */
+export type PortableSchema = {
+ parse: (input: unknown) => TOutput;
+ schema: JsonObject;
+};
diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts
deleted file mode 100644
index 940e821af5..0000000000
--- a/packages/frontend-plugin-api/src/types.ts
+++ /dev/null
@@ -1,110 +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 { AnyApiFactory } from '@backstage/core-plugin-api';
-import { RouteRef } from '@backstage/core-plugin-api';
-import { ComponentType } from 'react';
-import { PortableSchema } from './createSchemaFromZod';
-import { BackstagePlugin } from './wiring';
-
-/** @public */
-export type ExtensionDataRef = {
- id: string;
- T: T;
- $$type: 'extension-data';
-};
-
-/** @public */
-// TODO: change to options object with ID.
-export function createExtensionDataRef(id: string): ExtensionDataRef {
- return { id, $$type: 'extension-data' } as ExtensionDataRef;
-}
-
-/** @public */
-export const coreExtensionData = {
- reactComponent: createExtensionDataRef('core.reactComponent'),
- routePath: createExtensionDataRef('core.routing.path'),
- apiFactory: createExtensionDataRef('core.api.factory'),
- routeRef: createExtensionDataRef('core.routing.ref'),
-};
-
-/** @public */
-export type AnyExtensionDataMap = Record>;
-
-/** @public */
-export type ExtensionDataBind = {
- [K in keyof TData]: (value: TData[K]['T']) => void;
-};
-
-/** @public */
-export type ExtensionDataValue = {
- [K in keyof TData]: TData[K]['T'];
-};
-
-/** @public */
-export interface CreateExtensionOptions<
- TData extends AnyExtensionDataMap,
- TPoint extends Record,
- TConfig,
-> {
- id: string;
- at: string;
- disabled?: boolean;
- inputs?: TPoint;
- output: TData;
- configSchema?: PortableSchema;
- factory(options: {
- source?: BackstagePlugin;
- bind: ExtensionDataBind;
- config: TConfig;
- inputs: {
- [pointName in keyof TPoint]: ExtensionDataValue<
- TPoint[pointName]['extensionData']
- >[];
- };
- }): void;
-}
-
-/** @public */
-export interface Extension {
- $$type: 'extension';
- id: string;
- at: string;
- disabled: boolean;
- inputs: Record;
- output: AnyExtensionDataMap;
- configSchema?: PortableSchema;
- factory(options: {
- source?: BackstagePlugin;
- bind: ExtensionDataBind;
- config: TConfig;
- inputs: Record>>;
- }): void;
-}
-
-/** @public */
-export function createExtension<
- TData extends AnyExtensionDataMap,
- TPoint extends Record,
- TConfig = never,
->(options: CreateExtensionOptions): Extension {
- return {
- ...options,
- disabled: options.disabled ?? false,
- $$type: 'extension',
- inputs: options.inputs ?? {},
- };
-}
diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts
new file mode 100644
index 0000000000..1f4e4370c9
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { AnyApiFactory, RouteRef } from '@backstage/core-plugin-api';
+import { ComponentType } from 'react';
+import { createExtensionDataRef } from './createExtensionDataRef';
+
+/** @public */
+export const coreExtensionData = {
+ reactComponent: createExtensionDataRef('core.reactComponent'),
+ routePath: createExtensionDataRef('core.routing.path'),
+ apiFactory: createExtensionDataRef('core.api.factory'),
+ routeRef: createExtensionDataRef('core.routing.ref'),
+};
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts
new file mode 100644
index 0000000000..a992642d61
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts
@@ -0,0 +1,156 @@
+/*
+ * 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 { createExtension } from './createExtension';
+import { createExtensionDataRef } from './createExtensionDataRef';
+
+const stringData = createExtensionDataRef('string');
+
+function unused(..._any: any[]) {}
+
+describe('createExtension', () => {
+ it('should create an extension with a simple output', () => {
+ const extension = createExtension({
+ id: 'test',
+ at: 'root',
+ output: {
+ foo: stringData,
+ },
+ factory({ bind }) {
+ bind({
+ foo: 'bar',
+ });
+ bind({
+ // @ts-expect-error
+ foo: 3,
+ });
+ bind({
+ // @ts-expect-error
+ bar: 'bar',
+ });
+ // @ts-expect-error
+ bind({});
+ // @ts-expect-error
+ bind();
+ // @ts-expect-error
+ bind('bar');
+ },
+ });
+ expect(extension.id).toBe('test');
+ });
+
+ it('should create an extension with a some optional output', () => {
+ const extension = createExtension({
+ id: 'test',
+ at: 'root',
+ output: {
+ foo: stringData,
+ bar: stringData.optional(),
+ },
+ factory({ bind }) {
+ bind({
+ foo: 'bar',
+ });
+ bind({
+ foo: 'bar',
+ bar: 'baz',
+ });
+ bind({
+ // @ts-expect-error
+ foo: 3,
+ });
+ bind({
+ foo: 'bar',
+ // @ts-expect-error
+ bar: 3,
+ });
+ // @ts-expect-error
+ bind({ bar: 'bar' });
+ // @ts-expect-error
+ bind({});
+ // @ts-expect-error
+ bind();
+ // @ts-expect-error
+ bind('bar');
+ },
+ });
+ expect(extension.id).toBe('test');
+ });
+
+ it('should create an extension with input', () => {
+ const extension = createExtension({
+ id: 'test',
+ at: 'root',
+ inputs: {
+ mixed: {
+ extensionData: {
+ required: stringData,
+ optional: stringData.optional(),
+ },
+ },
+ onlyRequired: {
+ extensionData: {
+ required: stringData,
+ },
+ },
+ onlyOptional: {
+ extensionData: {
+ optional: stringData.optional(),
+ },
+ },
+ },
+ output: {
+ foo: stringData,
+ },
+ factory({ bind, inputs }) {
+ const a1: string = inputs.mixed?.[0].required;
+ // @ts-expect-error
+ const a2: number = inputs.mixed?.[0].required;
+ // @ts-expect-error
+ const a3: any = inputs.mixed?.[0].nonExistent;
+ unused(a1, a2, a3);
+
+ const b1: string | undefined = inputs.mixed?.[0].optional;
+ // @ts-expect-error
+ const b2: string = inputs.mixed?.[0].optional;
+ // @ts-expect-error
+ const b3: number = inputs.mixed?.[0].optional;
+ // @ts-expect-error
+ const b4: number | undefined = inputs.mixed?.[0].optional;
+ unused(b1, b2, b3, b4);
+
+ const c1: string = inputs.onlyRequired?.[0].required;
+ // @ts-expect-error
+ const c2: number = inputs.onlyRequired?.[0].required;
+ unused(c1, c2);
+
+ const d1: string | undefined = inputs.onlyOptional?.[0].optional;
+ // @ts-expect-error
+ const d2: string = inputs.onlyOptional?.[0].optional;
+ // @ts-expect-error
+ const d3: number = inputs.onlyOptional?.[0].optional;
+ // @ts-expect-error
+ const d4: number | undefined = inputs.onlyOptional?.[0].optional;
+ unused(d1, d2, d3, d4);
+
+ bind({
+ foo: 'bar',
+ });
+ },
+ });
+ expect(extension.id).toBe('test');
+ });
+});
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts
new file mode 100644
index 0000000000..bcea03595b
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 { 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'];
+ }
+ >;
+};
+
+/** @public */
+export type ExtensionDataBind = (
+ 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 */
+export interface CreateExtensionOptions<
+ TOutput extends AnyExtensionDataMap,
+ TInputs extends Record,
+ TConfig,
+> {
+ id: string;
+ at: string;
+ disabled?: boolean;
+ inputs?: TInputs;
+ output: TOutput;
+ configSchema?: PortableSchema;
+ factory(options: {
+ source?: BackstagePlugin;
+ bind: ExtensionDataBind;
+ config: TConfig;
+ inputs: ExtensionDataInputValues;
+ }): void;
+}
+
+/** @public */
+export function createExtension<
+ TOutput extends AnyExtensionDataMap,
+ TInputs extends Record,
+ TConfig = never,
+>(
+ options: CreateExtensionOptions,
+): Extension {
+ return {
+ ...options,
+ disabled: options.disabled ?? false,
+ $$type: 'extension',
+ inputs: options.inputs ?? {},
+ factory({ bind, config, inputs }) {
+ // TODO: Simplify this, but TS wouldn't infer the input type for some reason
+ return options.factory({
+ bind,
+ config,
+ inputs: inputs as ExtensionDataInputValues,
+ });
+ },
+ };
+}
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts
new file mode 100644
index 0000000000..6d6309bee4
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+/** @public */
+export type ExtensionDataRef<
+ TData,
+ TConfig extends { optional?: true } = {},
+> = {
+ id: string;
+ T: TData;
+ config: TConfig;
+ $$type: 'extension-data';
+};
+
+/** @public */
+export interface ConfigurableExtensionDataRef<
+ TData,
+ TConfig extends { optional?: true } = {},
+> extends ExtensionDataRef {
+ optional(): ConfigurableExtensionDataRef;
+}
+
+// TODO: change to options object with ID.
+/** @public */
+export function createExtensionDataRef(
+ id: string,
+): ConfigurableExtensionDataRef {
+ return {
+ id,
+ $$type: 'extension-data',
+ config: {},
+ optional() {
+ return { ...this, config: { ...this.config, optional: true } };
+ },
+ } as ConfigurableExtensionDataRef;
+}
diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
index 36389d7e63..724dad5143 100644
--- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
@@ -17,14 +17,12 @@
import React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { render, screen } from '@testing-library/react';
-import { createSchemaFromZod } from '../createSchemaFromZod';
-import {
- createExtension,
- coreExtensionData,
- createExtensionDataRef,
-} from '../types';
+import { createSchemaFromZod } from '../schema/createSchemaFromZod';
import { createPlugin, BackstagePlugin } from './createPlugin';
import { JsonObject } from '@backstage/types';
+import { createExtension } from './createExtension';
+import { createExtensionDataRef } from './createExtensionDataRef';
+import { coreExtensionData } from './coreExtensionData';
const nameExtensionDataRef = createExtensionDataRef('name');
@@ -35,7 +33,7 @@ const TechRadarPage = createExtension({
name: nameExtensionDataRef,
},
factory({ bind }) {
- bind.name('TechRadar');
+ bind({ name: 'TechRadar' });
},
});
@@ -49,7 +47,7 @@ const CatalogPage = createExtension({
z.object({ name: z.string().default('Catalog') }),
),
factory({ bind, config }) {
- bind.name(config.name);
+ bind({ name: config.name });
},
});
@@ -63,7 +61,7 @@ const TechDocsAddon = createExtension({
z.object({ name: z.string().default('TechDocsAddon') }),
),
factory({ bind, config }) {
- bind.name(config.name);
+ bind({ name: config.name });
},
});
@@ -81,7 +79,7 @@ const TechDocsPage = createExtension({
name: nameExtensionDataRef,
},
factory({ bind, inputs }) {
- bind.name(`TechDocs-${inputs.addons.map(n => n.name).join('-')}`);
+ bind({ name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` });
},
});
@@ -99,11 +97,12 @@ const outputExtension = createExtension({
component: coreExtensionData.reactComponent,
},
factory({ bind, inputs }) {
- bind.component(() =>
- React.createElement('span', {}, [
- `Names: ${inputs.names.map(n => n.name).join(', ')}`,
- ]),
- );
+ bind({
+ component: () =>
+ React.createElement('span', {}, [
+ `Names: ${inputs.names.map(n => n.name).join(', ')}`,
+ ]),
+ });
},
});
diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts
index 3e15fb0b19..7f7e44ff03 100644
--- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts
+++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Extension } from '../types';
+import { Extension } from './types';
/** @public */
export interface PluginOptions {
diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts
index d96ba09971..175200cf0c 100644
--- a/packages/frontend-plugin-api/src/wiring/index.ts
+++ b/packages/frontend-plugin-api/src/wiring/index.ts
@@ -14,8 +14,21 @@
* limitations under the License.
*/
+export { coreExtensionData } from './coreExtensionData';
+export {
+ createExtension,
+ type CreateExtensionOptions,
+ type ExtensionDataBind,
+ type ExtensionDataInputValues,
+} from './createExtension';
+export {
+ createExtensionDataRef,
+ type ExtensionDataRef,
+ type ConfigurableExtensionDataRef,
+} from './createExtensionDataRef';
export {
createPlugin,
type BackstagePlugin,
type PluginOptions,
} from './createPlugin';
+export type { AnyExtensionDataMap, Extension } from './types';
diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts
new file mode 100644
index 0000000000..dbe4633ff5
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/types.ts
@@ -0,0 +1,42 @@
+/*
+ * 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;
+};
+
+/** @public */
+export interface Extension {
+ $$type: 'extension';
+ id: string;
+ at: string;
+ disabled: boolean;
+ inputs: Record;
+ output: AnyExtensionDataMap;
+ configSchema?: PortableSchema;
+ factory(options: {
+ source?: BackstagePlugin;
+ bind: ExtensionDataBind;
+ config: TConfig;
+ inputs: Record>>;
+ }): void;
+}