From fa0277093e20f51581e3202a66b3bf6c345aef6c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Mar 2026 12:30:47 +0100 Subject: [PATCH] Address review feedback - Remove {@link} from @deprecated tag (freben feedback) - Use Object.hasOwn instead of hasOwnProperty - Use NotImplementedError instead of plain Error - Add @backstage/errors dependency - Add tests for withApis in core-compat-api Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/core-compat-api/package.json | 1 + .../core-compat-api/src/withApis.test.tsx | 85 +++++++++++++++++++ packages/core-compat-api/src/withApis.tsx | 15 ++-- .../src/apis/system/useApi.tsx | 2 +- yarn.lock | 1 + 5 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 packages/core-compat-api/src/withApis.test.tsx diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 605c6b2bd9..67ad3f1c97 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-app-react": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", diff --git a/packages/core-compat-api/src/withApis.test.tsx b/packages/core-compat-api/src/withApis.test.tsx new file mode 100644 index 0000000000..eb13ec76ee --- /dev/null +++ b/packages/core-compat-api/src/withApis.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2026 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 { createApiRef } from '@backstage/frontend-plugin-api'; +import { + TestApiProvider, + withLogCollector, +} from '@backstage/frontend-test-utils'; +import { render, screen } from '@testing-library/react'; +import { withApis } from './withApis'; + +describe('withApis', () => { + type MyApi = () => string; + const myApiRef = createApiRef({ id: 'my-api' }); + + const MyComponent = withApis({ getMessage: myApiRef })(({ getMessage }) => { + return

message: {getMessage()}

; + }); + + it('should inject APIs as props and set display name', () => { + render( + 'hello']]}> + + , + ); + + expect(screen.getByText('message: hello')).toBeInTheDocument(); + expect(MyComponent.displayName).toBe('withApis(Component)'); + }); + + it('should ignore properties from the prototype', () => { + const otherRef = createApiRef({ id: 'other' }); + const proto = { other: otherRef }; + const props = { getMessage: { enumerable: true, value: myApiRef } }; + const obj = Object.create(proto, props) as { + getMessage: typeof myApiRef; + other: typeof otherRef; + }; + + const WeirdComponent = withApis(obj)(({ getMessage }) => { + return

message: {getMessage()}

; + }); + + render( + 'hello']]}> + + , + ); + + expect(screen.getByText('message: hello')).toBeInTheDocument(); + }); + + it('should throw NotImplementedError if the API is not available', () => { + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).toThrow('No implementation available for apiRef{my-api}'); + }).error, + ).toEqual( + expect.arrayContaining([ + expect.stringContaining( + 'No implementation available for apiRef{my-api}', + ), + ]), + ); + }); +}); diff --git a/packages/core-compat-api/src/withApis.tsx b/packages/core-compat-api/src/withApis.tsx index a06b5cd7a9..fc7fc548ee 100644 --- a/packages/core-compat-api/src/withApis.tsx +++ b/packages/core-compat-api/src/withApis.tsx @@ -15,11 +15,8 @@ */ import { ComponentType, PropsWithChildren } from 'react'; -import { - ApiRef, - TypesToApiRefs, - useApiHolder, -} from '@backstage/frontend-plugin-api'; +import { TypesToApiRefs, useApiHolder } from '@backstage/frontend-plugin-api'; +import { NotImplementedError } from '@backstage/errors'; /** * Wrapper for giving component an API context. @@ -37,12 +34,14 @@ export function withApis(apis: TypesToApiRefs) { const impls = {} as T; for (const key in apis) { - if (apis.hasOwnProperty(key)) { - const ref: ApiRef = apis[key]; + if (Object.hasOwn(apis, key)) { + const ref = apis[key]; const api = apiHolder.get(ref); if (!api) { - throw new Error(`No implementation available for ${ref}`); + throw new NotImplementedError( + `No implementation available for ${ref}`, + ); } impls[key] = api; } diff --git a/packages/frontend-plugin-api/src/apis/system/useApi.tsx b/packages/frontend-plugin-api/src/apis/system/useApi.tsx index fecf826c94..5a8f5d5dab 100644 --- a/packages/frontend-plugin-api/src/apis/system/useApi.tsx +++ b/packages/frontend-plugin-api/src/apis/system/useApi.tsx @@ -57,7 +57,7 @@ export function useApi(apiRef: ApiRef): T { * Wrapper for giving component an API context. * * @param apis - APIs for the context. - * @deprecated Use {@link withApis} from `@backstage/core-compat-api` instead. + * @deprecated Use `withApis` from `@backstage/core-compat-api` instead. * @public */ export function withApis(apis: TypesToApiRefs) { diff --git a/yarn.lock b/yarn.lock index 180d0d7521..b6c5872c67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3580,6 +3580,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^"