frontend-plugin-api: support feature-level predicates

This lets plugin and module instances apply a shared condition to all of their extensions, while preserving extension-level conditions by combining them with logical AND during app spec resolution.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-14 12:00:55 +01:00
parent 457904d372
commit f30eeba995
9 changed files with 201 additions and 66 deletions
+44 -43
View File
@@ -255,12 +255,12 @@ export interface AppNodeSpec {
// (undocumented)
readonly disabled: boolean;
// (undocumented)
readonly enabled?: FilterPredicate;
// (undocumented)
readonly extension: Extension<unknown, unknown>;
// (undocumented)
readonly id: string;
// (undocumented)
readonly if?: FilterPredicate;
// (undocumented)
readonly plugin: FrontendPlugin;
}
@@ -580,7 +580,7 @@ export type CreateExtensionBlueprintOptions<
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
enabled?: FilterPredicate;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
config?: {
@@ -667,7 +667,7 @@ export type CreateExtensionOptions<
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
enabled?: FilterPredicate;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
config?: {
@@ -756,6 +756,8 @@ export interface CreateFrontendModuleOptions<
// (undocumented)
featureFlags?: FeatureFlagConfig[];
// (undocumented)
if?: FilterPredicate;
// (undocumented)
pluginId: TPluginId;
}
@@ -770,45 +772,13 @@ export function createFrontendPlugin<
[name in string]: ExternalRouteRef;
} = {},
>(
options: CreateFrontendPluginOptions<
TId,
TRoutes,
TExternalRoutes,
TExtensions
>,
options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,
): OverridableFrontendPlugin<
TRoutes,
TExternalRoutes,
MakeSortedExtensionsMap<TExtensions[number], TId>
>;
// @public
export interface CreateFrontendPluginOptions<
TId extends string,
TRoutes extends {
[name in string]: RouteRef | SubRouteRef;
},
TExternalRoutes extends {
[name in string]: ExternalRouteRef;
},
TExtensions extends readonly ExtensionDefinition[],
> {
// (undocumented)
extensions?: TExtensions;
// (undocumented)
externalRoutes?: TExternalRoutes;
// (undocumented)
featureFlags?: FeatureFlagConfig[];
icon?: IconElement;
// (undocumented)
info?: FrontendPluginInfoOptions;
// (undocumented)
pluginId: TId;
// (undocumented)
routes?: TRoutes;
title?: string;
}
// @public
export function createRouteRef<
TParams extends
@@ -1031,7 +1001,7 @@ export interface ExtensionBlueprint<
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<NonNullable<T['output']>, UParentInputs>;
disabled?: boolean;
enabled?: FilterPredicate;
if?: FilterPredicate;
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
@@ -1067,7 +1037,7 @@ export interface ExtensionBlueprint<
UParentInputs
>;
disabled?: boolean;
enabled?: FilterPredicate;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
@@ -1709,7 +1679,7 @@ export interface OverridableExtensionDefinition<
UParentInputs
>;
disabled?: boolean;
enabled?: FilterPredicate;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
@@ -1815,6 +1785,7 @@ export interface OverridableFrontendPlugin<
// (undocumented)
withOverrides(options: {
extensions?: Array<ExtensionDefinition>;
if?: FilterPredicate;
title?: string;
icon?: IconElement;
info?: FrontendPluginInfoOptions;
@@ -1971,8 +1942,8 @@ export const pluginHeaderActionsApiRef: ApiRef_2<
readonly $$type: '@backstage/ApiRef';
};
// @public @deprecated (undocumented)
export type PluginOptions<
// @public (undocumented)
export interface PluginOptions<
TId extends string,
TRoutes extends {
[name in string]: RouteRef | SubRouteRef;
@@ -1981,7 +1952,24 @@ export type PluginOptions<
[name in string]: ExternalRouteRef;
},
TExtensions extends readonly ExtensionDefinition[],
> = CreateFrontendPluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>;
> {
// (undocumented)
extensions?: TExtensions;
// (undocumented)
externalRoutes?: TExternalRoutes;
// (undocumented)
featureFlags?: FeatureFlagConfig[];
icon?: IconElement;
// (undocumented)
if?: FilterPredicate;
// (undocumented)
info?: FrontendPluginInfoOptions;
// (undocumented)
pluginId: TId;
// (undocumented)
routes?: TRoutes;
title?: string;
}
// @public
export type PluginWrapperApi = {
@@ -2064,6 +2052,19 @@ export const Progress: {
// @public (undocumented)
export type ProgressProps = {};
// @public
export type ResolvedExtensionInputs<
TInputs extends {
[name in string]: ExtensionInput;
},
> = {
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
? Array<Expand<ResolvedExtensionInput<TInputs[InputName]>>>
: false extends TInputs[InputName]['config']['optional']
? Expand<ResolvedExtensionInput<TInputs[InputName]>>
: Expand<ResolvedExtensionInput<TInputs[InputName]> | undefined>;
};
// @public
export type RouteFunc<TParams extends AnyRouteRefParams> = (
...input: TParams extends undefined ? readonly [] : readonly [params: TParams]
@@ -46,6 +46,7 @@ describe('createFrontendModule', () => {
"disabled": false,
"factory": [Function],
"id": "route:test/test",
"if": undefined,
"inputs": {},
"output": [],
"toString": [Function],
@@ -53,6 +54,7 @@ describe('createFrontendModule', () => {
},
],
"featureFlags": [],
"if": undefined,
"pluginId": "test",
"toString": [Function],
"version": "v1",
@@ -21,6 +21,7 @@ import {
resolveExtensionDefinition,
} from './resolveExtensionDefinition';
import { FeatureFlagConfig } from './types';
import { FilterPredicate } from '@backstage/filter-predicates';
/** @public */
export interface CreateFrontendModuleOptions<
@@ -30,6 +31,7 @@ export interface CreateFrontendModuleOptions<
pluginId: TPluginId;
extensions?: TExtensions;
featureFlags?: FeatureFlagConfig[];
if?: FilterPredicate;
}
/** @public */
@@ -43,6 +45,7 @@ export interface InternalFrontendModule extends FrontendModule {
readonly version: 'v1';
readonly extensions: Extension<unknown>[];
readonly featureFlags: FeatureFlagConfig[];
readonly if?: FilterPredicate;
}
/**
@@ -126,6 +129,7 @@ export function createFrontendModule<
version: 'v1',
pluginId,
featureFlags: options.featureFlags ?? [],
if: options.if,
extensions,
toString() {
return `Module{pluginId=${pluginId}}`;
@@ -171,6 +171,7 @@ describe('createFrontendPlugin', () => {
"configSchema": undefined,
"disabled": false,
"factory": [Function],
"if": undefined,
"inputs": {},
"kind": undefined,
"name": "1",
@@ -359,6 +360,7 @@ describe('createFrontendPlugin', () => {
"configSchema": undefined,
"disabled": false,
"factory": [Function],
"if": undefined,
"inputs": {},
"kind": undefined,
"name": "1",
@@ -32,6 +32,7 @@ import { JsonObject } from '@backstage/types';
import { IconElement } from '../icons/types';
import { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing';
import { ID_PATTERN } from './constants';
import { FilterPredicate } from '@backstage/filter-predicates';
/**
* Information about the plugin.
@@ -195,6 +196,7 @@ export interface CreateFrontendPluginOptions<
externalRoutes?: TExternalRoutes;
extensions?: TExtensions;
featureFlags?: FeatureFlagConfig[];
if?: FilterPredicate;
info?: FrontendPluginInfoOptions;
}
@@ -304,6 +306,7 @@ export function createFrontendPlugin<
routes: options.routes ?? ({} as TRoutes),
externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),
featureFlags: options.featureFlags ?? [],
if: options.if,
extensions: extensions,
infoOptions: options.info,