frontend-app-api: extract createApp out into frontend-defaults
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-defaults': minor
|
||||
---
|
||||
|
||||
Initial release of this package, which provides a default app setup through the `createApp` function. This replaces the existing `createApp` method from `@backstage/frontend-app-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `createSpecializedApp` function now creates a bare-bones app without any of the default app structure or APIs. To re-introduce this functionality if you need to use `createSpecializedApp` you can install the `app` plugin from `@backstage/plugin-app`.
|
||||
@@ -301,7 +301,7 @@ An example of [integration with scalprum](https://github.com/backstage/backstage
|
||||
|
||||
**Dynamic Feature configuration**
|
||||
|
||||
The dynamic remote loading can be added directly into the [`createApp`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L234) function.
|
||||
The dynamic remote loading can be added directly into the [`createApp`](https://backstage.io/docs/reference/frontend-defaults.createapp) function.
|
||||
|
||||
The current `feature` type can be expanded with a `DynamicFrontendFeature` type:
|
||||
|
||||
@@ -351,7 +351,7 @@ const scalprum = initialize({
|
||||
});
|
||||
```
|
||||
|
||||
Because the [`appLoader`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L193) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins.
|
||||
Because the [`appLoader`](https://backstage.io/docs/reference/frontend-defaults.createapp) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins.
|
||||
|
||||
Initializing the dynamic feature is just a case of mapping the `DynamicFrontendFeature` to `FrontendFeature` via Scalprum:
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Below is a simple example of how to create and render an app instance:
|
||||
|
||||
```ts
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
|
||||
// Create your app instance
|
||||
const app = createApp({
|
||||
|
||||
@@ -295,7 +295,7 @@ export default createExtensionOverrides({
|
||||
Assuming the above code resides in the `@internal/search-page` package, you can install it in your app like this:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import searchPageOverride from '@internal/search-page';
|
||||
|
||||
const app = createApp({
|
||||
|
||||
@@ -256,7 +256,7 @@ app:
|
||||
We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import catalog from '@backstage/plugin-catalog';
|
||||
import scaffolder from '@backstage/plugin-scaffolder';
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ The created-app is currently templated for legacy frontend system applications,
|
||||
|
||||
## The app instance
|
||||
|
||||
The starting point of a frontend app is the `createApp` function, which accepts a single options object as its only parameter. It is imported from `@backstage/frontend-app-api`, which is where you will find most of the common APIs for building apps.
|
||||
The starting point of a frontend app is the `createApp` function, which accepts a single options object as its only parameter. It is imported from `@backstage/frontend-defaults`, which is where you will find most of the common APIs for building apps.
|
||||
|
||||
This is how to create a minimal app:
|
||||
|
||||
```tsx title="in src/index.ts"
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import catalogPlugin from '@backstage/plugin-catalog/alpha';
|
||||
|
||||
// Create your app instance
|
||||
@@ -87,7 +87,7 @@ Previously you would customize the application routes, components, apis, sidebar
|
||||
A manual installation is required if your packages are not discovered automatically, either because you are not using `@backstage/cli` to build your application or because the features are defined in local modules in the app package. In order to manually install a feature, you must import it and pass it to the `createApp` function:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
// This plugin was create as a local module in the app
|
||||
import { somePlugin } from './plugins';
|
||||
|
||||
@@ -104,10 +104,10 @@ You can also pass overrides to the features array, for more details, please read
|
||||
|
||||
### Using an async features loader
|
||||
|
||||
In case you need to perform asynchronous operations before passing features to the `createApp` function, define a [feature loader](https://backstage.io/docs/reference/frontend-app-api.createappfeatureloader/) object and pass it to the `features` option:
|
||||
In case you need to perform asynchronous operations before passing features to the `createApp` function, define a [feature loader](https://backstage.io/docs/reference/frontend-defaults.createappfeatureloader/) object and pass it to the `features` option:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
|
||||
const app = createApp({
|
||||
features: {
|
||||
@@ -129,7 +129,7 @@ export default app.createRoot();
|
||||
In some cases we want to load our configuration from a backend server and to do so, you can pass an callback to the `configLoader` option when calling the `createApp` function, the callback should return a promise of an object with the config object:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import { getConfigFromServer } from './utils';
|
||||
|
||||
// Example lazy loading the app configuration
|
||||
|
||||
@@ -18,7 +18,7 @@ The first step in migrating an app is to switch out the `createApp` function for
|
||||
// highlight-remove-next-line
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
// highlight-add-next-line
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
```
|
||||
|
||||
This immediate switch will lead to a lot of breakages that we need to fix.
|
||||
|
||||
@@ -38,8 +38,8 @@ import {
|
||||
createRouteRef,
|
||||
useApp,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/frontend-test-utils';
|
||||
|
||||
describe('collectLegacyRoutes', () => {
|
||||
it('should collect legacy routes', () => {
|
||||
@@ -281,7 +281,7 @@ describe('collectLegacyRoutes', () => {
|
||||
</FlatRoutes>,
|
||||
);
|
||||
|
||||
render(createSpecializedApp({ features }).createRoot());
|
||||
renderInTestApp(<div />, { features });
|
||||
|
||||
await expect(
|
||||
screen.findByText('plugins: app, test'),
|
||||
|
||||
@@ -4,33 +4,20 @@
|
||||
|
||||
```ts
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { createApp as createApp_2 } from '@backstage/frontend-defaults';
|
||||
import { CreateAppFeatureLoader as CreateAppFeatureLoader_2 } from '@backstage/frontend-defaults';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApp(options?: {
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
loadingComponent?: ReactNode;
|
||||
}): {
|
||||
createRoot(): JSX_2.Element;
|
||||
};
|
||||
// @public @deprecated (undocumented)
|
||||
export const createApp: typeof createApp_2;
|
||||
|
||||
// @public
|
||||
export interface CreateAppFeatureLoader {
|
||||
getLoaderName(): string;
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
// @public @deprecated (undocumented)
|
||||
export type CreateAppFeatureLoader = CreateAppFeatureLoader_2;
|
||||
|
||||
// @public
|
||||
export type CreateAppRouteBinder = <
|
||||
|
||||
@@ -34,21 +34,18 @@
|
||||
"dependencies": {
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-defaults": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-app": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-app": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^15.0.0"
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
AppTreeApi,
|
||||
appTreeApiRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createFrontendPlugin,
|
||||
ApiBlueprint,
|
||||
createRouteRef,
|
||||
createExternalRouteRef,
|
||||
createExtensionInput,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { createSpecializedApp } from './createSpecializedApp';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
featureFlagsApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
|
||||
describe('createSpecializedApp', () => {
|
||||
it('should render the root app', () => {
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<div>Test</div>)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should deduplicate features keeping the last received one', () => {
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(<div>Test 1</div>),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(<div>Test 2</div>),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
expect(screen.getByText('Test 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should forward config', () => {
|
||||
const app = createSpecializedApp({
|
||||
config: new MockConfigApi({ test: 'foo' }),
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: ({ apis }) => [
|
||||
coreExtensionData.reactElement(
|
||||
<div>Test {apis.get(configApiRef)!.getString('test')}</div>,
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
expect(screen.getByText('Test foo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support APIs and feature flags', async () => {
|
||||
const flags = new Array<{ name: string; pluginId: string }>();
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
featureFlags: [{ name: 'a' }, { name: 'b' }],
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: ({ apis }) => [
|
||||
coreExtensionData.reactElement(
|
||||
<div>
|
||||
flags:
|
||||
{apis
|
||||
.get(featureFlagsApiRef)!
|
||||
.getRegisteredFlags()
|
||||
.map(f => `${f.pluginId}=${f.name}`)
|
||||
.join(',')}
|
||||
</div>,
|
||||
),
|
||||
],
|
||||
}),
|
||||
ApiBlueprint.make({
|
||||
params: {
|
||||
factory: createApiFactory(featureFlagsApiRef, {
|
||||
registerFlag(flag) {
|
||||
flags.push(flag);
|
||||
},
|
||||
getRegisteredFlags() {
|
||||
return flags;
|
||||
},
|
||||
} as typeof featureFlagsApiRef.T),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
expect(screen.getByText('flags:test=a,test=b')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should make the app structure available through the AppTreeApi', async () => {
|
||||
let appTreeApi: AppTreeApi | undefined = undefined;
|
||||
|
||||
createSpecializedApp({
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: ({ apis }) => {
|
||||
appTreeApi = apis.get(appTreeApiRef);
|
||||
return [coreExtensionData.reactElement(<div />)];
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(String(appTreeApi!.getTree().tree.root)).toMatchInlineSnapshot(`
|
||||
"<root out=[core.reactElement]>
|
||||
app [
|
||||
<test out=[core.reactElement] />
|
||||
]
|
||||
</root>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support route bindings', async () => {
|
||||
const routeRef = createRouteRef();
|
||||
const extRouteRef = createExternalRouteRef();
|
||||
|
||||
const pluginA = createFrontendPlugin({
|
||||
id: 'a',
|
||||
externalRoutes: {
|
||||
ext: extRouteRef,
|
||||
},
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'parent',
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
inputs: {
|
||||
children: createExtensionInput([coreExtensionData.reactElement]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: ({ apis, inputs }) => {
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<ApiProvider apis={apis}>
|
||||
<MemoryRouter>
|
||||
{inputs.children.map(i => (
|
||||
<React.Fragment key={i.node.spec.id}>
|
||||
{i.get(coreExtensionData.reactElement)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MemoryRouter>
|
||||
</ApiProvider>,
|
||||
),
|
||||
];
|
||||
},
|
||||
}),
|
||||
createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'a/parent', input: 'children' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => {
|
||||
const Component = () => {
|
||||
const link = useRouteRef(extRouteRef);
|
||||
return <div>link: {link?.() ?? 'none'}</div>;
|
||||
};
|
||||
return [coreExtensionData.reactElement(<Component />)];
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
const pluginB = createFrontendPlugin({
|
||||
id: 'b',
|
||||
routes: {
|
||||
root: routeRef,
|
||||
},
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'a/parent', input: 'children' },
|
||||
output: [
|
||||
coreExtensionData.reactElement,
|
||||
coreExtensionData.routePath,
|
||||
coreExtensionData.routeRef,
|
||||
],
|
||||
factory: () => {
|
||||
return [
|
||||
coreExtensionData.reactElement(<div />),
|
||||
coreExtensionData.routePath('/test'),
|
||||
coreExtensionData.routeRef(routeRef),
|
||||
];
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
createSpecializedApp({
|
||||
features: [pluginA, pluginB],
|
||||
bindRoutes({ bind }) {
|
||||
bind(pluginA.externalRoutes, { ext: pluginB.routes.root });
|
||||
},
|
||||
}).createRoot(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('link: /test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+28
-110
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { JSX, ReactNode } from 'react';
|
||||
import React, { JSX } from 'react';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
createApiFactory,
|
||||
routeResolutionApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
import {
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
@@ -41,15 +40,14 @@ import {
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
import { ApiFactoryRegistry, ApiResolver } from '@backstage/core-app-api';
|
||||
|
||||
// TODO: Get rid of all of these
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultConfigLoader';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
import {
|
||||
createApp as _createApp,
|
||||
CreateAppFeatureLoader as _CreateAppFeatureLoader,
|
||||
} from '@backstage/frontend-defaults';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
@@ -71,7 +69,6 @@ import {
|
||||
} from '../../../frontend-plugin-api/src/wiring/createFrontendModule';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
import { getBasePath } from '../routing/getBasePath';
|
||||
import { Root } from '../extensions/Root';
|
||||
import { resolveAppTree } from '../tree/resolveAppTree';
|
||||
@@ -83,7 +80,6 @@ import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
import { BackstageRouteObject } from '../routing/types';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import { FrontendFeature } from './types';
|
||||
|
||||
function deduplicateFeatures(
|
||||
@@ -109,93 +105,6 @@ function deduplicateFeatures(
|
||||
.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of dynamically loaded frontend features.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CreateAppFeatureLoader {
|
||||
/**
|
||||
* Returns name of this loader. suitable for showing to users.
|
||||
*/
|
||||
getLoaderName(): string;
|
||||
|
||||
/**
|
||||
* Loads a number of features dynamically.
|
||||
*/
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createApp(options?: {
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{ config: ConfigApi }>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
/**
|
||||
* The component to render while loading the app (waiting for config, features, etc)
|
||||
*
|
||||
* Is the text "Loading..." by default.
|
||||
* If set to "null" then no loading fallback component is rendered. *
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
}): {
|
||||
createRoot(): JSX.Element;
|
||||
} {
|
||||
let suspenseFallback = options?.loadingComponent;
|
||||
if (suspenseFallback === undefined) {
|
||||
suspenseFallback = 'Loading...';
|
||||
}
|
||||
|
||||
async function appLoader() {
|
||||
const config =
|
||||
(await options?.configLoader?.().then(c => c.config)) ??
|
||||
ConfigReader.fromConfigs(
|
||||
overrideBaseUrlConfigs(defaultConfigLoaderSync()),
|
||||
);
|
||||
|
||||
const discoveredFeatures = getAvailableFeatures(config);
|
||||
|
||||
const providedFeatures: FrontendFeature[] = [];
|
||||
for (const entry of options?.features ?? []) {
|
||||
if ('load' in entry) {
|
||||
try {
|
||||
const result = await entry.load({ config });
|
||||
providedFeatures.push(...result.features);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
providedFeatures.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const app = createSpecializedApp({
|
||||
config,
|
||||
features: [...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
}).createRoot();
|
||||
|
||||
return { default: () => app };
|
||||
}
|
||||
|
||||
return {
|
||||
createRoot() {
|
||||
const LazyApp = React.lazy(appLoader);
|
||||
return (
|
||||
<React.Suspense fallback={suspenseFallback}>
|
||||
<LazyApp />
|
||||
</React.Suspense>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Helps delay callers from reaching out to the API before the app tree has been materialized
|
||||
class AppTreeApiProxy implements AppTreeApi {
|
||||
#safeToUse: boolean = false;
|
||||
@@ -267,8 +176,21 @@ class RouteResolutionApiProxy implements RouteResolutionApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version of {@link createApp}, expecting all features and
|
||||
* config to have been loaded already.
|
||||
* @public
|
||||
* @deprecated Import from `@backstage/frontend-defaults` instead.
|
||||
*/
|
||||
export const createApp = _createApp;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Import from `@backstage/frontend-defaults` instead.
|
||||
*/
|
||||
export type CreateAppFeatureLoader = _CreateAppFeatureLoader;
|
||||
|
||||
/**
|
||||
* Creates an empty app without any default features. This is a low-level API is
|
||||
* intended for use in tests or specialized setups. Typically wou want to use
|
||||
* `createApp` from `@backstage/frontend-defaults` instead.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -277,12 +199,8 @@ export function createSpecializedApp(options?: {
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
}): { createRoot(): JSX.Element } {
|
||||
const {
|
||||
features: featuresWithoutApp = [],
|
||||
config = new ConfigReader({}, 'empty-config'),
|
||||
} = options ?? {};
|
||||
|
||||
const features = deduplicateFeatures([appPlugin, ...featuresWithoutApp]);
|
||||
const config = options?.config ?? new ConfigReader({}, 'empty-config');
|
||||
const features = deduplicateFeatures(options?.features ?? []);
|
||||
|
||||
const tree = resolveAppTree(
|
||||
'root',
|
||||
@@ -317,12 +235,6 @@ export function createSpecializedApp(options?: {
|
||||
],
|
||||
});
|
||||
|
||||
// Now instantiate the entire tree, which will skip anything that's already been instantiated
|
||||
instantiateAppNodeTree(tree.root, apiHolder);
|
||||
|
||||
routeResolutionApi.initialize();
|
||||
appTreeApi.initialize();
|
||||
|
||||
const featureFlagApi = apiHolder.get(featureFlagsApiRef);
|
||||
if (featureFlagApi) {
|
||||
for (const feature of features) {
|
||||
@@ -350,6 +262,12 @@ export function createSpecializedApp(options?: {
|
||||
}
|
||||
}
|
||||
|
||||
// Now instantiate the entire tree, which will skip anything that's already been instantiated
|
||||
instantiateAppNodeTree(tree.root, apiHolder);
|
||||
|
||||
routeResolutionApi.initialize();
|
||||
appTreeApi.initialize();
|
||||
|
||||
const rootEl = tree.root.instance!.getData(coreExtensionData.reactElement);
|
||||
|
||||
const AppComponent = () => rootEl;
|
||||
@@ -18,5 +18,5 @@ export {
|
||||
createApp,
|
||||
createSpecializedApp,
|
||||
type CreateAppFeatureLoader,
|
||||
} from './createApp';
|
||||
} from './createSpecializedApp';
|
||||
export * from './types';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,9 @@
|
||||
# @backstage/frontend-defaults
|
||||
|
||||
**The [new frontend system](https://backstage.io/docs/frontend-system/) that this package is part of is in alpha, and we do not yet recommend using it for production deployments**
|
||||
|
||||
This package provides the high-level APIs used to create Backstage frontend applications with the default setup. For more information, see the [documentation on how to build Backstage frontend applications](https://backstage.io/docs/frontend-system/building-apps/index).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Documentation](https://backstage.io/docs)
|
||||
@@ -0,0 +1,31 @@
|
||||
## API Report File for "@backstage/frontend-defaults"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ConfigApi } from '@backstage/frontend-plugin-api';
|
||||
import { CreateAppRouteBinder } from '@backstage/frontend-app-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-app-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @public
|
||||
export function createApp(options?: {
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
loadingComponent?: ReactNode;
|
||||
}): {
|
||||
createRoot(): JSX_2.Element;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface CreateAppFeatureLoader {
|
||||
getLoaderName(): string;
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-frontend-defaults
|
||||
title: '@backstage/frontend-defaults'
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-web-library
|
||||
owner: maintainers
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@backstage/frontend-defaults",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/frontend-defaults"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^15.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-app-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-app": "workspace:^",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
}
|
||||
+11
-14
@@ -19,7 +19,6 @@ import {
|
||||
appTreeApiRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
PageBlueprint,
|
||||
createFrontendPlugin,
|
||||
ThemeBlueprint,
|
||||
@@ -176,7 +175,13 @@ describe('createApp', () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: new MockConfigApi({}) }),
|
||||
features: [
|
||||
appPlugin,
|
||||
appPlugin.withOverrides({
|
||||
extensions: [
|
||||
appPlugin
|
||||
.getExtension('app/root')
|
||||
.override({ disabled: true, factory: orig => orig() }),
|
||||
],
|
||||
}),
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
featureFlags: [{ name: 'test-1' }],
|
||||
@@ -203,18 +208,10 @@ describe('createApp', () => {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createExtensionOverrides({
|
||||
createFrontendPlugin({
|
||||
id: 'other',
|
||||
featureFlags: [{ name: 'test-2' }],
|
||||
extensions: [
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'root',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
disabled: true,
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
],
|
||||
extensions: [],
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -222,7 +219,7 @@ describe('createApp', () => {
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText("Flags: test-1 from 'test', test-2 from ''"),
|
||||
screen.findByText("Flags: test-1 from 'test', test-2 from 'other'"),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2024 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 React, { JSX, ReactNode } from 'react';
|
||||
import { ConfigApi } from '@backstage/frontend-plugin-api';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import {
|
||||
CreateAppRouteBinder,
|
||||
FrontendFeature,
|
||||
createSpecializedApp,
|
||||
} from '@backstage/frontend-app-api';
|
||||
|
||||
/**
|
||||
* A source of dynamically loaded frontend features.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CreateAppFeatureLoader {
|
||||
/**
|
||||
* Returns name of this loader. suitable for showing to users.
|
||||
*/
|
||||
getLoaderName(): string;
|
||||
|
||||
/**
|
||||
* Loads a number of features dynamically.
|
||||
*/
|
||||
load(options: { config: ConfigApi }): Promise<{
|
||||
features: FrontendFeature[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Backstage frontend app instance. See https://backstage.io/docs/frontend-system/building-apps/index
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createApp(options?: {
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{ config: ConfigApi }>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
/**
|
||||
* The component to render while loading the app (waiting for config, features, etc)
|
||||
*
|
||||
* Is the text "Loading..." by default.
|
||||
* If set to "null" then no loading fallback component is rendered. *
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
}): {
|
||||
createRoot(): JSX.Element;
|
||||
} {
|
||||
let suspenseFallback = options?.loadingComponent;
|
||||
if (suspenseFallback === undefined) {
|
||||
suspenseFallback = 'Loading...';
|
||||
}
|
||||
|
||||
async function appLoader() {
|
||||
const config =
|
||||
(await options?.configLoader?.().then(c => c.config)) ??
|
||||
ConfigReader.fromConfigs(
|
||||
overrideBaseUrlConfigs(defaultConfigLoaderSync()),
|
||||
);
|
||||
|
||||
const discoveredFeatures = getAvailableFeatures(config);
|
||||
|
||||
const providedFeatures: FrontendFeature[] = [];
|
||||
for (const entry of options?.features ?? []) {
|
||||
if ('load' in entry) {
|
||||
try {
|
||||
const result = await entry.load({ config });
|
||||
providedFeatures.push(...result.features);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
providedFeatures.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const app = createSpecializedApp({
|
||||
config,
|
||||
features: [appPlugin, ...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
}).createRoot();
|
||||
|
||||
return { default: () => app };
|
||||
}
|
||||
|
||||
return {
|
||||
createRoot() {
|
||||
const LazyApp = React.lazy(appLoader);
|
||||
return (
|
||||
<React.Suspense fallback={suspenseFallback}>
|
||||
<LazyApp />
|
||||
</React.Suspense>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -53,7 +53,7 @@ function readPackageDetectionConfig(config: Config) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
export function getAvailableFeatures(config: Config): FrontendFeature[] {
|
||||
const discovered = (
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* APIs for creating Backstage apps with a default setup.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { createApp, type CreateAppFeatureLoader } from './createApp';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 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 '@testing-library/jest-dom';
|
||||
@@ -60,6 +60,7 @@ const roleRules = [
|
||||
targetRole: 'frontend-plugin',
|
||||
except: [
|
||||
// TODO(freben): Address these
|
||||
'@backstage/frontend-defaults',
|
||||
'@backstage/frontend-app-api',
|
||||
'@backstage/frontend-test-utils',
|
||||
'@backstage/plugin-api-docs',
|
||||
@@ -75,6 +76,7 @@ const roleRules = [
|
||||
'@backstage/app-defaults',
|
||||
'@backstage/core-compat-api',
|
||||
'@backstage/dev-utils',
|
||||
'@backstage/frontend-defaults',
|
||||
'@backstage/frontend-app-api',
|
||||
'@backstage/frontend-test-utils',
|
||||
'@backstage/test-utils',
|
||||
|
||||
@@ -4426,17 +4426,14 @@ __metadata:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/frontend-defaults": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-app": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@material-ui/core": ^4.12.4
|
||||
"@material-ui/icons": ^4.11.3
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^15.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
@@ -4447,6 +4444,26 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/frontend-defaults@workspace:^, @backstage/frontend-defaults@workspace:packages/frontend-defaults":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/frontend-defaults@workspace:packages/frontend-defaults"
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/frontend-app-api": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-app": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^15.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/frontend-plugin-api@npm:^0.7.0":
|
||||
version: 0.7.0
|
||||
resolution: "@backstage/frontend-plugin-api@npm:0.7.0"
|
||||
|
||||
Reference in New Issue
Block a user