frontend-plugin-api: add new version of createExtension that avoids data maps

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-07-26 15:24:21 +02:00
parent cd9f3a526e
commit 0d9c38f1b8
3 changed files with 285 additions and 28 deletions
@@ -18,7 +18,8 @@ import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { createExtensionInput } from './createExtensionInput';
const stringData = createExtensionDataRef<string>().with({ id: 'string' });
const stringDataRef = createExtensionDataRef<string>().with({ id: 'string' });
const numberDataRef = createExtensionDataRef<number>().with({ id: 'number' });
function unused(..._any: any[]) {}
@@ -28,7 +29,7 @@ describe('createExtension', () => {
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: {
foo: stringData,
foo: stringDataRef,
},
};
const extension = createExtension({
@@ -42,11 +43,11 @@ describe('createExtension', () => {
expect(extension.namespace).toBe('test');
// When declared as an error function without a block the TypeScript errors
// are a more specific and will point at the property that is problematic.
// are a more specific and will often point at the property that is problematic.
// @ts-expect-error
createExtension({
...baseConfig,
factory: () => ({
// @ts-expect-error
foo: 3,
}),
});
@@ -166,8 +167,8 @@ describe('createExtension', () => {
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: {
foo: stringData,
bar: stringData.optional(),
foo: stringDataRef,
bar: stringDataRef.optional(),
},
};
const extension = createExtension({
@@ -185,18 +186,18 @@ describe('createExtension', () => {
bar: 'baz',
}),
});
// @ts-expect-error
createExtension({
...baseConfig,
factory: () => ({
// @ts-expect-error
foo: 3,
}),
});
// @ts-expect-error
createExtension({
...baseConfig,
factory: () => ({
foo: 'bar',
// @ts-expect-error
bar: 3,
}),
});
@@ -237,18 +238,18 @@ describe('createExtension', () => {
attachTo: { id: 'root', input: 'default' },
inputs: {
mixed: createExtensionInput({
required: stringData,
optional: stringData.optional(),
required: stringDataRef,
optional: stringDataRef.optional(),
}),
onlyRequired: createExtensionInput({
required: stringData,
required: stringDataRef,
}),
onlyOptional: createExtensionInput({
optional: stringData.optional(),
optional: stringDataRef.optional(),
}),
},
output: {
foo: stringData,
foo: stringDataRef,
},
factory({ inputs }) {
const a1: string = inputs.mixed?.[0].output.required;
@@ -304,7 +305,7 @@ describe('createExtension', () => {
},
},
output: {
foo: stringData,
foo: stringDataRef,
},
factory({ config }) {
const a1: string = config.foo;
@@ -355,4 +356,88 @@ describe('createExtension', () => {
return extension.configSchema?.parse({});
}).toThrow("Missing required value at 'foo'");
});
it('should new form of inputs and outputs', () => {
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
inputs: {
header: createExtensionInput([stringDataRef.optional()], {
optional: true,
singleton: true,
}),
content: createExtensionInput([stringDataRef, numberDataRef], {
optional: false,
singleton: true,
}),
},
output: [stringDataRef],
factory({ inputs }) {
const headerStr = inputs.header?.get(stringDataRef);
const contentStr = inputs.content.get(stringDataRef);
const contentNum = inputs.content.get(numberDataRef);
// @ts-expect-error
inputs.header?.get(numberDataRef);
// @ts-expect-error
const x1: string = headerStr; // string | undefined
unused(x1);
return [stringDataRef(contentStr.repeat(contentNum))];
},
});
// Double output
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: [stringDataRef],
// @ts-expect-error
factory() {
return [stringDataRef('hello'), stringDataRef('hello')];
},
});
// Missing output
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: [stringDataRef, numberDataRef],
// @ts-expect-error
factory() {
return [stringDataRef('hello')];
},
});
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: [stringDataRef, numberDataRef],
factory() {
return [stringDataRef('hello'), numberDataRef(4)];
},
});
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: [stringDataRef, numberDataRef.optional()],
factory() {
return [stringDataRef('hello'), numberDataRef(4)];
},
});
createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: [stringDataRef, numberDataRef.optional()],
factory() {
return [stringDataRef('hello')];
},
});
expect(true).toBe(true);
});
});
@@ -17,17 +17,26 @@
import { AppNode } from '../apis';
import { PortableSchema, createSchemaFromZod } from '../schema';
import { Expand } from '../types';
import { ExtensionDataRef } from './createExtensionDataRef';
import {
AnyExtensionDataRef,
ExtensionDataRef,
ExtensionDataValue,
} from './createExtensionDataRef';
import { ExtensionInput } from './createExtensionInput';
import { z } from 'zod';
/** @public */
/**
* @public
* @deprecated Extension data maps will be removed.
*/
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<unknown, string, { optional?: true }>;
[name in string]: AnyExtensionDataRef;
};
/** @public */
export type AnyExtensionInputMap = {
[inputName in string]: ExtensionInput<
AnyExtensionDataRef,
AnyExtensionDataMap,
{ optional: boolean; singleton: boolean }
>;
@@ -36,6 +45,7 @@ export type AnyExtensionInputMap = {
/**
* Converts an extension data map into the matching concrete data values type.
* @public
* @deprecated Extension data maps will be removed.
*/
export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
@@ -51,22 +61,41 @@ export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
: never]?: TExtensionData[DataName]['T'];
};
/** @public */
export type ExtensionDataContainer<UExtensionData extends AnyExtensionDataRef> =
{
get<TId extends UExtensionData['id']>(
ref: ExtensionDataRef<any, TId, any>,
): UExtensionData extends ExtensionDataRef<infer IData, TId, infer IConfig>
? IConfig['optional'] extends true
? IData | undefined
: IData
: never;
};
/**
* Convert a single extension input into a matching resolved input.
* @public
*/
export type ResolvedExtensionInput<TExtensionData extends AnyExtensionDataMap> =
{
node: AppNode;
output: ExtensionDataValues<TExtensionData>;
};
export type ResolvedExtensionInput<
TExtensionData extends AnyExtensionDataMap | AnyExtensionDataRef,
> = [TExtensionData] extends [AnyExtensionDataRef]
? {
node: AppNode;
} & ExtensionDataContainer<TExtensionData>
: TExtensionData extends AnyExtensionDataMap
? {
node: AppNode;
output: ExtensionDataValues<TExtensionData>;
}
: never;
/**
* Converts an extension input map into a matching collection of resolved inputs.
* @public
*/
export type ResolvedExtensionInputs<
TInputs extends { [name in string]: ExtensionInput<any, any> },
TInputs extends { [name in string]: ExtensionInput<any, any, any> },
> = {
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
? Array<Expand<ResolvedExtensionInput<TInputs[InputName]['extensionData']>>>
@@ -77,8 +106,11 @@ export type ResolvedExtensionInputs<
>;
};
/** @public */
export interface CreateExtensionOptions<
/**
* @public
* @deprecated This way of structuring the options is deprecated, this type will be removed in the future
*/
export interface LegacyCreateExtensionOptions<
TOutput extends AnyExtensionDataMap,
TInputs extends AnyExtensionInputMap,
TConfig,
@@ -111,6 +143,61 @@ export interface CreateExtensionOptions<
}): Expand<ExtensionDataValues<TOutput>>;
}
// TODO(Rugvip): This creates a permutation of all possible output tuples,
// taking the optionality into account. It's not optimal, since it might hurt
// performance for large outputs, doesn't provide a very clear error message,
// and doesn't allow us to refactor to allow for a generator.
/** @public */
type ExtensionFactoryOutput<
URef extends AnyExtensionDataRef,
TPickRef extends AnyExtensionDataRef = URef,
> = [URef] extends [never]
? []
: TPickRef extends ExtensionDataRef<infer IData, infer IId, infer IConfig>
?
| (IConfig['optional'] extends true
? ExtensionFactoryOutput<Exclude<URef, TPickRef>>
: never)
| [
ExtensionDataValue<IData, IId>,
...ExtensionFactoryOutput<Exclude<URef, TPickRef>>,
]
: never;
/** @public */
export type CreateExtensionOptions<
UOutput extends AnyExtensionDataRef,
TInputs extends AnyExtensionInputMap,
TConfig,
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
> = {
kind?: string;
namespace?: string;
name?: string;
attachTo: { id: string; input: string };
disabled?: boolean;
inputs?: TInputs;
output: Array<UOutput>;
/** @deprecated - use `config.schema` instead */
configSchema?: PortableSchema<TConfig, TConfigInput>;
config?: {
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): ExtensionFactoryOutput<UOutput>;
};
/** @public */
export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
$$type: '@backstage/ExtensionDefinition';
@@ -127,12 +214,12 @@ export interface InternalExtensionDefinition<TConfig, TConfigInput>
extends ExtensionDefinition<TConfig, TConfigInput> {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly output: AnyExtensionDataMap;
readonly output: AnyExtensionDataMap | Array<AnyExtensionDataRef>;
factory(context: {
node: AppNode;
config: TConfig;
inputs: ResolvedExtensionInputs<any>;
}): ExtensionDataValues<any>;
}): ExtensionDataValues<any> | Iterable<ExtensionDataValue<any, any>>;
}
/** @internal */
@@ -157,6 +244,46 @@ export function toInternalExtensionDefinition<TConfig, TConfigInput>(
}
/** @public */
export function createExtension<
UOutput extends AnyExtensionDataRef,
TInputs extends {
[inputName in string]: ExtensionInput<
AnyExtensionDataRef,
never,
{ optional: boolean; singleton: boolean }
>;
},
TConfig,
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
>(
options: CreateExtensionOptions<
UOutput,
TInputs,
TConfig,
TConfigInput,
TConfigSchema
>,
): ExtensionDefinition<
TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
>;
/**
* @public
* @deprecated - use the array format of `output` instead, see TODO-doc-link
*/
export function createExtension<
TOutput extends AnyExtensionDataMap,
TInputs extends AnyExtensionInputMap,
@@ -164,7 +291,7 @@ export function createExtension<
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
>(
options: CreateExtensionOptions<
options: LegacyCreateExtensionOptions<
TOutput,
TInputs,
TConfig,
@@ -186,6 +313,44 @@ export function createExtension<
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
>;
export function createExtension<
UOutput extends AnyExtensionDataRef,
TInputs extends AnyExtensionInputMap,
TConfig,
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
>(
options:
| CreateExtensionOptions<
UOutput,
TInputs,
TConfig,
TConfigInput,
TConfigSchema
>
| LegacyCreateExtensionOptions<
AnyExtensionDataMap,
TInputs,
TConfig,
TConfigInput,
TConfigSchema
>,
): ExtensionDefinition<
TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
}),
TConfigInput &
(string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>)
> {
const newConfigSchema = options.config?.schema;
if (newConfigSchema && options.configSchema) {
@@ -33,6 +33,13 @@ export type ExtensionDataRef<
readonly config: TConfig;
};
/** @public */
export type AnyExtensionDataRef = ExtensionDataRef<
unknown,
string,
{ optional?: true }
>;
/** @public */
export interface ConfigurableExtensionDataRef<
TData,