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 new file mode 100644 index 0000000000..6ae300f556 --- /dev/null +++ b/.changeset/opaque-api-ref-type.md @@ -0,0 +1,7 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added a builder form for `createApiRef` in the new frontend system and deprecated the direct `createApiRef({ ... })` call in favor of `createApiRef().with({ ... })`. The builder form now also preserves literal API ref IDs in the resulting `ApiRef` type. + +The `createApiRef().with({ ... })` form can also use an explicit `pluginId` to declare API ownership without encoding the plugin ID into the API ref ID, while keeping that metadata internal to runtime handling. 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.test.ts b/packages/core-plugin-api/src/apis/system/ApiRef.test.ts index dab872236f..5bfb83ce3e 100644 --- a/packages/core-plugin-api/src/apis/system/ApiRef.test.ts +++ b/packages/core-plugin-api/src/apis/system/ApiRef.test.ts @@ -19,9 +19,15 @@ import { createApiRef } from './ApiRef'; describe('ApiRef', () => { it('should be created', () => { const ref = createApiRef({ id: 'abc' }); + expect(ref.$$type).toBe('@backstage/ApiRef'); expect(ref.id).toBe('abc'); expect(String(ref)).toBe('apiRef{abc}'); - expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); + expect(ref.T).toBeNull(); + }); + + it('should not accept pluginId in the core createApiRef config', () => { + // @ts-expect-error pluginId is not supported in core-plugin-api + createApiRef({ id: 'abc', pluginId: 'test' }); }); it('should reject invalid ids', () => { 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 51c31aca25..5a011c058e 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -16,6 +16,7 @@ import { AppTreeApi, + type ApiRef, appTreeApiRef, coreExtensionData, createExtension, @@ -166,10 +167,13 @@ describe('createSpecializedApp', () => { "factories": Map { "core.featureflags" => { "factory": { - "api": ApiRefImpl { - "config": { - "id": "core.featureflags", - }, + "api": { + "$$type": "@backstage/ApiRef", + "T": null, + "id": "core.featureflags", + "pluginId": "app", + "toString": [Function], + "version": "v1", }, "deps": {}, "factory": [Function], @@ -178,10 +182,13 @@ describe('createSpecializedApp', () => { }, "core.app-tree" => { "factory": { - "api": ApiRefImpl { - "config": { - "id": "core.app-tree", - }, + "api": { + "$$type": "@backstage/ApiRef", + "T": null, + "id": "core.app-tree", + "pluginId": "app", + "toString": [Function], + "version": "v1", }, "deps": {}, "factory": [Function], @@ -190,10 +197,13 @@ describe('createSpecializedApp', () => { }, "core.config" => { "factory": { - "api": ApiRefImpl { - "config": { - "id": "core.config", - }, + "api": { + "$$type": "@backstage/ApiRef", + "T": null, + "id": "core.config", + "pluginId": "app", + "toString": [Function], + "version": "v1", }, "deps": {}, "factory": [Function], @@ -202,10 +212,13 @@ describe('createSpecializedApp', () => { }, "core.route-resolution" => { "factory": { - "api": ApiRefImpl { - "config": { - "id": "core.route-resolution", - }, + "api": { + "$$type": "@backstage/ApiRef", + "T": null, + "id": "core.route-resolution", + "pluginId": "app", + "toString": [Function], + "version": "v1", }, "deps": {}, "factory": [Function], @@ -214,10 +227,13 @@ describe('createSpecializedApp', () => { }, "core.identity" => { "factory": { - "api": ApiRefImpl { - "config": { - "id": "core.identity", - }, + "api": { + "$$type": "@backstage/ApiRef", + "T": null, + "id": "core.identity", + "pluginId": "app", + "toString": [Function], + "version": "v1", }, "deps": {}, "factory": [Function], @@ -359,6 +375,150 @@ 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 reject unsupported opaque ApiRef versions', () => { + const testApiRef = { + $$type: '@backstage/ApiRef', + version: 'v0', + id: 'shared.api', + pluginId: 'owner', + T: null as unknown as { value: string }, + toString() { + return 'apiRef{shared.api}'; + }, + } as ApiRef<{ value: string }, 'shared.api'> & { + readonly $$type: '@backstage/ApiRef'; + readonly version: 'v0'; + readonly pluginId: 'owner'; + }; + + expect(() => + 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' }), + }), + }), + ], + }), + ], + }), + ).toThrow("Invalid opaque type instance, got version 'v0', expected 'v1'"); + }); + + it('should not infer app ownership from core-prefixed API ids', () => { + const testApiRef = createApiRef<{ value: string }>({ id: 'core.shared' }); + + const app = createSpecializedApp({ + features: [ + makeAppPlugin(), + createFrontendPlugin({ + pluginId: 'other-before', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'other' }), + }), + }), + ], + }), + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: testApiRef, + deps: {}, + factory: () => ({ value: 'app' }), + }), + }), + ], + }), + ], + }); + + expect(app.errors).toEqual([ + expect.objectContaining({ + code: 'API_FACTORY_CONFLICT', + message: expect.stringContaining("API 'core.shared'"), + }), + ]); + + expect(app.apis.get(testApiRef)).toEqual({ value: 'other' }); + }); + 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..0c47dca6f6 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -43,6 +43,7 @@ import { import { ApiFactoryRegistry, ApiResolver } from '@backstage/core-app-api'; import { createExtensionDataContainer, + OpaqueApiRef, OpaqueFrontendPlugin, } from '@internal/frontend'; @@ -401,14 +402,14 @@ 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); // This allows modules to override factories provided by the plugin, but // it rejects API overrides from other plugins. In the event of a - // conflict, the owning plugin is attempted to be inferred from the API - // reference ID. + // conflict, the owning plugin is inferred from the explicit pluginId or + // legacy plugin-prefixed API reference ID. if (existingFactory && existingFactory.pluginId !== pluginId) { const shouldReplace = ownerId === pluginId && existingFactory.pluginId !== ownerId; @@ -455,14 +456,19 @@ 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 }): string { + if (OpaqueApiRef.isType(apiRef)) { + const { pluginId } = OpaqueApiRef.toInternal(apiRef); + if (pluginId) { + return pluginId; + } + } + + const apiRefId = apiRef.id; const [prefix, ...rest] = apiRefId.split('.'); if (!prefix) { return apiRefId; } - if (prefix === 'core') { - return 'app'; - } if (prefix === 'plugin' && rest[0]) { return rest[0]; } diff --git a/packages/frontend-internal/src/apis/OpaqueApiRef.ts b/packages/frontend-internal/src/apis/OpaqueApiRef.ts new file mode 100644 index 0000000000..8a1b058641 --- /dev/null +++ b/packages/frontend-internal/src/apis/OpaqueApiRef.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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 type { ApiRef } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from '@internal/opaque'; + +export const OpaqueApiRef = OpaqueType.create<{ + public: ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; + versions: { + readonly version: 'v1'; + readonly pluginId?: string; + }; +}>({ + type: '@backstage/ApiRef', + versions: ['v1'], +}); diff --git a/packages/frontend-internal/src/apis/index.ts b/packages/frontend-internal/src/apis/index.ts new file mode 100644 index 0000000000..f445683652 --- /dev/null +++ b/packages/frontend-internal/src/apis/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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 * from './OpaqueApiRef'; diff --git a/packages/frontend-internal/src/index.ts b/packages/frontend-internal/src/index.ts index 38bfdc53f8..447b488fca 100644 --- a/packages/frontend-internal/src/index.ts +++ b/packages/frontend-internal/src/index.ts @@ -15,4 +15,5 @@ */ export * from './routing'; +export * from './apis'; 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..921ad1039c 100644 --- a/packages/frontend-plugin-api/report-alpha.api.md +++ b/packages/frontend-plugin-api/report-alpha.api.md @@ -24,7 +24,12 @@ export type PluginWrapperApi = { }; // @public -export const pluginWrapperApiRef: ApiRef; +export const pluginWrapperApiRef: ApiRef< + PluginWrapperApi, + 'core.plugin-wrapper' +> & { + 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 768f26c0eb..e3113e1d9b 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -31,7 +31,9 @@ export type AlertApi = { }; // @public -export const alertApiRef: ApiRef; +export const alertApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type AlertMessage = { @@ -46,7 +48,9 @@ export type AnalyticsApi = { }; // @public -export const analyticsApiRef: ApiRef; +export const analyticsApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export const AnalyticsContext: (options: { @@ -187,9 +191,10 @@ export type ApiHolder = { }; // @public -export type ApiRef = { - id: string; - T: T; +export type ApiRef = { + readonly $$type?: '@backstage/ApiRef'; + readonly id: TId; + readonly T: T; }; // @public @@ -212,7 +217,9 @@ export type AppLanguageApi = { }; // @public (undocumented) -export const appLanguageApiRef: ApiRef; +export const appLanguageApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export interface AppNode { @@ -285,7 +292,9 @@ export type AppThemeApi = { }; // @public -export const appThemeApiRef: ApiRef; +export const appThemeApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export interface AppTree { @@ -305,12 +314,17 @@ export interface AppTreeApi { } // @public -export const appTreeApiRef: ApiRef_2; +export const appTreeApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const atlassianAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export const atlassianAuthApiRef: ApiRef_2< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi, + 'core.auth.atlassian' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type AuthProviderInfo = { @@ -348,20 +362,28 @@ export type BackstageUserIdentity = { }; // @public -export const bitbucketAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export const bitbucketAuthApiRef: ApiRef_2< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi, + 'core.auth.bitbucket' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const bitbucketServerAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export const bitbucketServerAuthApiRef: ApiRef_2< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi, + 'core.auth.bitbucket-server' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type ConfigApi = Config; // @public -export const configApiRef: ApiRef; +export const configApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export interface ConfigurableExtensionDataRef< @@ -415,8 +437,22 @@ export function createApiFactory( instance: Impl, ): ApiFactory; +// @public @deprecated +export function createApiRef(config: ApiRefConfig): ApiRef & { + readonly $$type: '@backstage/ApiRef'; +}; + // @public -export function createApiRef(config: ApiRefConfig): ApiRef; +export function createApiRef(): { + with( + config: ApiRefConfig & { + id: TId; + pluginId?: string; + }, + ): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; +}; // @public export function createExtension< @@ -869,7 +905,9 @@ export interface DialogApiDialog { } // @public -export const dialogApiRef: ApiRef_2; +export const dialogApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type DiscoveryApi = { @@ -877,7 +915,9 @@ export type DiscoveryApi = { }; // @public -export const discoveryApiRef: ApiRef; +export const discoveryApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type ErrorApi = { @@ -901,7 +941,9 @@ export type ErrorApiErrorContext = { }; // @public -export const errorApiRef: ApiRef; +export const errorApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export const ErrorDisplay: { @@ -1285,7 +1327,12 @@ export interface FeatureFlagsApi { } // @public -export const featureFlagsApiRef: ApiRef; +export const featureFlagsApiRef: ApiRef_2< + FeatureFlagsApi, + 'core.featureflags' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type FeatureFlagsSaveOptions = { @@ -1317,7 +1364,9 @@ export type FetchApi = { }; // @public -export const fetchApiRef: ApiRef; +export const fetchApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export type FrontendFeature = @@ -1390,27 +1439,36 @@ export type FrontendPluginInfoOptions = { }; // @public -export const githubAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export const githubAuthApiRef: ApiRef_2< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi, + 'core.auth.github' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const gitlabAuthApiRef: ApiRef< +export const gitlabAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.gitlab' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const googleAuthApiRef: ApiRef< +export const googleAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.google' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public @deprecated export type IconComponent = ComponentType<{ @@ -1430,7 +1488,9 @@ export interface IconsApi { } // @public -export const iconsApiRef: ApiRef_2; +export const iconsApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type IdentityApi = { @@ -1443,16 +1503,21 @@ export type IdentityApi = { }; // @public -export const identityApiRef: ApiRef; +export const identityApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const microsoftAuthApiRef: ApiRef< +export const microsoftAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.microsoft' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public @deprecated export const NavItemBlueprint: ExtensionBlueprint_2<{ @@ -1515,7 +1580,12 @@ export type OAuthRequestApi = { }; // @public -export const oauthRequestApiRef: ApiRef; +export const oauthRequestApiRef: ApiRef_2< + OAuthRequestApi, + 'core.oauthrequest' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type OAuthRequester = ( @@ -1532,22 +1602,28 @@ export type OAuthRequesterOptions = { export type OAuthScope = string | string[]; // @public -export const oktaAuthApiRef: ApiRef< +export const oktaAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.okta' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public -export const oneloginAuthApiRef: ApiRef< +export const oneloginAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.onelogin' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type OpenIdConnectApi = { @@ -1555,9 +1631,12 @@ export type OpenIdConnectApi = { }; // @public -export const openshiftAuthApiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; +export const openshiftAuthApiRef: ApiRef_2< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi, + 'core.auth.openshift' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export interface OverridableExtensionDefinition< @@ -1845,7 +1924,12 @@ export type PluginHeaderActionsApi = { }; // @public -export const pluginHeaderActionsApiRef: ApiRef_2; +export const pluginHeaderActionsApiRef: ApiRef_2< + PluginHeaderActionsApi, + 'core.plugin-header-actions' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export interface PluginOptions< @@ -1887,7 +1971,12 @@ export type PluginWrapperApi = { }; // @public -export const pluginWrapperApiRef: ApiRef_2; +export const pluginWrapperApiRef: ApiRef_2< + PluginWrapperApi, + 'core.plugin-wrapper' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export const PluginWrapperBlueprint: ExtensionBlueprint_2<{ @@ -1993,7 +2082,12 @@ export interface RouteResolutionApi { } // @public -export const routeResolutionApiRef: ApiRef_2; +export const routeResolutionApiRef: ApiRef_2< + RouteResolutionApi, + 'core.route-resolution' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type SessionApi = { @@ -2031,7 +2125,9 @@ export interface StorageApi { } // @public -export const storageApiRef: ApiRef; +export const storageApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public export type StorageValueSnapshot = @@ -2121,7 +2217,12 @@ export interface SwappableComponentsApi { } // @public -export const swappableComponentsApiRef: ApiRef_2; +export const swappableComponentsApiRef: ApiRef_2< + SwappableComponentsApi, + 'core.swappable-components' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export type TranslationApi = { @@ -2142,7 +2243,9 @@ export type TranslationApi = { }; // @public (undocumented) -export const translationApiRef: ApiRef; +export const translationApiRef: ApiRef_2 & { + readonly $$type: '@backstage/ApiRef'; +}; // @public (undocumented) export type TranslationFunction< @@ -2332,13 +2435,16 @@ export const useTranslationRef: ( }; // @public -export const vmwareCloudAuthApiRef: ApiRef< +export const vmwareCloudAuthApiRef: ApiRef_2< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & - SessionApi ->; + SessionApi, + 'core.auth.vmware-cloud' +> & { + readonly $$type: '@backstage/ApiRef'; +}; // @public @deprecated export function withApis( diff --git a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts index afdcb6a617..3dd7c8e443 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AlertApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef, ApiRef } from '../system'; +import { createApiRef } from '../system'; import { Observable } from '@backstage/types'; /** @@ -51,6 +51,7 @@ export type AlertApi = { * * @public */ -export const alertApiRef: ApiRef = createApiRef({ +export const alertApiRef = 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..bd5d9a4394 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { AnalyticsContextValue } from '../../analytics/types'; /** @@ -151,6 +151,7 @@ export type AnalyticsApi = { * * @public */ -export const analyticsApiRef: ApiRef = createApiRef({ +export const analyticsApiRef = 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..1b1b7437b5 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppLanguageApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { Observable } from '@backstage/types'; /** @public */ @@ -31,6 +31,7 @@ export type AppLanguageApi = { /** * @public */ -export const appLanguageApiRef: ApiRef = createApiRef({ +export const appLanguageApiRef = 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..66669e8cab 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -15,7 +15,7 @@ */ import { ReactNode } from 'react'; -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { Observable } from '@backstage/types'; /** @@ -82,6 +82,7 @@ export type AppThemeApi = { * * @public */ -export const appThemeApiRef: ApiRef = createApiRef({ +export const appThemeApiRef = 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..8cc1638d90 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ConfigApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import type { Config } from '@backstage/config'; /** @@ -29,6 +29,7 @@ export type ConfigApi = Config; * * @public */ -export const configApiRef: ApiRef = createApiRef({ +export const configApiRef = 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..824174c8f6 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DiscoveryApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; /** * The discovery API is used to provide a mechanism for plugins to @@ -50,6 +50,7 @@ export type DiscoveryApi = { * * @public */ -export const discoveryApiRef: ApiRef = createApiRef({ +export const discoveryApiRef = 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..e7be718326 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ErrorApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { Observable } from '@backstage/types'; /** @@ -86,6 +86,7 @@ export type ErrorApi = { * * @public */ -export const errorApiRef: ApiRef = createApiRef({ +export const errorApiRef = 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..b7d1c4ccaa 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FeatureFlagsApi.ts @@ -16,7 +16,7 @@ /* We want to maintain the same information as an enum, so we disable the redeclaration warning */ /* eslint-disable @typescript-eslint/no-redeclare */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; /** * Feature flag descriptor. @@ -121,6 +121,7 @@ export interface FeatureFlagsApi { * * @public */ -export const featureFlagsApiRef: ApiRef = createApiRef({ +export const featureFlagsApiRef = 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..5299878c34 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/FetchApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; /** * A wrapper for the fetch API, that has additional behaviors such as the @@ -46,6 +46,7 @@ export type FetchApi = { * * @public */ -export const fetchApiRef: ApiRef = createApiRef({ +export const fetchApiRef = 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..70b6ebf56a 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IdentityApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { BackstageUserIdentity, ProfileInfo } from './auth'; /** @@ -51,6 +51,7 @@ export type IdentityApi = { * * @public */ -export const identityApiRef: ApiRef = createApiRef({ +export const identityApiRef = 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..d94ab44d15 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -15,7 +15,7 @@ */ import { Observable } from '@backstage/types'; -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { AuthProviderInfo } from './auth'; /** @@ -126,6 +126,7 @@ export type OAuthRequestApi = { * * @public */ -export const oauthRequestApiRef: ApiRef = createApiRef({ +export const oauthRequestApiRef = 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..1624f8402c 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/StorageApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { JsonValue, Observable } from '@backstage/types'; /** @@ -105,6 +105,7 @@ export interface StorageApi { * * @public */ -export const storageApiRef: ApiRef = createApiRef({ +export const storageApiRef = 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..ab50baf5e1 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/TranslationApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { Expand, ExpandRecursive, Observable } from '@backstage/types'; import { TranslationRef } from '../../translation'; import { JSX } from 'react'; @@ -358,6 +358,7 @@ export type TranslationApi = { /** * @public */ -export const translationApiRef: ApiRef = createApiRef({ +export const translationApiRef = 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..d686ed5d08 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -16,7 +16,7 @@ /* We want to maintain the same information as an enum, so we disable the redeclaration warning */ /* eslint-disable @typescript-eslint/no-redeclare */ -import { ApiRef, createApiRef } from '../system'; +import { createApiRef } from '../system'; import { IconComponent, IconElement } from '../../icons/types'; import { Observable } from '@backstage/types'; @@ -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', + * }) */ /** @@ -333,14 +336,15 @@ export type SessionApi = { * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, * email and expiration information. Do not rely on any other fields, as they might not be present. */ -export const googleAuthApiRef: ApiRef< +export const googleAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.google', + pluginId: 'app', }); /** @@ -352,10 +356,11 @@ export const googleAuthApiRef: ApiRef< * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} * for a full list of supported scopes. */ -export const githubAuthApiRef: ApiRef< +export const githubAuthApiRef = createApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.github', + pluginId: 'app', }); /** @@ -367,14 +372,15 @@ export const githubAuthApiRef: ApiRef< * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} * for a full list of supported scopes. */ -export const oktaAuthApiRef: ApiRef< +export const oktaAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.okta', + pluginId: 'app', }); /** @@ -386,14 +392,15 @@ export const oktaAuthApiRef: ApiRef< * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} * for a full list of supported scopes. */ -export const gitlabAuthApiRef: ApiRef< +export const gitlabAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.gitlab', + pluginId: 'app', }); /** @@ -406,14 +413,15 @@ export const gitlabAuthApiRef: ApiRef< * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} */ -export const microsoftAuthApiRef: ApiRef< +export const microsoftAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.microsoft', + pluginId: 'app', }); /** @@ -421,14 +429,15 @@ export const microsoftAuthApiRef: ApiRef< * * @public */ -export const oneloginAuthApiRef: ApiRef< +export const oneloginAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.onelogin', + pluginId: 'app', }); /** @@ -440,10 +449,11 @@ export const oneloginAuthApiRef: ApiRef< * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} * for a full list of supported scopes. */ -export const bitbucketAuthApiRef: ApiRef< +export const bitbucketAuthApiRef = createApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.bitbucket', + pluginId: 'app', }); /** @@ -455,10 +465,11 @@ export const bitbucketAuthApiRef: ApiRef< * See {@link https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes} * for a full list of supported scopes. */ -export const bitbucketServerAuthApiRef: ApiRef< +export const bitbucketServerAuthApiRef = createApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.bitbucket-server', + pluginId: 'app', }); /** @@ -470,10 +481,11 @@ export const bitbucketServerAuthApiRef: ApiRef< * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} * for a full list of supported scopes. */ -export const atlassianAuthApiRef: ApiRef< +export const atlassianAuthApiRef = createApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.atlassian', + pluginId: 'app', }); /** @@ -485,14 +497,15 @@ export const atlassianAuthApiRef: ApiRef< * For more info about VMware Cloud identity and access management: * - {@link https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-53D39337-D93A-4B84-BD18-DDF43C21479A.html} */ -export const vmwareCloudAuthApiRef: ApiRef< +export const vmwareCloudAuthApiRef = createApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().with({ id: 'core.auth.vmware-cloud', + pluginId: 'app', }); /** @@ -506,8 +519,9 @@ export const vmwareCloudAuthApiRef: ApiRef< * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} * for available scopes. */ -export const openshiftAuthApiRef: ApiRef< +export const openshiftAuthApiRef = createApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ +>().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 dab872236f..37bf0205be 100644 --- a/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts +++ b/packages/frontend-plugin-api/src/apis/system/ApiRef.test.ts @@ -15,13 +15,54 @@ */ import { createApiRef } from './ApiRef'; +import type { ApiRef as ApiRefType } from './types'; describe('ApiRef', () => { - it('should be created', () => { + it('should be created with config', () => { const ref = createApiRef({ id: 'abc' }); + expect(ref.$$type).toBe('@backstage/ApiRef'); expect(ref.id).toBe('abc'); expect(String(ref)).toBe('apiRef{abc}'); - expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}'); + expect(ref.T).toBeNull(); + }); + + it('should not accept pluginId with deprecated config form', () => { + expect(createApiRef({ id: 'abc' }).id).toBe('abc'); + + // @ts-expect-error pluginId is only supported through .with(...) + createApiRef({ id: 'abc', pluginId: 'test' }); + }); + + it('should keep the deprecated config form id wide', () => { + const ref = createApiRef({ id: 'abc' }); + const wideRef: ApiRefType = ref; + expect(wideRef.id).toBe('abc'); + + // @ts-expect-error deprecated config form should not infer literal ids + const literalRef: ApiRefType = ref; + expect(literalRef.id).toBe('abc'); + }); + + it('should be created with builder pattern', () => { + const ref = createApiRef().with({ id: 'abc', pluginId: 'test' }); + expect(ref.$$type).toBe('@backstage/ApiRef'); + expect(ref.id).toBe('abc'); + expect(String(ref)).toBe('apiRef{abc}'); + expect(ref.T).toBeNull(); + expect((ref as { pluginId?: string }).pluginId).toBe('test'); + + // @ts-expect-error pluginId is internal runtime metadata + expect(ref.pluginId).toBe('test'); + }); + + it('should infer literal ids with builder pattern', () => { + const ref = createApiRef().with({ id: 'abc', pluginId: 'test' }); + const literalRef: ApiRefType = ref; + expect(literalRef.id).toBe('abc'); + + // @ts-expect-error builder pattern should preserve literal ids + const wrongLiteralRef: ApiRefType = ref; + expect(wrongLiteralRef.id).toBe('abc'); }); it('should reject invalid ids', () => { @@ -47,4 +88,10 @@ describe('ApiRef', () => { ); } }); + + it('should reject invalid ids with builder pattern', () => { + expect(() => createApiRef().with({ id: '123' })).toThrow( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '123'`, + ); + }); }); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRef.ts b/packages/frontend-plugin-api/src/apis/system/ApiRef.ts index 0db3d89bd5..557341d98a 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 { OpaqueApiRef } from '@internal/frontend'; import type { ApiRef } from './types'; /** @@ -25,48 +26,110 @@ export type ApiRefConfig = { id: string; }; -class ApiRefImpl implements ApiRef { - constructor(private readonly config: ApiRefConfig) { - const valid = config.id - .split('.') - .flatMap(part => part.split('-')) - .every(part => part.match(/^[a-z][a-z0-9]*$/)); - if (!valid) { - throw new Error( - `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`, - ); - } - } +type ApiRefBuilderConfig = { + id: TId; + pluginId?: string; +}; - get id(): string { - return this.config.id; - } - - // Utility for getting type of an api, using `typeof apiRef.T` - get T(): T { - throw new Error(`tried to read ApiRef.T of ${this}`); - } - - toString() { - return `apiRef{${this.config.id}}`; +function validateId(id: string): void { + const valid = id + .split('.') + .flatMap(part => part.split('-')) + .every(part => part.match(/^[a-z][a-z0-9]*$/)); + if (!valid) { + throw new Error( + `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`, + ); } } +function makeApiRef( + config: ApiRefBuilderConfig, +): ApiRef & { readonly $$type: '@backstage/ApiRef' } { + return OpaqueApiRef.createInstance('v1', { + id: config.id, + ...(config.pluginId ? { pluginId: config.pluginId } : {}), + T: null as unknown as T, + toString() { + return `apiRef{${config.id}}`; + }, + }) as ApiRef & { readonly $$type: '@backstage/ApiRef' }; +} + /** - * Creates a reference to an API. The provided `id` is a stable identifier for - * the API implementation. + * Creates a reference to an API. * * @remarks * - * The frontend system infers the owning plugin for an API from the `id`. The + * The `id` is a stable identifier for the API implementation. The frontend + * system infers the owning plugin for an API from the `id`. When using the + * builder form, you can instead 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. * - * @param config - The descriptor of the API to reference. - * @returns An API reference. + * The recommended way to create an API reference is: + * + * ```ts + * const myApiRef = createApiRef().with({ + * id: 'my-api', + * pluginId: 'my-plugin', + * }); + * ``` + * + * The legacy way to create an API reference is: + * + * ```ts + * const myApiRef = createApiRef({ id: 'plugin.my.api' }); + * ``` + * * @public */ -export function createApiRef(config: ApiRefConfig): ApiRef { - return new ApiRefImpl(config); +/** + * 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 API reference + * configuration. + * + * @public + */ +export function createApiRef(): { + with( + config: ApiRefConfig & { id: TId; pluginId?: string }, + ): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; +}; +export function createApiRef(config?: ApiRefConfig): + | (ApiRef & { readonly $$type: '@backstage/ApiRef' }) + | { + with( + config: ApiRefConfig & { id: TId; pluginId?: string }, + ): ApiRef & { + readonly $$type: '@backstage/ApiRef'; + }; + } { + if (config) { + validateId(config.id); + return makeApiRef(config); + } + return { + with( + withConfig: ApiRefConfig & { id: TId; pluginId?: string }, + ): ApiRef & { readonly $$type: '@backstage/ApiRef' } { + validateId(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 96614c320c..5bc6136bdf 100644 --- a/packages/frontend-plugin-api/src/apis/system/types.ts +++ b/packages/frontend-plugin-api/src/apis/system/types.ts @@ -19,9 +19,10 @@ * * @public */ -export type ApiRef = { - id: string; - T: T; +export type ApiRef = { + readonly $$type?: '@backstage/ApiRef'; + readonly id: TId; + 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/app-react/report.api.md b/plugins/app-react/report.api.md index a4e63e254b..4e3fff6e42 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -86,7 +86,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ }; output: ExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} @@ -97,7 +97,7 @@ export const IconBundleBlueprint: ExtensionBlueprint<{ dataRefs: { icons: ConfigurableExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 0262c4c1ef..8347e5f447 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -478,7 +478,7 @@ const appPlugin: OverridableFrontendPlugin< icons: ExtensionInput< ConfigurableExtensionDataRef< { - [x: string]: IconComponent | IconElement; + [x: string]: IconElement | IconComponent; }, 'core.icons', {} 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 {}