frontend-app-api: follow up phased app review feedback
Fix utility API resolution for falsy values and clarify how phased app finalization is owned between onFinalized and finalize. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -52,7 +52,7 @@ For information on how to configure feature discovery and other installation opt
|
||||
|
||||
Most apps should use `createApp` from `@backstage/frontend-defaults`, which takes care of all app preparation internally. For more advanced use cases there is also a lower-level `prepareSpecializedApp` API in `@backstage/frontend-app-api`.
|
||||
|
||||
This API is useful when you need to render a bootstrap tree before the full app can be finalized, for example while waiting for sign-in or other session-dependent state. It gives you access to a bootstrap app tree immediately, notifies you when the finalized app is ready, and lets you reuse a prepared session in a later app instance.
|
||||
This API is useful when you need to render a bootstrap tree before the full app can be finalized, for example while waiting for sign-in or other session-dependent state. It gives you access to a bootstrap app tree immediately, lets you either subscribe to finalization with `onFinalized()` or finalize synchronously with `finalize()`, and lets you reuse a prepared session in a later app instance.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
@@ -74,7 +74,21 @@ const unsubscribe = preparedApp.onFinalized(
|
||||
);
|
||||
```
|
||||
|
||||
The `getBootstrapApp()` method exposes the partial app tree that is available during bootstrap. The `onFinalized()` method notifies you once the full app tree has been finalized, and `finalize(sessionState?)` can be used when you already have a reusable session state or when you want to bypass the asynchronous bootstrap flow in tests.
|
||||
The `getBootstrapApp()` method exposes the partial app tree that is available during bootstrap. If you call `onFinalized()`, you are subscribing to the bootstrap-owned finalization flow. In the sign-in case, the sign-in page receives an `onSignInSuccess` callback, and once it provides an identity through that callback the full app is finalized and `onFinalized()` subscribers are notified.
|
||||
|
||||
If you instead call `finalize()`, you are taking ownership of finalization yourself. This only works when the app can be finalized synchronously, for example when all predicate context is already available or when you passed a reusable session state to `prepareSpecializedApp()` up front:
|
||||
|
||||
```tsx
|
||||
const preparedApp = prepareSpecializedApp({
|
||||
config,
|
||||
features: [appPlugin, ...features],
|
||||
advanced: {
|
||||
sessionState,
|
||||
},
|
||||
});
|
||||
|
||||
const app = preparedApp.finalize();
|
||||
```
|
||||
|
||||
When using phased app preparation, `app/root.children` acts as the main session boundary. Conditional extensions behind that boundary are evaluated during finalization. Conditional `app/root.elements` and API branches are also deferred until finalization, while other bootstrap-visible predicates are ignored and reported as warnings.
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 {
|
||||
type AnyApiFactory,
|
||||
createApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
FrontendApiRegistry,
|
||||
FrontendApiResolver,
|
||||
} from './FrontendApiRegistry';
|
||||
|
||||
describe('FrontendApiResolver', () => {
|
||||
it('should cache falsy API values', () => {
|
||||
const falseApiRef = createApiRef<boolean>({ id: 'test.false' });
|
||||
const falseFactoryFn = jest.fn(() => false);
|
||||
const registry = new FrontendApiRegistry();
|
||||
|
||||
registry.register({
|
||||
api: falseApiRef,
|
||||
deps: {},
|
||||
factory: falseFactoryFn,
|
||||
} as AnyApiFactory);
|
||||
|
||||
const resolver = new FrontendApiResolver({ primaryRegistry: registry });
|
||||
|
||||
expect(resolver.get(falseApiRef)).toBe(false);
|
||||
expect(resolver.get(falseApiRef)).toBe(false);
|
||||
expect(falseFactoryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should resolve falsy dependencies', () => {
|
||||
const falseApiRef = createApiRef<boolean>({ id: 'test.false' });
|
||||
const dependentApiRef = createApiRef<string>({ id: 'test.dependent' });
|
||||
const falseFactoryFn = jest.fn(() => false);
|
||||
const dependentFactoryFn = jest.fn((deps: { falseDependency: boolean }) =>
|
||||
deps.falseDependency === false ? 'resolved' : 'unexpected',
|
||||
);
|
||||
const registry = new FrontendApiRegistry();
|
||||
|
||||
registry.register({
|
||||
api: falseApiRef,
|
||||
deps: {},
|
||||
factory: falseFactoryFn,
|
||||
} as AnyApiFactory);
|
||||
registry.register({
|
||||
api: dependentApiRef,
|
||||
deps: { falseDependency: falseApiRef },
|
||||
factory: dependentFactoryFn,
|
||||
} as AnyApiFactory);
|
||||
|
||||
const resolver = new FrontendApiResolver({ primaryRegistry: registry });
|
||||
|
||||
expect(resolver.get(dependentApiRef)).toBe('resolved');
|
||||
expect(dependentFactoryFn).toHaveBeenCalledWith({
|
||||
falseDependency: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -107,7 +107,7 @@ export class FrontendApiResolver implements ApiHolder {
|
||||
|
||||
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
|
||||
const existing = this.apis.get(ref.id);
|
||||
if (existing) {
|
||||
if (this.apis.has(ref.id)) {
|
||||
return existing as T;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export class FrontendApiResolver implements ApiHolder {
|
||||
const deps = {} as { [name: string]: unknown };
|
||||
for (const [key, depRef] of Object.entries(factory.deps)) {
|
||||
const dep = this.load(depRef, [...loading, factory.api]);
|
||||
if (!dep) {
|
||||
if (dep === undefined) {
|
||||
throw new Error(
|
||||
`No API factory available for dependency ${depRef} of dependent ${factory.api}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user