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 {}