frontend-plugin-api: support predicate overrides

Allow plugin and extension overrides to replace or remove existing if predicates. This makes conditional plugin and extension behavior overrideable through both plugin overrides and module-installed extension overrides.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-14 12:33:19 +01:00
parent 47f7cd2505
commit d65d4812f6
5 changed files with 111 additions and 2 deletions
@@ -567,6 +567,42 @@ describe('resolveAppNodeSpecs', () => {
expect(specs[1].if).toEqual(pluginIf);
});
it('should allow plugin overrides to replace or remove plugin if predicates', () => {
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
const pluginIf = { featureFlags: { $contains: 'plugin-flag' } };
const overrideIf = { permissions: { $contains: 'override.permission' } };
const plugin = createFrontendPlugin({
pluginId: 'test-plugin',
if: pluginIf,
extensions: [
createExtension({
name: 'one',
attachTo: { id: 'app', input: 'root' },
output: [dataRef],
factory: () => [dataRef('one')],
}),
],
});
const overriddenSpecs = resolveAppNodeSpecs({
features: [plugin.withOverrides({ if: overrideIf })],
builtinExtensions: [],
parameters: [],
collector,
});
const clearedSpecs = resolveAppNodeSpecs({
features: [plugin.withOverrides({ if: undefined })],
builtinExtensions: [],
parameters: [],
collector,
});
expect(overriddenSpecs).toHaveLength(1);
expect(overriddenSpecs[0].if).toEqual(overrideIf);
expect(clearedSpecs).toHaveLength(1);
expect(clearedSpecs[0].if).toBeUndefined();
});
it('should merge plugin and module if predicates with extension predicates', () => {
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
const pluginIf = { featureFlags: { $contains: 'plugin-flag' } };
@@ -616,4 +652,60 @@ describe('resolveAppNodeSpecs', () => {
expect(specs[1].id).toBe('test-plugin/module-extension');
expect(specs[1].if).toEqual({ $all: [moduleIf, moduleExtensionIf] });
});
it('should allow module extension overrides to replace or remove extension if predicates', () => {
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
const extensionIf = { featureFlags: { $contains: 'extension-flag' } };
const overrideIf = { permissions: { $contains: 'override.permission' } };
const plugin = createFrontendPlugin({
pluginId: 'test-plugin',
extensions: [
createExtension({
name: 'extension',
attachTo: { id: 'app', input: 'root' },
if: extensionIf,
output: [dataRef],
factory: () => [dataRef('base')],
}),
],
});
const overriddenSpecs = resolveAppNodeSpecs({
features: [
plugin,
createFrontendModule({
pluginId: 'test-plugin',
extensions: [
plugin.getExtension('test-plugin/extension').override({
if: overrideIf,
}),
],
}),
],
builtinExtensions: [],
parameters: [],
collector,
});
const clearedSpecs = resolveAppNodeSpecs({
features: [
plugin,
createFrontendModule({
pluginId: 'test-plugin',
extensions: [
plugin.getExtension('test-plugin/extension').override({
if: undefined,
}),
],
}),
],
builtinExtensions: [],
parameters: [],
collector,
});
expect(overriddenSpecs).toHaveLength(1);
expect(overriddenSpecs[0].if).toEqual(overrideIf);
expect(clearedSpecs).toHaveLength(1);
expect(clearedSpecs[0].if).toBeUndefined();
});
});
@@ -549,13 +549,18 @@ export function createExtension<
);
}
let ifPredicate = options.if;
if ('if' in overrideOptions) {
ifPredicate = overrideOptions.if;
}
return createExtension({
kind: options.kind,
name: options.name,
attachTo: (overrideOptions.attachTo ??
options.attachTo) as ExtensionDefinitionAttachTo,
disabled: overrideOptions.disabled ?? options.disabled,
if: overrideOptions.if ?? options.if,
if: ifPredicate,
inputs: bindInputs(
{
...(options.inputs ?? {}),
@@ -116,6 +116,11 @@ export interface OverridableFrontendPlugin<
withOverrides(options: {
extensions?: Array<ExtensionDefinition>;
/**
* Overrides the shared condition that applies to all extensions in the plugin.
*/
if?: FilterPredicate;
/**
* Overrides the display title of the plugin.
*/
@@ -329,6 +334,10 @@ export function createFrontendPlugin<
return `Plugin{id=${pluginId}}`;
},
withOverrides(overrides) {
let ifPredicate = options.if;
if ('if' in overrides) {
ifPredicate = overrides.if;
}
const overrideExtensions = overrides.extensions ?? [];
const overriddenExtensionIds = new Set(
overrideExtensions.map(
@@ -344,6 +353,7 @@ export function createFrontendPlugin<
return createFrontendPlugin({
...options,
pluginId,
if: ifPredicate,
title: overrides.title ?? options.title,
icon: overrides.icon ?? options.icon,
extensions: [...nonOverriddenExtensions, ...overrideExtensions],