Merge pull request #30813 from backstage/freben/advanced-options
Arrange most `create[Specialized]App` options into an `advanced` field
This commit is contained in:
@@ -20,25 +20,19 @@ export function createApp(options?: CreateAppOptions): {
|
||||
|
||||
// @public
|
||||
export interface CreateAppOptions {
|
||||
// (undocumented)
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
// (undocumented)
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
// (undocumented)
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
// (undocumented)
|
||||
features?: (FrontendFeature | FrontendFeatureLoader)[];
|
||||
// (undocumented)
|
||||
flags?: {
|
||||
advanced?: {
|
||||
allowUnknownExtensionConfig?: boolean;
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
loadingComponent?: ReactNode;
|
||||
pluginInfoResolver?: FrontendPluginInfoResolver;
|
||||
};
|
||||
loadingComponent?: ReactNode;
|
||||
// (undocumented)
|
||||
pluginInfoResolver?: FrontendPluginInfoResolver;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
features?: (FrontendFeature | FrontendFeatureLoader)[];
|
||||
}
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
|
||||
@@ -45,18 +45,20 @@ describe('createApp', () => {
|
||||
|
||||
it('should allow themes to be installed', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({
|
||||
data: {
|
||||
app: {
|
||||
extensions: [
|
||||
{ 'theme:app/light': false },
|
||||
{ 'theme:app/dark': false },
|
||||
],
|
||||
advanced: {
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({
|
||||
data: {
|
||||
app: {
|
||||
extensions: [
|
||||
{ 'theme:app/light': false },
|
||||
{ 'theme:app/dark': false },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
@@ -85,7 +87,9 @@ describe('createApp', () => {
|
||||
it('should deduplicate features keeping the last received one', async () => {
|
||||
const duplicatedFeatureId = 'test';
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
pluginId: duplicatedFeatureId,
|
||||
@@ -138,7 +142,6 @@ describe('createApp', () => {
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
features: [
|
||||
appPlugin,
|
||||
createFrontendPlugin({
|
||||
@@ -153,12 +156,15 @@ describe('createApp', () => {
|
||||
],
|
||||
}),
|
||||
],
|
||||
pluginInfoResolver: async () => {
|
||||
return {
|
||||
info: {
|
||||
packageName: '@test/test',
|
||||
},
|
||||
};
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
pluginInfoResolver: async () => {
|
||||
return {
|
||||
info: {
|
||||
packageName: '@test/test',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -187,9 +193,11 @@ describe('createApp', () => {
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({ data: { key: 'config-value' } }),
|
||||
}),
|
||||
advanced: {
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({ data: { key: 'config-value' } }),
|
||||
}),
|
||||
},
|
||||
features: [appPlugin, loader],
|
||||
});
|
||||
|
||||
@@ -208,9 +216,11 @@ describe('createApp', () => {
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config(),
|
||||
}),
|
||||
advanced: {
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config(),
|
||||
}),
|
||||
},
|
||||
features: [loader],
|
||||
});
|
||||
|
||||
@@ -221,7 +231,9 @@ describe('createApp', () => {
|
||||
|
||||
it('should register feature flags', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
appPlugin.withOverrides({
|
||||
extensions: [
|
||||
@@ -273,15 +285,6 @@ describe('createApp', () => {
|
||||
|
||||
it('should allow unknown extension config if the flag is set', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({
|
||||
data: {
|
||||
app: {
|
||||
extensions: [{ 'unknown:lols/wut': false }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
features: [
|
||||
appPlugin,
|
||||
createFrontendPlugin({
|
||||
@@ -296,7 +299,18 @@ describe('createApp', () => {
|
||||
],
|
||||
}),
|
||||
],
|
||||
flags: { allowUnknownExtensionConfig: true },
|
||||
advanced: {
|
||||
allowUnknownExtensionConfig: true,
|
||||
configLoader: async () => ({
|
||||
config: mockApis.config({
|
||||
data: {
|
||||
app: {
|
||||
extensions: [{ 'unknown:lols/wut': false }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
@@ -307,7 +321,9 @@ describe('createApp', () => {
|
||||
let appTreeApi: AppTreeApi | undefined = undefined;
|
||||
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
appPlugin,
|
||||
createFrontendPlugin({
|
||||
@@ -413,7 +429,9 @@ describe('createApp', () => {
|
||||
|
||||
it('should use "Loading..." as the default suspense fallback', async () => {
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
},
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
@@ -423,8 +441,10 @@ describe('createApp', () => {
|
||||
|
||||
it('should use no suspense fallback if the "loadingComponent" is null', async () => {
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
loadingComponent: null,
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
loadingComponent: null,
|
||||
},
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
@@ -434,8 +454,10 @@ describe('createApp', () => {
|
||||
|
||||
it('should use a custom "loadingComponent"', async () => {
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
loadingComponent: <span>"Custom loading message"</span>,
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
loadingComponent: <span>"Custom loading message"</span>,
|
||||
},
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
@@ -445,7 +467,9 @@ describe('createApp', () => {
|
||||
|
||||
it('should allow overriding the app plugin', async () => {
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
},
|
||||
features: [
|
||||
appPlugin.withOverrides({
|
||||
extensions: [
|
||||
@@ -468,7 +492,6 @@ describe('createApp', () => {
|
||||
|
||||
it('should use a custom extensionFactoryMiddleware', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
features: [
|
||||
appPlugin,
|
||||
createFrontendPlugin({
|
||||
@@ -484,18 +507,21 @@ describe('createApp', () => {
|
||||
],
|
||||
}),
|
||||
],
|
||||
*extensionFactoryMiddleware(originalFactory, context) {
|
||||
const output = originalFactory();
|
||||
yield* output;
|
||||
const element = output.get(coreExtensionData.reactElement);
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
*extensionFactoryMiddleware(originalFactory, context) {
|
||||
const output = originalFactory();
|
||||
yield* output;
|
||||
const element = output.get(coreExtensionData.reactElement);
|
||||
|
||||
if (element) {
|
||||
yield coreExtensionData.reactElement(
|
||||
<div data-testid={`wrapped(${context.node.spec.id})`}>
|
||||
{element}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (element) {
|
||||
yield coreExtensionData.reactElement(
|
||||
<div data-testid={`wrapped(${context.node.spec.id})`}>
|
||||
{element}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -522,7 +548,9 @@ describe('createApp', () => {
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
},
|
||||
features: [mod],
|
||||
});
|
||||
|
||||
@@ -549,7 +577,9 @@ describe('createApp', () => {
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
advanced: {
|
||||
configLoader: () => new Promise(() => {}),
|
||||
},
|
||||
features: [mod],
|
||||
});
|
||||
|
||||
|
||||
@@ -42,21 +42,64 @@ import { resolveAsyncFeatures } from './resolution';
|
||||
* @public
|
||||
*/
|
||||
export interface CreateAppOptions {
|
||||
features?: (FrontendFeature | FrontendFeatureLoader)[];
|
||||
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. *
|
||||
* The list of features to load.
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
pluginInfoResolver?: FrontendPluginInfoResolver;
|
||||
flags?: { allowUnknownExtensionConfig?: boolean };
|
||||
features?: (FrontendFeature | FrontendFeatureLoader)[];
|
||||
|
||||
/**
|
||||
* Allows for the binding of plugins' external route refs within the app.
|
||||
*/
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
|
||||
/**
|
||||
* Advanced, more rarely used options.
|
||||
*/
|
||||
advanced?: {
|
||||
/**
|
||||
* If set to true, the system will silently accept and move on if
|
||||
* encountering config for extensions that do not exist. The default is to
|
||||
* reject such config to help catch simple mistakes.
|
||||
*
|
||||
* This flag can be useful in some scenarios where you have a dynamic set of
|
||||
* extensions enabled at different times, but also increases the risk of
|
||||
* accidentally missing e.g. simple typos in your config.
|
||||
*/
|
||||
allowUnknownExtensionConfig?: boolean;
|
||||
|
||||
/**
|
||||
* Sets a custom config loader, replacing the builtin one.
|
||||
*
|
||||
* This can be used e.g. if you have the need to source config out of custom
|
||||
* storages.
|
||||
*/
|
||||
configLoader?: () => Promise<{ config: ConfigApi }>;
|
||||
|
||||
/**
|
||||
* Applies one or more middleware on every extension, as they are added to
|
||||
* the application.
|
||||
*
|
||||
* This is an advanced use case for modifying extension data on the fly as
|
||||
* it gets emitted by extensions being instantiated.
|
||||
*/
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
|
||||
/**
|
||||
* The component to render while loading the app (waiting for config,
|
||||
* features, etc).
|
||||
*
|
||||
* This is the text "Loading..." by default. If set to "null" then no loading
|
||||
* fallback component is rendered at all.
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
|
||||
/**
|
||||
* Allows for customizing how plugin info is retrieved.
|
||||
*/
|
||||
pluginInfoResolver?: FrontendPluginInfoResolver;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,14 +110,14 @@ export interface CreateAppOptions {
|
||||
export function createApp(options?: CreateAppOptions): {
|
||||
createRoot(): JSX.Element;
|
||||
} {
|
||||
let suspenseFallback = options?.loadingComponent;
|
||||
let suspenseFallback = options?.advanced?.loadingComponent;
|
||||
if (suspenseFallback === undefined) {
|
||||
suspenseFallback = 'Loading...';
|
||||
}
|
||||
|
||||
async function appLoader() {
|
||||
const config =
|
||||
(await options?.configLoader?.().then(c => c.config)) ??
|
||||
(await options?.advanced?.configLoader?.().then(c => c.config)) ??
|
||||
ConfigReader.fromConfigs(
|
||||
overrideBaseUrlConfigs(defaultConfigLoaderSync()),
|
||||
);
|
||||
@@ -87,12 +130,10 @@ export function createApp(options?: CreateAppOptions): {
|
||||
});
|
||||
|
||||
const app = createSpecializedApp({
|
||||
config,
|
||||
features: [appPlugin, ...loadedFeatures],
|
||||
config,
|
||||
bindRoutes: options?.bindRoutes,
|
||||
extensionFactoryMiddleware: options?.extensionFactoryMiddleware,
|
||||
pluginInfoResolver: options?.pluginInfoResolver,
|
||||
flags: options?.flags,
|
||||
advanced: options?.advanced,
|
||||
});
|
||||
|
||||
const rootEl = app.tree.root.instance!.getData(
|
||||
|
||||
@@ -30,7 +30,9 @@ describe('createPublicSignInApp', () => {
|
||||
|
||||
it('should render a sign-in page', async () => {
|
||||
const app = createPublicSignInApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
@@ -58,7 +60,9 @@ describe('createPublicSignInApp', () => {
|
||||
.mockReturnValue();
|
||||
|
||||
const app = createPublicSignInApp({
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
|
||||
Reference in New Issue
Block a user