From 89f83827c959ff771d21a94bcd299f8cf36852d8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Mar 2026 22:39:25 +0100 Subject: [PATCH] frontend-app-api: expose bootstrap app state Expose the prepared bootstrap tree together with subscription-based phase updates so createApp can render from app state directly instead of forcing rerenders through a sign-in callback. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../prepare-specialized-app-signin-flow.md | 2 +- packages/frontend-app-api/src/wiring/index.ts | 2 +- packages/frontend-defaults/src/createApp.tsx | 22 ++++++++----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.changeset/prepare-specialized-app-signin-flow.md b/.changeset/prepare-specialized-app-signin-flow.md index 746df0d4fe..be7ddca934 100644 --- a/.changeset/prepare-specialized-app-signin-flow.md +++ b/.changeset/prepare-specialized-app-signin-flow.md @@ -3,4 +3,4 @@ '@backstage/frontend-defaults': patch --- -Adds `prepareSpecializedApp` as a new two-phase app wiring API for rendering a sign-in page before full app finalization. The sign-in step is now exposed as a bootstrap component whose `onReady` callback signals that the prepared app can now be finalized, while the opaque reusable `sessionState` is stored internally on the prepared app and returned from `tryFinalize()` and `finalize()`. That session state can also be passed into a future `prepareSpecializedApp` call to skip sign-in and reuse the prepared session. The existing `createSpecializedApp` API is now deprecated and backed by `prepareSpecializedApp().finalize()`, while `createApp` has been updated to use the same prepare/finalize flow. +Adds `prepareSpecializedApp` as a new two-phase app wiring API for rendering a bootstrap tree before full app finalization. The bootstrap phase is now exposed as a partial app tree that consumers can render while subscribing to phase transitions, while the opaque reusable `sessionState` is stored internally on the prepared app and returned from `getFinalizedApp()` and `finalize()`. That session state can also be passed into a future `prepareSpecializedApp` call to skip sign-in and reuse the prepared session. The existing `createSpecializedApp` API is now deprecated and backed by `prepareSpecializedApp().finalize()`, while `createApp` has been updated to use the same prepare/finalize flow. diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index d4db40bfee..8f55d1f3cf 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -15,10 +15,10 @@ */ export { + type BootstrapSpecializedApp, type FinalizedSpecializedApp, prepareSpecializedApp, type PreparedSpecializedApp, - type PreparedSpecializedAppSignInProps, type SpecializedAppSessionState, createSpecializedApp, type CreateSpecializedAppOptions, diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 310cd48f17..41c70adcf5 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 } from 'react'; +import { JSX, lazy, ReactNode, Suspense, useSyncExternalStore } from 'react'; import { ConfigApi, coreExtensionData, @@ -149,21 +149,17 @@ export function createApp(options?: CreateAppOptions): { function PreparedAppRoot(props: { preparedApp: PreparedSpecializedApp; }): JSX.Element { - const signIn = props.preparedApp.getSignIn(); - const SignIn = signIn.Component; - const [, triggerRerender] = useReducer((count: number) => count + 1, 0); - + const bootstrapApp = props.preparedApp.getBootstrapApp(); const finalizedApp: FinalizedSpecializedApp | undefined = - props.preparedApp.tryFinalize(); + useSyncExternalStore( + props.preparedApp.subscribe, + props.preparedApp.getFinalizedApp, + ); if (!finalizedApp) { - return ( - { - triggerRerender(); - }} - /> - ); + return bootstrapApp.tree.root.instance!.getData( + coreExtensionData.reactElement, + )!; } const errorPage = maybeCreateErrorPage(finalizedApp);