frontend-app-api: internalize prepared session state

Move prepared app session ownership into frontend-app-api so bootstrap only signals readiness while callers use tryFinalize or finalize to read the finalized app state. This reduces createApp boilerplate and keeps the specialized app lifecycle centered in the lower-level API.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-12 21:59:25 +01:00
parent 94e91d9de3
commit f4c03772a4
3 changed files with 24 additions and 46 deletions
@@ -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. Session preparation now resolves to an opaque reusable `sessionState`, which is returned from `getSignIn().ready` and from `finalize()`, and can 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 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.
@@ -15,8 +15,11 @@
*/
export {
type FinalizedSpecializedApp,
prepareSpecializedApp,
type PreparedSpecializedApp,
type PreparedSpecializedAppSignInProps,
type SpecializedAppSessionState,
createSpecializedApp,
type CreateSpecializedAppOptions,
} from './createSpecializedApp';
+20 -45
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JSX, lazy, ReactNode, Suspense, useEffect, useState } from 'react';
import { JSX, lazy, ReactNode, Suspense, useReducer, useState } from 'react';
import {
ConfigApi,
coreExtensionData,
@@ -30,6 +30,7 @@ import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseU
import { ConfigReader } from '@backstage/config';
import {
CreateAppRouteBinder,
FinalizedSpecializedApp,
prepareSpecializedApp,
PreparedSpecializedApp,
FrontendPluginInfoResolver,
@@ -126,18 +127,9 @@ export function createApp(options?: CreateAppOptions): {
bindRoutes: options?.bindRoutes,
advanced: options?.advanced,
});
const signIn = preparedApp.getSignIn();
if (signIn.element) {
return {
default: () => <PreparedAppRoot preparedApp={preparedApp} />,
};
}
const { sessionState } = await signIn.ready;
return {
default: () => renderFinalizedApp(preparedApp.finalize(sessionState)),
default: () => <PreparedAppRoot preparedApp={preparedApp} />,
};
}
@@ -158,51 +150,34 @@ function PreparedAppRoot(props: {
preparedApp: PreparedSpecializedApp;
}): JSX.Element {
const signIn = props.preparedApp.getSignIn();
const SignIn = signIn.Component;
const [finalizeError, setFinalizeError] = useState<Error>();
const [finalizedApp, setFinalizedApp] = useState<
ReturnType<PreparedSpecializedApp['finalize']> | undefined
>(undefined);
useEffect(() => {
let cancelled = false;
const runFinalize = async () => {
try {
const { sessionState } = await signIn.ready;
if (cancelled) {
return;
}
setFinalizedApp(props.preparedApp.finalize(sessionState));
} catch (error) {
if (cancelled) {
return;
}
setFinalizeError(error as Error);
}
};
void runFinalize();
return () => {
cancelled = true;
};
}, [props.preparedApp, signIn]);
const [, triggerRerender] = useReducer((count: number) => count + 1, 0);
if (finalizeError) {
throw finalizeError;
}
const finalizedApp: FinalizedSpecializedApp | undefined =
props.preparedApp.tryFinalize();
if (!finalizedApp) {
return signIn.element ?? <></>;
return (
<SignIn
onReady={() => {
triggerRerender();
}}
onError={setFinalizeError}
/>
);
}
return renderFinalizedApp(finalizedApp);
}
function renderFinalizedApp(
app: ReturnType<PreparedSpecializedApp['finalize']>,
) {
const errorPage = maybeCreateErrorPage(app);
const errorPage = maybeCreateErrorPage(finalizedApp);
if (errorPage) {
return errorPage;
}
return app.tree.root.instance!.getData(coreExtensionData.reactElement)!;
return finalizedApp.tree.root.instance!.getData(
coreExtensionData.reactElement,
)!;
}