diff --git a/.changeset/sixty-adults-kiss.md b/.changeset/sixty-adults-kiss.md
new file mode 100644
index 0000000000..eb73387308
--- /dev/null
+++ b/.changeset/sixty-adults-kiss.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-app-api': patch
+---
+
+Added `createSpecializedApp`, which is a synchronous version of `createApp` where config and features already need to be loaded.
diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md
index 3d6d936018..3ca51f6331 100644
--- a/packages/frontend-app-api/api-report.md
+++ b/packages/frontend-app-api/api-report.md
@@ -41,6 +41,15 @@ export function createApp(options?: {
// @public (undocumented)
export function createExtensionTree(options: { config: Config }): ExtensionTree;
+// @public
+export function createSpecializedApp(options?: {
+ features?: (BackstagePlugin | ExtensionOverrides)[];
+ config?: ConfigApi;
+ bindRoutes?(context: { bind: AppRouteBinder }): void;
+}): {
+ createRoot(): JSX_2.Element;
+};
+
// @public (undocumented)
export interface ExtensionTree {
// (undocumented)
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index ca0e1d60d1..dd20604e8c 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -244,49 +244,18 @@ export function createApp(options?: {
const discoveredFeatures = getAvailableFeatures(config);
const loadedFeatures = (await options?.featureLoader?.({ config })) ?? [];
- const allFeatures = deduplicateFeatures([
- ...discoveredFeatures,
- ...loadedFeatures,
- ...(options?.features ?? []),
- ]);
- const tree = createAppTree({
- features: allFeatures,
- builtinExtensions,
+ const app = createSpecializedApp({
config,
- });
+ features: [
+ ...discoveredFeatures,
+ ...loadedFeatures,
+ ...(options?.features ?? []),
+ ],
+ bindRoutes: options?.bindRoutes,
+ }).createRoot();
- const appContext = createLegacyAppContext(
- allFeatures.filter(
- (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
- ),
- );
-
- const routeIds = collectRouteIds(allFeatures);
-
- const App = () => (
-
-
-
-
- {/* TODO: set base path using the logic from AppRouter */}
-
- {tree.root.instance!.getData(coreExtensionData.reactElement)}
-
-
-
-
-
- );
-
- return { default: App };
+ return { default: () => app };
}
return {
@@ -301,6 +270,66 @@ export function createApp(options?: {
};
}
+/**
+ * Synchronous version of {@link createApp}, expecting all features and
+ * config to have been loaded already.
+ * @public
+ */
+export function createSpecializedApp(options?: {
+ features?: (BackstagePlugin | ExtensionOverrides)[];
+ config?: ConfigApi;
+ bindRoutes?(context: { bind: AppRouteBinder }): void;
+}): { createRoot(): JSX.Element } {
+ const {
+ features: duplicatedFeatures = [],
+ config = new ConfigReader({}, 'empty-config'),
+ } = options ?? {};
+
+ const features = deduplicateFeatures(duplicatedFeatures);
+
+ const tree = createAppTree({
+ features,
+ builtinExtensions,
+ config,
+ });
+
+ const appContext = createLegacyAppContext(
+ features.filter(
+ (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
+ ),
+ );
+
+ const routeIds = collectRouteIds(features);
+
+ const App = () => (
+
+
+
+
+ {/* TODO: set base path using the logic from AppRouter */}
+
+ {tree.root.instance!.getData(coreExtensionData.reactElement)}
+
+
+
+
+
+ );
+
+ return {
+ createRoot() {
+ return ;
+ },
+ };
+}
+
// Make sure that we only convert each new plugin instance to its legacy equivalent once
const legacyPluginStore = getOrCreateGlobalSingleton(
'legacy-plugin-compatibility-store',
diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts
index dc1ba3bc13..412e643523 100644
--- a/packages/frontend-app-api/src/wiring/index.ts
+++ b/packages/frontend-app-api/src/wiring/index.ts
@@ -16,6 +16,7 @@
export {
createApp,
+ createSpecializedApp,
createExtensionTree,
type ExtensionTreeNode,
type ExtensionTree,