Merge pull request #5985 from backstage/rugvip/appfix
core: a couple for forwards compatibility fixes for core-*
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-api': patch
|
||||
---
|
||||
|
||||
Improve forwards compatibility with `@backstage/core-app-api` and `@backstage/core-plugin-api` by re-using route reference types and factory methods from `@backstage/core-plugin-api`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Fixes a type bug where supplying all app icons to `createApp` was required, rather than just a partial list.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Update `createApp` options to allow plugins with unknown output types in order to improve forwards and backwards compatibility.
|
||||
@@ -20,56 +20,9 @@ import {
|
||||
ExternalRouteRef,
|
||||
routeRefType,
|
||||
AnyParams,
|
||||
ParamKeys,
|
||||
OptionalParams,
|
||||
} from './types';
|
||||
|
||||
export class ExternalRouteRefImpl<
|
||||
Params extends AnyParams,
|
||||
Optional extends boolean
|
||||
> implements ExternalRouteRef<Params, Optional> {
|
||||
readonly [routeRefType] = 'external';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly params: ParamKeys<Params>,
|
||||
readonly optional: Optional,
|
||||
) {}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=external,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createExternalRouteRef<
|
||||
Params extends { [param in ParamKey]: string },
|
||||
Optional extends boolean = false,
|
||||
ParamKey extends string = never
|
||||
>(options: {
|
||||
/**
|
||||
* An identifier for this route, used to identify it in error messages
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The parameters that will be provided to the external route reference.
|
||||
*/
|
||||
params?: ParamKey[];
|
||||
|
||||
/**
|
||||
* Whether or not this route is optional, defaults to false.
|
||||
*
|
||||
* Optional external routes are not required to be bound in the app, and
|
||||
* if they aren't, `useRouteRef` will return `undefined`.
|
||||
*/
|
||||
optional?: Optional;
|
||||
}): ExternalRouteRef<OptionalParams<Params>, Optional> {
|
||||
return new ExternalRouteRefImpl(
|
||||
options.id,
|
||||
(options.params ?? []) as ParamKeys<OptionalParams<Params>>,
|
||||
Boolean(options.optional) as Optional,
|
||||
);
|
||||
}
|
||||
export { createExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export function isExternalRouteRef<
|
||||
Params extends AnyParams,
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
routeRefType,
|
||||
AnyParams,
|
||||
ParamKeys,
|
||||
OptionalParams,
|
||||
} from './types';
|
||||
import { IconComponent } from '../icons/types';
|
||||
|
||||
export { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// TODO(Rugvip): Remove this in the next breaking release, it's exported but unused
|
||||
export type RouteRefConfig<Params extends AnyParams> = {
|
||||
params?: ParamKeys<Params>;
|
||||
@@ -33,69 +34,6 @@ export type RouteRefConfig<Params extends AnyParams> = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export class RouteRefImpl<Params extends AnyParams>
|
||||
implements RouteRef<Params> {
|
||||
readonly [routeRefType] = 'absolute';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly params: ParamKeys<Params>,
|
||||
private readonly config: {
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title?: string;
|
||||
},
|
||||
) {}
|
||||
|
||||
get icon() {
|
||||
return this.config.icon;
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
|
||||
get path() {
|
||||
return this.config.path ?? '';
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.config.title ?? this.id;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=absolute,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRouteRef<
|
||||
// Params is the type that we care about and the one to be embedded in the route ref.
|
||||
// For example, given the params ['name', 'kind'], Params will be {name: string, kind: string}
|
||||
Params extends { [param in ParamKey]: string },
|
||||
// ParamKey is here to make sure the Params type properly has its keys narrowed down
|
||||
// to only the elements of params. Defaulting to never makes sure we end up with
|
||||
// Param = {} if the params array is empty.
|
||||
ParamKey extends string = never
|
||||
>(config: {
|
||||
/** The id of the route ref, used to identify it when printed */
|
||||
id?: string;
|
||||
/** A list of parameter names that the path that this route ref is bound to must contain */
|
||||
params?: ParamKey[];
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
/** @deprecated Route refs no longer decide their own icon */
|
||||
icon?: IconComponent;
|
||||
/** @deprecated Route refs no longer decide their own title */
|
||||
title?: string;
|
||||
}): RouteRef<OptionalParams<Params>> {
|
||||
const id = config.id || config.title;
|
||||
if (!id) {
|
||||
throw new Error('RouteRef must be provided a non-empty id');
|
||||
}
|
||||
return new RouteRefImpl(
|
||||
id,
|
||||
(config.params ?? []) as ParamKeys<OptionalParams<Params>>,
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
export function isRouteRef<Params extends AnyParams>(
|
||||
routeRef:
|
||||
| RouteRef<Params>
|
||||
|
||||
@@ -17,106 +17,12 @@
|
||||
import {
|
||||
AnyParams,
|
||||
ExternalRouteRef,
|
||||
OptionalParams,
|
||||
ParamKeys,
|
||||
RouteRef,
|
||||
routeRefType,
|
||||
SubRouteRef,
|
||||
} from './types';
|
||||
|
||||
// Should match the pattern in react-router
|
||||
const PARAM_PATTERN = /^\w+$/;
|
||||
|
||||
export class SubRouteRefImpl<Params extends AnyParams>
|
||||
implements SubRouteRef<Params> {
|
||||
readonly [routeRefType] = 'sub';
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
readonly path: string,
|
||||
readonly parent: RouteRef,
|
||||
readonly params: ParamKeys<Params>,
|
||||
) {}
|
||||
|
||||
toString() {
|
||||
return `routeRef{type=sub,id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
// These utility types help us infer a Param object type from a string path
|
||||
// For example, `/foo/:bar/:baz` inferred to `{ bar: string, baz: string }`
|
||||
type ParamPart<S extends string> = S extends `:${infer Param}` ? Param : never;
|
||||
type ParamNames<S extends string> = S extends `${infer Part}/${infer Rest}`
|
||||
? ParamPart<Part> | ParamNames<Rest>
|
||||
: ParamPart<S>;
|
||||
type PathParams<S extends string> = { [name in ParamNames<S>]: string };
|
||||
|
||||
/**
|
||||
* Merges a param object type with with an optional params type into a params object
|
||||
*/
|
||||
type MergeParams<
|
||||
P1 extends { [param in string]: string },
|
||||
P2 extends AnyParams
|
||||
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
|
||||
|
||||
/**
|
||||
* Creates a SubRouteRef type given the desired parameters and parent route parameters.
|
||||
* The parameters types are merged together while ensuring that there is no overlap between the two.
|
||||
*/
|
||||
type MakeSubRouteRef<
|
||||
Params extends { [param in string]: string },
|
||||
ParentParams extends AnyParams
|
||||
> = keyof Params & keyof ParentParams extends never
|
||||
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
|
||||
: never;
|
||||
|
||||
export function createSubRouteRef<
|
||||
Path extends string,
|
||||
ParentParams extends AnyParams = never
|
||||
>(config: {
|
||||
id: string;
|
||||
path: Path;
|
||||
parent: RouteRef<ParentParams>;
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams> {
|
||||
const { id, path, parent } = config;
|
||||
type Params = PathParams<Path>;
|
||||
|
||||
// Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz'
|
||||
const pathParams = path
|
||||
.split('/')
|
||||
.filter(p => p.startsWith(':'))
|
||||
.map(p => p.substring(1));
|
||||
const params = [...parent.params, ...pathParams];
|
||||
|
||||
if (parent.params.some(p => pathParams.includes(p as string))) {
|
||||
throw new Error(
|
||||
'SubRouteRef may not have params that overlap with its parent',
|
||||
);
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
throw new Error(`SubRouteRef path must start with '/', got '${path}'`);
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
throw new Error(`SubRouteRef path must not end with '/', got '${path}'`);
|
||||
}
|
||||
for (const param of pathParams) {
|
||||
if (!PARAM_PATTERN.test(param)) {
|
||||
throw new Error(`SubRouteRef path has invalid param, got '${param}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// We ensure that the type of the return type is sane here
|
||||
const subRouteRef = new SubRouteRefImpl(
|
||||
id,
|
||||
path,
|
||||
parent,
|
||||
params as ParamKeys<MergeParams<Params, ParentParams>>,
|
||||
) as SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>;
|
||||
|
||||
// But skip type checking of the return value itself, because the conditional
|
||||
// type checking of the parent parameter overlap is tricky to express.
|
||||
return subRouteRef as any;
|
||||
}
|
||||
export { createSubRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export function isSubRouteRef<Params extends AnyParams>(
|
||||
routeRef:
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconComponent } from '../icons/types';
|
||||
import {
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getOrCreateGlobalSingleton } from '../lib/globalObject';
|
||||
import { RouteRef as NewRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export type { RouteRef, SubRouteRef, ExternalRouteRef };
|
||||
|
||||
export type AnyParams = { [param in string]: string } | undefined;
|
||||
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
|
||||
@@ -36,7 +41,7 @@ export type RouteFunc<Params extends AnyParams> = (
|
||||
) => string;
|
||||
|
||||
type RouteRefType = Exclude<
|
||||
keyof NewRouteRef,
|
||||
keyof RouteRef,
|
||||
'params' | 'path' | 'title' | 'icon'
|
||||
>;
|
||||
export const routeRefType: RouteRefType = getOrCreateGlobalSingleton<any>(
|
||||
@@ -44,41 +49,6 @@ export const routeRefType: RouteRefType = getOrCreateGlobalSingleton<any>(
|
||||
() => Symbol('route-ref-type'),
|
||||
);
|
||||
|
||||
export type RouteRef<Params extends AnyParams = any> = {
|
||||
readonly [routeRefType]: 'absolute';
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
|
||||
// TODO(Rugvip): Remove all of these once plugins don't rely on the path
|
||||
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
|
||||
path: string;
|
||||
/** @deprecated icons are no longer accessed via RouteRefs */
|
||||
icon?: IconComponent;
|
||||
/** @deprecated titles are no longer accessed via RouteRefs */
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type SubRouteRef<Params extends AnyParams = any> = {
|
||||
readonly [routeRefType]: 'sub';
|
||||
|
||||
parent: RouteRef;
|
||||
|
||||
path: string;
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
};
|
||||
|
||||
export type ExternalRouteRef<
|
||||
Params extends AnyParams = any,
|
||||
Optional extends boolean = any
|
||||
> = {
|
||||
readonly [routeRefType]: 'external';
|
||||
|
||||
params: ParamKeys<Params>;
|
||||
|
||||
optional?: Optional;
|
||||
};
|
||||
|
||||
export type AnyRouteRef =
|
||||
| RouteRef<any>
|
||||
| SubRouteRef<any>
|
||||
|
||||
@@ -42,6 +42,7 @@ import { oktaAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { oneloginAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { PendingAuthRequest } from '@backstage/core-plugin-api';
|
||||
import { PluginOutput } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
@@ -137,10 +138,10 @@ export type AppContext = {
|
||||
// @public (undocumented)
|
||||
export type AppOptions = {
|
||||
apis?: Iterable<AnyApiFactory>;
|
||||
icons?: AppIcons & {
|
||||
icons?: Partial<AppIcons> & {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
plugins?: BackstagePlugin<any, any>[];
|
||||
plugins?: BackstagePluginWithAnyOutput[];
|
||||
components?: Partial<AppComponents>;
|
||||
themes?: AppTheme[];
|
||||
configLoader?: AppConfigLoader;
|
||||
@@ -183,6 +184,11 @@ export type BackstageApp = {
|
||||
getRouter(): ComponentType<{}>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackstagePluginWithAnyOutput = Omit<BackstagePlugin<any, any>, 'output'> & {
|
||||
output(): (PluginOutput | UnknownPluginOutput)[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type BootErrorPageProps = {
|
||||
step: 'load-config' | 'load-chunk';
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
BootErrorPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
} from './types';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* The default config loader, which expects that config is available at compile-time
|
||||
@@ -171,7 +172,7 @@ export function createApp(options?: AppOptions) {
|
||||
return new PrivateAppImpl({
|
||||
apis,
|
||||
icons,
|
||||
plugins,
|
||||
plugins: plugins as BackstagePlugin<any, any>[],
|
||||
components,
|
||||
themes,
|
||||
configLoader,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
PluginOutput,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { AppIcons } from './icons';
|
||||
@@ -131,6 +132,21 @@ export type AppRouteBinder = <
|
||||
>,
|
||||
) => void;
|
||||
|
||||
// Output from newer or older plugin API versions that might not be supported by
|
||||
// this version of the app API, but we don't want to break at the type checking level.
|
||||
// We only use this more permissive type for the `createApp` options, as we otherwise
|
||||
// want to stick to using the type for the outputs that we know about in this version
|
||||
// of the app api.
|
||||
type UnknownPluginOutput = {
|
||||
type: string;
|
||||
};
|
||||
export type BackstagePluginWithAnyOutput = Omit<
|
||||
BackstagePlugin<any, any>,
|
||||
'output'
|
||||
> & {
|
||||
output(): (PluginOutput | UnknownPluginOutput)[];
|
||||
};
|
||||
|
||||
export type AppOptions = {
|
||||
/**
|
||||
* A collection of ApiFactories to register in the application to either
|
||||
@@ -141,12 +157,12 @@ export type AppOptions = {
|
||||
/**
|
||||
* Supply icons to override the default ones.
|
||||
*/
|
||||
icons?: AppIcons & { [key in string]: IconComponent };
|
||||
icons?: Partial<AppIcons> & { [key in string]: IconComponent };
|
||||
|
||||
/**
|
||||
* A list of all plugins to include in the app.
|
||||
*/
|
||||
plugins?: BackstagePlugin<any, any>[];
|
||||
plugins?: BackstagePluginWithAnyOutput[];
|
||||
|
||||
/**
|
||||
* Supply components to the app to override the default ones.
|
||||
|
||||
Reference in New Issue
Block a user