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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user