diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
index 84aa3396e8..92fc11ef17 100644
--- a/packages/frontend-plugin-api/api-report.md
+++ b/packages/frontend-plugin-api/api-report.md
@@ -5,6 +5,8 @@
```ts
///
+import { AnyApiFactory } from '@backstage/core-plugin-api';
+import { AnyApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { JsonObject } from '@backstage/types';
import { z } from 'zod';
@@ -28,8 +30,40 @@ export interface BackstagePlugin {
export const coreExtensionData: {
reactComponent: ExtensionDataRef>;
routePath: ExtensionDataRef;
+ apiFactory: ExtensionDataRef;
};
+// @public (undocumented)
+export function createApiExtension<
+ TConfig extends {},
+ TInputs extends Record<
+ string,
+ {
+ extensionData: AnyExtensionDataMap;
+ }
+ >,
+>(
+ options: (
+ | {
+ api: AnyApiRef;
+ factory: (options: {
+ config: TConfig;
+ inputs: {
+ [pointName in keyof TInputs]: ExtensionDataValue<
+ TInputs[pointName]['extensionData']
+ >[];
+ };
+ }) => AnyApiFactory;
+ }
+ | {
+ factory: AnyApiFactory;
+ }
+ ) & {
+ configSchema?: PortableSchema;
+ inputs?: TInputs;
+ },
+): Extension;
+
// @public (undocumented)
export function createExtension<
TData extends AnyExtensionDataMap,
diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json
index 0c420abbf3..fa2b4ff093 100644
--- a/packages/frontend-plugin-api/package.json
+++ b/packages/frontend-plugin-api/package.json
@@ -35,6 +35,7 @@
"react": "^16.13.1 || ^17.0.0"
},
"dependencies": {
+ "@backstage/core-plugin-api": "workspace:^",
"@backstage/types": "workspace:^",
"lodash": "^4.17.21",
"zod": "^3.21.4",
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
new file mode 100644
index 0000000000..a3662e5ef1
--- /dev/null
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { createApiExtension } from './createApiExtension';
+import { createApiFactory, createApiRef } from '@backstage/core-plugin-api';
+
+describe('createApiExtension', () => {
+ it('fills in the expected values for an existing factory', () => {
+ const api = createApiRef<{ foo: string }>({ id: 'test' });
+ const factory = createApiFactory({
+ api,
+ deps: {},
+ factory: () => ({ foo: 'bar' }),
+ });
+
+ const extension = createApiExtension({
+ factory,
+ });
+
+ expect(extension).toEqual({
+ $$type: 'extension',
+ id: 'apis.test',
+ at: 'core/apis',
+ disabled: false,
+ configSchema: undefined,
+ inputs: {},
+ output: {
+ api: {
+ $$type: 'extension-data',
+ id: 'core.api.factory',
+ },
+ },
+ factory: expect.any(Function),
+ });
+ });
+
+ it('fills in the expected values for a ref and custom factory', () => {
+ const api = createApiRef<{ foo: string }>({ id: 'test' });
+ const factory = jest.fn(() => ({ foo: 'bar' }));
+
+ const extension = createApiExtension({
+ api,
+ inputs: {},
+ factory({ config: _config, inputs: _inputs }) {
+ return createApiFactory({
+ api,
+ deps: {},
+ factory,
+ });
+ },
+ });
+
+ expect(extension).toEqual({
+ $$type: 'extension',
+ id: 'apis.test',
+ at: 'core/apis',
+ disabled: false,
+ configSchema: undefined,
+ inputs: {},
+ output: {
+ api: {
+ $$type: 'extension-data',
+ id: 'core.api.factory',
+ },
+ },
+ factory: expect.any(Function),
+ });
+ });
+});
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
new file mode 100644
index 0000000000..5dab38aec8
--- /dev/null
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
@@ -0,0 +1,72 @@
+/*
+ * 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, AnyApiRef } from '@backstage/core-plugin-api';
+import { PortableSchema } from '../createSchemaFromZod';
+import {
+ AnyExtensionDataMap,
+ coreExtensionData,
+ createExtension,
+ ExtensionDataValue,
+} from '../types';
+
+/** @public */
+export function createApiExtension<
+ TConfig extends {},
+ TInputs extends Record,
+>(
+ options: (
+ | {
+ api: AnyApiRef;
+ factory: (options: {
+ config: TConfig;
+ inputs: {
+ [pointName in keyof TInputs]: ExtensionDataValue<
+ TInputs[pointName]['extensionData']
+ >[];
+ };
+ }) => AnyApiFactory;
+ }
+ | {
+ factory: AnyApiFactory;
+ }
+ ) & {
+ configSchema?: PortableSchema;
+ inputs?: TInputs;
+ },
+) {
+ const { factory, configSchema, inputs: extensionInputs } = options;
+
+ const apiRef =
+ 'api' in options ? options.api : (factory as { api: AnyApiRef }).api;
+
+ return createExtension({
+ id: `apis.${apiRef.id}`,
+ at: 'core/apis',
+ inputs: extensionInputs,
+ configSchema,
+ output: {
+ api: coreExtensionData.apiFactory,
+ },
+ factory({ bind, config, inputs }) {
+ if (typeof factory === 'function') {
+ bind.api(factory({ config, inputs }));
+ } else {
+ bind.api(factory);
+ }
+ },
+ });
+}
diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts
index d2a18fe977..fa6e60894f 100644
--- a/packages/frontend-plugin-api/src/extensions/index.ts
+++ b/packages/frontend-plugin-api/src/extensions/index.ts
@@ -14,4 +14,5 @@
* limitations under the License.
*/
+export { createApiExtension } from './createApiExtension';
export { createPageExtension } from './createPageExtension';
diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts
index 2affa4ae42..2c9f515d9d 100644
--- a/packages/frontend-plugin-api/src/types.ts
+++ b/packages/frontend-plugin-api/src/types.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { AnyApiFactory } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { PortableSchema } from './createSchemaFromZod';
@@ -34,6 +35,7 @@ export function createExtensionDataRef(id: string): ExtensionDataRef {
export const coreExtensionData = {
reactComponent: createExtensionDataRef('core.reactComponent'),
routePath: createExtensionDataRef('core.routing.path'),
+ apiFactory: createExtensionDataRef('core.api.factory'),
};
/** @public */
diff --git a/yarn.lock b/yarn.lock
index 22fcc1a57d..275104023a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4298,6 +4298,7 @@ __metadata:
resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api"
dependencies:
"@backstage/cli": "workspace:^"
+ "@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^5.10.1