From d911b7281160a2bc3acd40282d84bb4a43822891 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 16:40:44 +0100 Subject: [PATCH] frontend-plugin-api: add explicit ApiRef plugin ownership Add the new frontend ApiRef builder form while preserving compatibility with existing refs, and let frontend apps resolve API ownership through an explicit pluginId when provided. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/api-ref-plugin-owner-app.md | 5 ++ .changeset/api-ref-plugin-owner-core.md | 5 ++ .changeset/opaque-api-ref-type.md | 8 +- packages/core-plugin-api/report.api.md | 4 +- .../core-plugin-api/src/apis/system/ApiRef.ts | 22 ++++- .../src/wiring/createSpecializedApp.test.tsx | 53 ++++++++++++ .../src/wiring/createSpecializedApp.tsx | 9 +- .../src/apis/OpaqueApiRef.ts | 28 ------- packages/frontend-internal/src/apis/index.ts | 17 ---- packages/frontend-internal/src/index.ts | 1 - .../frontend-plugin-api/report-alpha.api.md | 4 +- packages/frontend-plugin-api/report.api.md | 38 ++++++--- .../src/apis/definitions/AlertApi.ts | 3 +- .../src/apis/definitions/AnalyticsApi.ts | 8 +- .../src/apis/definitions/AppLanguageApi.ts | 8 +- .../src/apis/definitions/AppThemeApi.ts | 8 +- .../src/apis/definitions/AppTreeApi.ts | 5 +- .../src/apis/definitions/ConfigApi.ts | 3 +- .../src/apis/definitions/DialogApi.ts | 3 +- .../src/apis/definitions/DiscoveryApi.ts | 8 +- .../src/apis/definitions/ErrorApi.ts | 3 +- .../src/apis/definitions/FeatureFlagsApi.ts | 8 +- .../src/apis/definitions/FetchApi.ts | 3 +- .../src/apis/definitions/IconsApi.ts | 3 +- .../src/apis/definitions/IdentityApi.ts | 8 +- .../src/apis/definitions/OAuthRequestApi.ts | 8 +- .../definitions/PluginHeaderActionsApi.ts | 8 +- .../src/apis/definitions/PluginWrapperApi.ts | 3 +- .../apis/definitions/RouteResolutionApi.ts | 3 +- .../src/apis/definitions/StorageApi.ts | 8 +- .../definitions/SwappableComponentsApi.ts | 8 +- .../src/apis/definitions/TranslationApi.ts | 8 +- .../src/apis/definitions/auth.ts | 84 ++++++++++++++++--- .../src/apis/system/ApiRef.test.ts | 3 +- .../src/apis/system/ApiRef.ts | 79 ++++++++++++----- .../src/apis/system/types.ts | 3 +- .../src/apis/system/useApi.test.tsx | 6 +- .../src/blueprints/ApiBlueprint.test.ts | 8 +- plugins/scaffolder-react/report-alpha.api.md | 4 +- plugins/scaffolder-react/report.api.md | 4 +- plugins/scaffolder/report-alpha.api.md | 4 +- plugins/scaffolder/report.api.md | 4 +- 42 files changed, 355 insertions(+), 155 deletions(-) create mode 100644 .changeset/api-ref-plugin-owner-app.md create mode 100644 .changeset/api-ref-plugin-owner-core.md delete mode 100644 packages/frontend-internal/src/apis/OpaqueApiRef.ts delete mode 100644 packages/frontend-internal/src/apis/index.ts diff --git a/.changeset/api-ref-plugin-owner-app.md b/.changeset/api-ref-plugin-owner-app.md new file mode 100644 index 0000000000..e9727a3255 --- /dev/null +++ b/.changeset/api-ref-plugin-owner-app.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Frontend apps now respect an explicit `pluginId` on `ApiRef`s when deciding which plugin owns an API factory. diff --git a/.changeset/api-ref-plugin-owner-core.md b/.changeset/api-ref-plugin-owner-core.md new file mode 100644 index 0000000000..005dc06424 --- /dev/null +++ b/.changeset/api-ref-plugin-owner-core.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Updated `createApiRef` to preserve the direct config call without deprecation warnings while staying compatible with the new frontend API ref typing. diff --git a/.changeset/opaque-api-ref-type.md b/.changeset/opaque-api-ref-type.md index b1d7caf063..7397982cf6 100644 --- a/.changeset/opaque-api-ref-type.md +++ b/.changeset/opaque-api-ref-type.md @@ -1,7 +1,5 @@ ---- -'@backstage/frontend-plugin-api': minor ---- +## '@backstage/frontend-plugin-api': patch -**BREAKING**: The `ApiRef` type is now an opaque type with a `$$type` discriminator field and `readonly` properties. This means that `ApiRef` instances can no longer be created as plain object literals. Use `createApiRef` to create API references. +Added a builder form for `createApiRef` in the new frontend system and deprecated the direct `createApiRef({ ... })` call in favor of `createApiRef().with({ ... })`. -Added a new builder pattern for creating API references: `createApiRef().with({ id: 'plugin.my.api' })`. The existing `createApiRef({ id: 'plugin.my.api' })` pattern continues to work. +`ApiRef` and `ApiRefConfig` now also support an explicit `pluginId`, making it possible to declare API ownership without encoding the plugin ID into the API ref ID. diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index 654bbe94d1..7165cfc697 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -28,7 +28,6 @@ import { ComponentType } from 'react'; import { ConfigApi } from '@backstage/frontend-plugin-api'; import { configApiRef } from '@backstage/frontend-plugin-api'; import { createApiFactory } from '@backstage/frontend-plugin-api'; -import { createApiRef } from '@backstage/frontend-plugin-api'; import { DiscoveryApi } from '@backstage/frontend-plugin-api'; import { discoveryApiRef } from '@backstage/frontend-plugin-api'; import { ErrorApi } from '@backstage/frontend-plugin-api'; @@ -256,7 +255,8 @@ export { configApiRef }; export { createApiFactory }; -export { createApiRef }; +// @public +export function createApiRef(config: ApiRefConfig): ApiRef; // @public export function createComponentExtension< diff --git a/packages/core-plugin-api/src/apis/system/ApiRef.ts b/packages/core-plugin-api/src/apis/system/ApiRef.ts index ffd074b9a7..8c3986c583 100644 --- a/packages/core-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/core-plugin-api/src/apis/system/ApiRef.ts @@ -14,5 +14,23 @@ * limitations under the License. */ -export { createApiRef } from '@backstage/frontend-plugin-api'; -export type { ApiRefConfig } from '@backstage/frontend-plugin-api'; +import { + createApiRef as createFrontendApiRef, + type ApiRef, + type ApiRefConfig, +} from '@backstage/frontend-plugin-api'; + +const createFrontendApiRefCompat = createFrontendApiRef as ( + config: ApiRefConfig, +) => ApiRef; + +/** + * Creates a reference to an API. + * + * @public + */ +export function createApiRef(config: ApiRefConfig): ApiRef { + return createFrontendApiRefCompat(config); +} + +export type { ApiRefConfig }; diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index e07e83677b..d01bc4fe9c 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -169,6 +169,7 @@ describe('createSpecializedApp', () => { "api": { "$$type": "@backstage/ApiRef", "id": "core.featureflags", + "pluginId": "app", "toString": [Function], "version": "v1", }, @@ -182,6 +183,7 @@ describe('createSpecializedApp', () => { "api": { "$$type": "@backstage/ApiRef", "id": "core.app-tree", + "pluginId": "app", "toString": [Function], "version": "v1", }, @@ -195,6 +197,7 @@ describe('createSpecializedApp', () => { "api": { "$$type": "@backstage/ApiRef", "id": "core.config", + "pluginId": "app", "toString": [Function], "version": "v1", }, @@ -208,6 +211,7 @@ describe('createSpecializedApp', () => { "api": { "$$type": "@backstage/ApiRef", "id": "core.route-resolution", + "pluginId": "app", "toString": [Function], "version": "v1", }, @@ -221,6 +225,7 @@ describe('createSpecializedApp', () => { "api": { "$$type": "@backstage/ApiRef", "id": "core.identity", + "pluginId": "app", "toString": [Function], "version": "v1", }, @@ -364,6 +369,54 @@ describe('createSpecializedApp', () => { expect(app.apis.get(testApiRef)).toEqual({ value: 'owner' }); }); + it('should select the API factory from an explicitly owned plugin on conflict', () => { + const testApiRef = createApiRef<{ value: string }>().with({ + id: 'shared.api', + pluginId: 'owner', + }); + + const app = createSpecializedApp({ + features: [ + makeAppPlugin(), + createFrontendPlugin({ + pluginId: 'other-before', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'other' }), + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'owner', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'owner' }), + }), + }), + ], + }), + ], + }); + + expect(app.errors).toEqual([ + expect.objectContaining({ + code: 'API_FACTORY_CONFLICT', + message: expect.stringContaining("API 'shared.api'"), + }), + ]); + + expect(app.apis.get(testApiRef)).toEqual({ value: 'owner' }); + }); + it('should allow API overrides within the same plugin', () => { const testApiRef = createApiRef<{ value: string }>({ id: 'test.api' }); diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index fda00b70b1..7ab36f2fad 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -401,7 +401,7 @@ function createApiFactories(options: { const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); if (apiFactory) { const apiRefId = apiFactory.api.id; - const ownerId = getApiOwnerId(apiRefId); + const ownerId = getApiOwnerId(apiFactory.api); const pluginId = apiNode.spec.plugin.pluginId ?? 'app'; const existingFactory = factoriesById.get(apiRefId); @@ -455,7 +455,12 @@ function createApiFactories(options: { // TODO(Rugvip): It would be good if this was more explicit, but I think that // might need to wait for some future update for API factories. -function getApiOwnerId(apiRefId: string): string { +function getApiOwnerId(apiRef: { id: string; pluginId?: string }): string { + if (apiRef.pluginId) { + return apiRef.pluginId; + } + + const apiRefId = apiRef.id; const [prefix, ...rest] = apiRefId.split('.'); if (!prefix) { return apiRefId; diff --git a/packages/frontend-internal/src/apis/OpaqueApiRef.ts b/packages/frontend-internal/src/apis/OpaqueApiRef.ts deleted file mode 100644 index 5e054a4bc5..0000000000 --- a/packages/frontend-internal/src/apis/OpaqueApiRef.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 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 { ApiRef } from '@backstage/frontend-plugin-api'; -import { OpaqueType } from '@internal/opaque'; - -export const OpaqueApiRef = OpaqueType.create<{ - public: ApiRef; - versions: { - readonly version: 'v1'; - }; -}>({ - type: '@backstage/ApiRef', - versions: ['v1'], -}); diff --git a/packages/frontend-internal/src/apis/index.ts b/packages/frontend-internal/src/apis/index.ts deleted file mode 100644 index 8476e86409..0000000000 --- a/packages/frontend-internal/src/apis/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2025 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 { OpaqueApiRef } from './OpaqueApiRef'; diff --git a/packages/frontend-internal/src/index.ts b/packages/frontend-internal/src/index.ts index 4bd0348345..38bfdc53f8 100644 --- a/packages/frontend-internal/src/index.ts +++ b/packages/frontend-internal/src/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export * from './apis'; export * from './routing'; export * from './wiring'; diff --git a/packages/frontend-plugin-api/report-alpha.api.md b/packages/frontend-plugin-api/report-alpha.api.md index 76678107ef..c1a3c71a21 100644 --- a/packages/frontend-plugin-api/report-alpha.api.md +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -24,7 +24,9 @@ export type PluginWrapperApi = { }; // @public -export const pluginWrapperApiRef: ApiRef; +export const pluginWrapperApiRef: ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export const PluginWrapperBlueprint: ExtensionBlueprint<{ diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index bf71922386..17b3ecc75d 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -188,14 +188,16 @@ export type ApiHolder = { // @public export type ApiRef = { - readonly $$type: '@backstage/ApiRef'; + readonly $$type?: '@backstage/ApiRef'; readonly id: string; + readonly pluginId?: string; readonly T: T; }; // @public export type ApiRefConfig = { id: string; + pluginId?: string; }; // @public (undocumented) @@ -306,7 +308,9 @@ export interface AppTreeApi { } // @public -export const appTreeApiRef: ApiRef_2; +export const appTreeApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export const atlassianAuthApiRef: ApiRef< @@ -416,12 +420,16 @@ export function createApiFactory( instance: Impl, ): ApiFactory; -// @public -export function createApiRef(config: ApiRefConfig): ApiRef; +// @public @deprecated +export function createApiRef(config: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export function createApiRef(): { - with(config: ApiRefConfig): ApiRef; + with(config: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; }; // @public @@ -875,7 +883,9 @@ export interface DialogApiDialog { } // @public -export const dialogApiRef: ApiRef_2; +export const dialogApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type DiscoveryApi = { @@ -1436,7 +1446,9 @@ export interface IconsApi { } // @public -export const iconsApiRef: ApiRef_2; +export const iconsApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type IdentityApi = { @@ -1851,7 +1863,9 @@ export type PluginHeaderActionsApi = { }; // @public -export const pluginHeaderActionsApiRef: ApiRef_2; +export const pluginHeaderActionsApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export interface PluginOptions< @@ -1999,7 +2013,9 @@ export interface RouteResolutionApi { } // @public -export const routeResolutionApiRef: ApiRef_2; +export const routeResolutionApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type SessionApi = { @@ -2127,7 +2143,9 @@ export interface SwappableComponentsApi { } // @public -export const swappableComponentsApiRef: ApiRef_2; +export const swappableComponentsApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export type TranslationApi = { diff --git a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts index afdcb6a617..09d979ad9a 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts @@ -51,6 +51,7 @@ export type AlertApi = { * * @public */ -export const alertApiRef: ApiRef = createApiRef({ +export const alertApiRef: ApiRef = createApiRef().with({ id: 'core.alert', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts index aa1f08ffbe..51acc1851d 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -151,6 +151,8 @@ export type AnalyticsApi = { * * @public */ -export const analyticsApiRef: ApiRef = createApiRef({ - id: 'core.analytics', -}); +export const analyticsApiRef: ApiRef = + createApiRef().with({ + id: 'core.analytics', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts index 36b97f9fff..c4a1c8be73 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts @@ -31,6 +31,8 @@ export type AppLanguageApi = { /** * @public */ -export const appLanguageApiRef: ApiRef = createApiRef({ - id: 'core.applanguage', -}); +export const appLanguageApiRef: ApiRef = + createApiRef().with({ + id: 'core.applanguage', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts index e771fad597..39561f5f96 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -82,6 +82,8 @@ export type AppThemeApi = { * * @public */ -export const appThemeApiRef: ApiRef = createApiRef({ - id: 'core.apptheme', -}); +export const appThemeApiRef: ApiRef = + createApiRef().with({ + id: 'core.apptheme', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index 89902d2727..16369680f2 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -117,4 +117,7 @@ export interface AppTreeApi { * * @public */ -export const appTreeApiRef = createApiRef({ id: 'core.app-tree' }); +export const appTreeApiRef = createApiRef().with({ + id: 'core.app-tree', + pluginId: 'app', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts index f935dfa3af..eb52c1cbca 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts @@ -29,6 +29,7 @@ export type ConfigApi = Config; * * @public */ -export const configApiRef: ApiRef = createApiRef({ +export const configApiRef: ApiRef = createApiRef().with({ id: 'core.config', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts index d7ab2e5d75..bcb92f528a 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts @@ -173,6 +173,7 @@ export interface DialogApi { * * @public */ -export const dialogApiRef = createApiRef({ +export const dialogApiRef = createApiRef().with({ id: 'core.dialog', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts index d23fe3db6c..fb348c156d 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -50,6 +50,8 @@ export type DiscoveryApi = { * * @public */ -export const discoveryApiRef: ApiRef = createApiRef({ - id: 'core.discovery', -}); +export const discoveryApiRef: ApiRef = + createApiRef().with({ + id: 'core.discovery', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts index 9c73d94cac..d106ccc05c 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts @@ -86,6 +86,7 @@ export type ErrorApi = { * * @public */ -export const errorApiRef: ApiRef = createApiRef({ +export const errorApiRef: ApiRef = createApiRef().with({ id: 'core.error', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts index d4429975cc..7206dd9900 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -121,6 +121,8 @@ export interface FeatureFlagsApi { * * @public */ -export const featureFlagsApiRef: ApiRef = createApiRef({ - id: 'core.featureflags', -}); +export const featureFlagsApiRef: ApiRef = + createApiRef().with({ + id: 'core.featureflags', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts index aba0e53bb7..4e4909b7d4 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts @@ -46,6 +46,7 @@ export type FetchApi = { * * @public */ -export const fetchApiRef: ApiRef = createApiRef({ +export const fetchApiRef: ApiRef = createApiRef().with({ id: 'core.fetch', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts index d22ebcce4a..a54ae7f3b1 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts @@ -41,6 +41,7 @@ export interface IconsApi { * * @public */ -export const iconsApiRef = createApiRef({ +export const iconsApiRef = createApiRef().with({ id: 'core.icons', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts index 1b127a1971..dc23202c2e 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts @@ -51,6 +51,8 @@ export type IdentityApi = { * * @public */ -export const identityApiRef: ApiRef = createApiRef({ - id: 'core.identity', -}); +export const identityApiRef: ApiRef = + createApiRef().with({ + id: 'core.identity', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 75bf3a3864..0c199948af 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -126,6 +126,8 @@ export type OAuthRequestApi = { * * @public */ -export const oauthRequestApiRef: ApiRef = createApiRef({ - id: 'core.oauthrequest', -}); +export const oauthRequestApiRef: ApiRef = + createApiRef().with({ + id: 'core.oauthrequest', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts index 78d0e2623f..9e2d1ca0fd 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/PluginHeaderActionsApi.ts @@ -40,6 +40,8 @@ export type PluginHeaderActionsApi = { * * @public */ -export const pluginHeaderActionsApiRef = createApiRef({ - id: 'core.plugin-header-actions', -}); +export const pluginHeaderActionsApiRef = + createApiRef().with({ + id: 'core.plugin-header-actions', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts index 8d9b224b1f..7965b8ccb4 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/PluginWrapperApi.ts @@ -47,6 +47,7 @@ export type PluginWrapperApi = { * * @public */ -export const pluginWrapperApiRef = createApiRef({ +export const pluginWrapperApiRef = createApiRef().with({ id: 'core.plugin-wrapper', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts index 0c3ca9c4cf..9dea5d7bc2 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts @@ -65,6 +65,7 @@ export interface RouteResolutionApi { * * @public */ -export const routeResolutionApiRef = createApiRef({ +export const routeResolutionApiRef = createApiRef().with({ id: 'core.route-resolution', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts index 1506e5f143..7e8372b6fb 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts @@ -105,6 +105,8 @@ export interface StorageApi { * * @public */ -export const storageApiRef: ApiRef = createApiRef({ - id: 'core.storage', -}); +export const storageApiRef: ApiRef = + createApiRef().with({ + id: 'core.storage', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts index 09dff04f43..47ffed91ef 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/SwappableComponentsApi.ts @@ -36,6 +36,8 @@ export interface SwappableComponentsApi { * * @public */ -export const swappableComponentsApiRef = createApiRef({ - id: 'core.swappable-components', -}); +export const swappableComponentsApiRef = + createApiRef().with({ + id: 'core.swappable-components', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts index b569800e3e..6997269484 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts @@ -358,6 +358,8 @@ export type TranslationApi = { /** * @public */ -export const translationApiRef: ApiRef = createApiRef({ - id: 'core.translation', -}); +export const translationApiRef: ApiRef = + createApiRef().with({ + id: 'core.translation', + pluginId: 'app', + }); diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 76aa24e8f8..0c05047f24 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -28,7 +28,10 @@ import { Observable } from '@backstage/types'; * For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, * would be declared as follows: * - * const googleAuthApiRef = createApiRef({ ... }) + * const googleAuthApiRef = createApiRef().with({ + * id: 'core.auth.google', + * pluginId: 'app', + * }) */ /** @@ -339,8 +342,15 @@ export const googleAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.google', + pluginId: 'app', }); /** @@ -354,8 +364,11 @@ export const googleAuthApiRef: ApiRef< */ export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>().with({ id: 'core.auth.github', + pluginId: 'app', }); /** @@ -373,8 +386,15 @@ export const oktaAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.okta', + pluginId: 'app', }); /** @@ -392,8 +412,15 @@ export const gitlabAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.gitlab', + pluginId: 'app', }); /** @@ -412,8 +439,15 @@ export const microsoftAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.microsoft', + pluginId: 'app', }); /** @@ -427,8 +461,15 @@ export const oneloginAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.onelogin', + pluginId: 'app', }); /** @@ -442,8 +483,11 @@ export const oneloginAuthApiRef: ApiRef< */ export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>().with({ id: 'core.auth.bitbucket', + pluginId: 'app', }); /** @@ -457,8 +501,11 @@ export const bitbucketAuthApiRef: ApiRef< */ export const bitbucketServerAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>().with({ id: 'core.auth.bitbucket-server', + pluginId: 'app', }); /** @@ -472,8 +519,11 @@ export const bitbucketServerAuthApiRef: ApiRef< */ export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>().with({ id: 'core.auth.atlassian', + pluginId: 'app', }); /** @@ -491,8 +541,15 @@ export const vmwareCloudAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +>().with({ id: 'core.auth.vmware-cloud', + pluginId: 'app', }); /** @@ -508,6 +565,9 @@ export const vmwareCloudAuthApiRef: ApiRef< */ export const openshiftAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +> = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>().with({ id: 'core.auth.openshift', + pluginId: 'app', }); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts index 556fc3e477..b20134cc2a 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts @@ -26,9 +26,10 @@ describe('ApiRef', () => { }); it('should be created with builder pattern', () => { - const ref = createApiRef().with({ id: 'abc' }); + const ref = createApiRef().with({ id: 'abc', pluginId: 'test' }); expect(ref.$$type).toBe('@backstage/ApiRef'); expect(ref.id).toBe('abc'); + expect(ref.pluginId).toBe('test'); expect(String(ref)).toBe('apiRef{abc}'); expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); }); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts index 0dc181591c..3cc7fc6649 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { OpaqueType } from '@internal/opaque'; import type { ApiRef } from './types'; /** @@ -23,8 +24,21 @@ import type { ApiRef } from './types'; */ export type ApiRefConfig = { id: string; + pluginId?: string; }; +const OpaqueApiRef = OpaqueType.create<{ + public: ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; + versions: { + readonly version: 'v1'; + }; +}>({ + type: '@backstage/ApiRef', + versions: ['v1'], +}); + function validateId(id: string): void { const valid = id .split('.') @@ -37,22 +51,24 @@ function validateId(id: string): void { } } -function makeApiRef(id: string): ApiRef { - const ref = { - $$type: '@backstage/ApiRef' as const, - version: 'v1', - id, +function makeApiRef( + config: ApiRefConfig, +): ApiRef & { readonly $$type: '@backstage/ApiRef' } { + const ref = OpaqueApiRef.createInstance('v1', { + id: config.id, + ...(config.pluginId ? { pluginId: config.pluginId } : {}), + T: undefined as T, toString() { - return `apiRef{${id}}`; + return `apiRef{${config.id}}`; }, - }; + }) as ApiRef & { readonly $$type: '@backstage/ApiRef' }; Object.defineProperty(ref, 'T', { get(): T { throw new Error(`tried to read ApiRef.T of ${this}`); }, enumerable: false, }); - return ref as unknown as ApiRef; + return ref; } /** @@ -61,18 +77,22 @@ function makeApiRef(id: string): ApiRef { * @remarks * * The `id` is a stable identifier for the API implementation. The frontend - * system infers the owning plugin for an API from the `id`. The recommended - * pattern is `plugin..*` (for example, + * system infers the owning plugin for an API from the `id`, unless you provide + * a `pluginId` explicitly. The recommended pattern is `plugin..*` + * (for example, * `plugin.catalog.entity-presentation`). This ensures that other plugins can't * mistakenly override your API implementation. * * The recommended way to create an API reference is: * * ```ts - * const myApiRef = createApiRef().with({ id: 'plugin.my.api' }); + * const myApiRef = createApiRef().with({ + * id: 'my-api', + * pluginId: 'my-plugin', + * }); * ``` * - * For backwards compatibility, you can also pass the config directly: + * The legacy way to create an API reference is: * * ```ts * const myApiRef = createApiRef({ id: 'plugin.my.api' }); @@ -80,30 +100,47 @@ function makeApiRef(id: string): ApiRef { * * @public */ -export function createApiRef(config: ApiRefConfig): ApiRef; +/** + * Creates a reference to an API. + * + * @deprecated Use `createApiRef().with(...)` instead. + * @public + */ +export function createApiRef( + config: ApiRefConfig, +): ApiRef & { readonly $$type: '@backstage/ApiRef' }; /** * Creates a reference to an API. * * @remarks * - * Returns a builder with a `.with()` method for providing the `id`. + * Returns a builder with a `.with()` method for providing the API reference + * configuration. * * @public */ export function createApiRef(): { - with(config: ApiRefConfig): ApiRef; + with(config: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; }; -export function createApiRef( - config?: ApiRefConfig, -): ApiRef | { with(config: ApiRefConfig): ApiRef } { +export function createApiRef(config?: ApiRefConfig): + | (ApiRef & { readonly $$type: '@backstage/ApiRef' }) + | { + with(config: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; + } { if (config) { validateId(config.id); - return makeApiRef(config.id); + return makeApiRef(config); } return { - with(withConfig: ApiRefConfig): ApiRef { + with(withConfig: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + } { validateId(withConfig.id); - return makeApiRef(withConfig.id); + return makeApiRef(withConfig); }, }; } diff --git a/packages/frontend-plugin-api/src/apis/system/types.ts b/packages/frontend-plugin-api/src/apis/system/types.ts index 570e9a4e25..90e7365164 100644 --- a/packages/frontend-plugin-api/src/apis/system/types.ts +++ b/packages/frontend-plugin-api/src/apis/system/types.ts @@ -20,8 +20,9 @@ * @public */ export type ApiRef = { - readonly $$type: '@backstage/ApiRef'; + readonly $$type?: '@backstage/ApiRef'; readonly id: string; + readonly pluginId?: string; readonly T: T; }; diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx index 7596105810..a5cfb627ac 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.test.tsx @@ -38,7 +38,9 @@ describe('useApiHolder', () => { const renderedHook = renderHook(() => useApiHolder()); const holder = renderedHook.result.current; - expect(holder.get(createApiRef({ id: 'x' }))).toBeUndefined(); + expect( + holder.get(createApiRef().with({ id: 'x' })), + ).toBeUndefined(); }); }); @@ -53,7 +55,7 @@ describe('useApi', () => { const get = jest.fn(() => 'my-api-impl'); context.set({ 1: { get } }); - const apiRef = createApiRef({ id: 'x' }); + const apiRef = createApiRef().with({ id: 'x' }); const renderedHook = renderHook(() => useApi(apiRef)); const value = renderedHook.result.current; diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts index 8043752687..8e9c35afc8 100644 --- a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -20,7 +20,7 @@ import { createApiRef } from '../apis/system'; describe('ApiBlueprint', () => { it('should create an extension with sensible defaults', () => { - const api = createApiRef<{ foo: string }>({ id: 'test' }); + const api = createApiRef<{ foo: string }>().with({ id: 'test' }); const extension = ApiBlueprint.make({ params: defineParams => @@ -57,8 +57,8 @@ describe('ApiBlueprint', () => { }); it('should properly type the API factory', () => { - const fooApi = createApiRef<{ foo: string }>({ id: 'foo' }); - const barApi = createApiRef<{ bar: string }>({ id: 'bar' }); + const fooApi = createApiRef<{ foo: string }>().with({ id: 'foo' }); + const barApi = createApiRef<{ bar: string }>().with({ id: 'bar' }); expect('test').not.toBe('failing without assertions'); @@ -152,7 +152,7 @@ describe('ApiBlueprint', () => { }); it('should create an extension with custom factory', () => { - const api = createApiRef<{ foo: string }>({ id: 'test' }); + const api = createApiRef<{ foo: string }>().with({ id: 'test' }); const factory = jest.fn(() => ({ foo: 'bar' })); const extension = ApiBlueprint.makeWithOverrides({ diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index c546f265be..9e01257c5c 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -200,7 +200,9 @@ export type FormFieldExtensionData< }; // @alpha (undocumented) -export const formFieldsApiRef: ApiRef; +export const formFieldsApiRef: ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; // @alpha (undocumented) export type FormValidation = { diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index daec52a7e3..d40b0278bf 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -208,7 +208,9 @@ export type ReviewStepProps = { export type ScaffolderApi = ScaffolderApi_2; // @public (undocumented) -export const scaffolderApiRef: ApiRef; +export const scaffolderApiRef: ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; // @public @deprecated (undocumented) export type ScaffolderDryRunOptions = ScaffolderDryRunOptions_2; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 7c96a74c8b..b2cd1bbcbe 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -479,7 +479,9 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{ }>; // @alpha (undocumented) -export const formDecoratorsApiRef: ApiRef; +export const formDecoratorsApiRef: ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; export { formFieldsApiRef }; diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 5c0fffbcc1..17fd25e646 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -528,7 +528,9 @@ export type RouterProps = { export type ScaffolderApi = ScaffolderApi_2; // @public @deprecated (undocumented) -export const scaffolderApiRef: ApiRef; +export const scaffolderApiRef: ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; // @public @deprecated export class ScaffolderClient extends ScaffolderClient_2 {}