frontend-plugin-api: add new internal option for extension inputs

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-01-22 14:22:56 +01:00
parent 24eb7d7933
commit 7edb810248
27 changed files with 315 additions and 149 deletions
+5 -1
View File
@@ -604,7 +604,7 @@ export function createExtensionDataRef<TData>(): {
}): ConfigurableExtensionDataRef<TData, TId>;
};
// @public (undocumented)
// @public
export function createExtensionInput<
UExtensionData extends ExtensionDataRef<
unknown,
@@ -616,6 +616,7 @@ export function createExtensionInput<
TConfig extends {
singleton?: boolean;
optional?: boolean;
internal?: boolean;
},
>(
extensionData: Array<UExtensionData>,
@@ -630,6 +631,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
>;
@@ -1288,9 +1290,11 @@ export interface ExtensionInput<
TConfig extends {
singleton: boolean;
optional: boolean;
internal?: boolean;
} = {
singleton: boolean;
optional: boolean;
internal?: boolean;
},
> {
// (undocumented)
@@ -29,28 +29,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: false, optional: false },
config: { singleton: false, optional: false, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -64,7 +64,7 @@ describe('createExtensionInput', () => {
expect(inputWithContext).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: false, optional: false },
config: { singleton: false, optional: false, internal: false },
withContext: expect.any(Function),
context,
});
@@ -77,28 +77,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: true, optional: false },
config: { singleton: true, optional: false, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -112,28 +112,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: true, optional: true },
config: { singleton: true, optional: true, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: true }
{ singleton: true; optional: true; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: true; optional: true }
{ singleton: true; optional: true; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -144,4 +144,14 @@ describe('createExtensionInput', () => {
createExtensionInput([stringDataRef, stringDataRef], { singleton: true }),
).toThrow("ExtensionInput may not have duplicate data refs: 'str'");
});
it('should create an internal input', () => {
const input = createExtensionInput([stringDataRef], { internal: true });
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef],
config: { singleton: false, optional: false, internal: true },
withContext: expect.any(Function),
});
});
});
@@ -27,9 +27,14 @@ export interface ExtensionInput<
string,
{ optional?: true }
> = ExtensionDataRef,
TConfig extends { singleton: boolean; optional: boolean } = {
TConfig extends {
singleton: boolean;
optional: boolean;
internal?: boolean;
} = {
singleton: boolean;
optional: boolean;
internal?: boolean;
},
> {
readonly $$type: '@backstage/ExtensionInput';
@@ -38,10 +43,60 @@ export interface ExtensionInput<
readonly replaces?: Array<{ id: string; input: string }>;
}
/** @public */
/**
* Creates a new extension input to be passed to the input map of an extension.
*
* @remarks
*
* Extension inputs created with this function can be passed to any `inputs` map
* as part of creating or overriding an extension.
*
* The array of extension data references defines the data this input expects.
* If the required data is not provided by the attached extension, the
* attachment will fail.
*
* The `config` object can be used to restrict the behavior and shape of the
* input. By default an input will accept zero or more extensions from any
* plugin. The following options are available:
*
* - `singleton`: If set to `true`, only one extension can be attached to the
* input at a time. Additional extension will trigger an app error and be
* ignored.
* - `optional`: If set to `true`, the input is optional and can be omitted,
* this only has an effect if the `singleton` is set to `true`.
* - `internal`: If set to `true`, only extensions from the same plugins will be
* allowed to attach to this input. Other extensions will trigger an app error
* and be ignored.
*
* @param extensionData - The array of extension data references that this input
* expects.
* @param config - The configuration object for the input.
* @returns An extension input declaration.
* @example
* ```ts
* const extension = createExtension({
* attachTo: { id: 'example-parent', input: 'example-input' },
* inputs: {
* content: createExtensionInput([coreExtensionData.reactElement], {
* singleton: true,
* }),
* },
* output: [coreExtensionData.reactElement],
* *factory({ inputs }) {
* const content = inputs.content?.get(coreExtensionData.reactElement);
* yield coreExtensionData.reactElement(<ContentWrapper>{content}</ContentWrapper>);
* },
* });
* ```
* @public
*/
export function createExtensionInput<
UExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,
TConfig extends { singleton?: boolean; optional?: boolean },
TConfig extends {
singleton?: boolean;
optional?: boolean;
internal?: boolean;
},
>(
extensionData: Array<UExtensionData>,
config?: TConfig & { replaces?: Array<{ id: string; input: string }> },
@@ -50,6 +105,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
> {
if (process.env.NODE_ENV !== 'production') {
@@ -81,6 +137,9 @@ export function createExtensionInput<
optional: Boolean(config?.optional) as TConfig['optional'] extends true
? true
: false,
internal: Boolean(config?.internal) as TConfig['internal'] extends true
? true
: false,
},
replaces: config?.replaces,
};
@@ -90,6 +149,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
> {
return OpaqueExtensionInput.createInstance(undefined, {
@@ -37,6 +37,7 @@ export type ResolvedInputValueOverrides<
{
optional: infer IOptional extends boolean;
singleton: boolean;
internal?: boolean;
}
>
? IOptional extends true
@@ -44,7 +45,11 @@ export type ResolvedInputValueOverrides<
: KName
: never]: TInputs[KName] extends ExtensionInput<
infer IDataRefs,
{ optional: boolean; singleton: infer ISingleton extends boolean }
{
optional: boolean;
singleton: infer ISingleton extends boolean;
internal?: boolean;
}
>
? ISingleton extends true
? Iterable<ExtensionDataRefToValue<IDataRefs>>
@@ -56,6 +61,7 @@ export type ResolvedInputValueOverrides<
{
optional: infer IOptional extends boolean;
singleton: boolean;
internal?: boolean;
}
>
? IOptional extends true
@@ -63,7 +69,11 @@ export type ResolvedInputValueOverrides<
: never
: never]?: TInputs[KName] extends ExtensionInput<
infer IDataRefs,
{ optional: boolean; singleton: infer ISingleton extends boolean }
{
optional: boolean;
singleton: infer ISingleton extends boolean;
internal?: boolean;
}
>
? ISingleton extends true
? Iterable<ExtensionDataRefToValue<IDataRefs>>