diff --git a/.changeset/tame-clouds-confess.md b/.changeset/tame-clouds-confess.md new file mode 100644 index 0000000000..05af248eb1 --- /dev/null +++ b/.changeset/tame-clouds-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': patch +--- + +Wait for `configApi` to be ready before using `featureFlagsApi` diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index a9f1e81a11..8f4847c2f0 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -14,6 +14,12 @@ * limitations under the License. */ +import { + configApiRef, + createApiFactory, + featureFlagsApiRef, + LocalStorageFeatureFlags, +} from '../apis'; import { renderWithEffects, withLogCollector } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; @@ -245,6 +251,67 @@ describe('Integration Test', () => { expect(screen.getByText('extLink4: ')).toBeInTheDocument(); }); + it('should wait for the config to load before calling feature flags', async () => { + const storageFlags = new LocalStorageFeatureFlags(); + jest.spyOn(storageFlags, 'registerFlag'); + + const apis = [ + createApiFactory({ + api: featureFlagsApiRef, + deps: { configApi: configApiRef }, + factory() { + return storageFlags; + }, + }), + ]; + + const app = new PrivateAppImpl({ + apis, + defaultApis: [], + themes: [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + theme: lightTheme, + }, + ], + icons: defaultSystemIcons, + plugins: [ + createPlugin({ + id: 'test', + register: p => p.featureFlags.register('name'), + }), + ], + components, + bindRoutes: ({ bind }) => { + bind(plugin1.externalRoutes, { + err: plugin1RouteRef, + errParams: plugin2RouteRef, + }); + }, + }); + + const Provider = app.getProvider(); + const Router = app.getRouter(); + + await renderWithEffects( + + + + + + + + , + ); + + expect(storageFlags.registerFlag).toHaveBeenCalledWith({ + name: 'name', + pluginId: 'test', + }); + }); + it('should throw some error when the route has duplicate params', () => { const app = new PrivateAppImpl({ apis: [], diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 9e14c9a5fb..c20eb0824e 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Config } from '@backstage/config'; import React, { ComponentType, PropsWithChildren, ReactElement, + useEffect, useMemo, useState, } from 'react'; @@ -278,24 +280,6 @@ export class PrivateAppImpl implements BackstageApp { const appContext = new AppContextImpl(this); const apiHolder = this.getApiHolder(); - const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; - - for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'feature-flag': { - featureFlagsApi.registerFlag({ - name: output.name, - pluginId: plugin.getId(), - }); - break; - } - default: - break; - } - } - } - const Provider = ({ children }: PropsWithChildren<{}>) => { const appThemeApi = useMemo( () => AppThemeSelector.createWithStorage(this.themes), @@ -324,13 +308,39 @@ export class PrivateAppImpl implements BackstageApp { appThemeApi, ); + const hasConfigApi = 'api' in loadedConfig; + if (hasConfigApi) { + const { api } = loadedConfig as { api: Config }; + this.configApi = api; + } + + useEffect(() => { + if (hasConfigApi) { + const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; + + for (const plugin of this.plugins.values()) { + for (const output of plugin.output()) { + switch (output.type) { + case 'feature-flag': { + featureFlagsApi.registerFlag({ + name: output.name, + pluginId: plugin.getId(), + }); + break; + } + default: + break; + } + } + } + } + }, [hasConfigApi, loadedConfig]); + if ('node' in loadedConfig) { // Loading or error return loadedConfig.node; } - this.configApi = loadedConfig.api; - return (