From 72dd26a3a69760fb2acc805fbe78d90878033b31 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Mar 2026 22:13:45 +0100 Subject: [PATCH] frontend-app-api: route sign-in errors through app boundary Rethrow sign-in bootstrap failures from inside the prepared sign-in tree so the app root extension boundary handles them instead of createApp keeping its own error state. This keeps bootstrap error handling aligned with the rest of the extension tree. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../frontend-defaults/src/createApp.test.tsx | 80 +++++++++++++++++++ packages/frontend-defaults/src/createApp.tsx | 8 +- plugins/app/src/extensions/AppRoot.tsx | 31 +++---- 3 files changed, 98 insertions(+), 21 deletions(-) diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 540fb5e41c..9b3733b5af 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -187,6 +187,86 @@ describe('createApp', () => { ).resolves.toBeInTheDocument(); }); + it('should surface sign-in bootstrap errors through the app root boundary', async () => { + const identityApi = { + getProfileInfo: async () => ({ displayName: 'Test User' }), + getBackstageIdentity: async () => ({ + type: 'user' as const, + userEntityRef: 'user:default/test-user', + ownershipEntityRefs: ['user:default/test-user'], + }), + getCredentials: async () => ({ token: 'token' }), + signOut: async () => {}, + }; + const featureFlagsApi = { + isActive: jest.fn(() => { + throw new Error('sign-in bootstrap failed'); + }), + registerFlag: jest.fn(), + getRegisteredFlags: () => [], + save: jest.fn(), + } as unknown as typeof featureFlagsApiRef.T; + + const app = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPluginOriginal, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => featureFlagsApi, + }), + }), + appPluginOriginal.getExtension('sign-in-page:app').override({ + factory: () => { + function SignInPage(props: { + onSignInSuccess(identity: IdentityApi): void; + }) { + useEffect(() => { + props.onSignInSuccess(identityApi); + }, [props]); + + return
Custom Sign In
; + } + + return [signInPageComponentDataRef(SignInPage)]; + }, + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [{ name: 'test-flag' }], + extensions: [ + PageBlueprint.make({ + if: { featureFlags: { $contains: 'test-flag' } }, + params: { + path: '/', + loader: async () =>
Flagged Page
, + }, + }), + ], + }), + ], + }); + + await renderWithEffects(app.createRoot()); + + await expect( + screen.findByText(/Error in app/), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByText('sign-in bootstrap failed'), + ).resolves.toBeInTheDocument(); + }); + it('should deduplicate features keeping the last received one', async () => { const duplicatedFeatureId = 'test'; const app = createApp({ diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 38eadf6509..310cd48f17 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JSX, lazy, ReactNode, Suspense, useReducer, useState } from 'react'; +import { JSX, lazy, ReactNode, Suspense, useReducer } from 'react'; import { ConfigApi, coreExtensionData, @@ -151,13 +151,8 @@ function PreparedAppRoot(props: { }): JSX.Element { const signIn = props.preparedApp.getSignIn(); const SignIn = signIn.Component; - const [finalizeError, setFinalizeError] = useState(); const [, triggerRerender] = useReducer((count: number) => count + 1, 0); - if (finalizeError) { - throw finalizeError; - } - const finalizedApp: FinalizedSpecializedApp | undefined = props.preparedApp.tryFinalize(); @@ -167,7 +162,6 @@ function PreparedAppRoot(props: { onReady={() => { triggerRerender(); }} - onError={setFinalizeError} /> ); } diff --git a/plugins/app/src/extensions/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index a970ae4463..af6b134fc1 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -22,6 +22,7 @@ import { JSX, } from 'react'; import { + ExtensionBoundary, coreExtensionData, discoveryApiRef, fetchApiRef, @@ -84,7 +85,7 @@ export const AppRoot = createExtension({ ), }, output: [coreExtensionData.reactElement], - factory({ inputs, apis }) { + factory({ inputs, apis, node }) { if (isProtectedApp()) { const identityApi = apis.get(identityApiRef); if (!identityApi) { @@ -123,19 +124,21 @@ export const AppRoot = createExtension({ return [ coreExtensionData.reactElement( - - el.get(coreExtensionData.reactElement), - )} - > - {content} - , + + + el.get(coreExtensionData.reactElement), + )} + > + {content} + + , ), ]; },