feat: added input types for the config to the extensions

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-07-24 14:36:22 +02:00
parent b424956a35
commit 0ad542cbc7
26 changed files with 300 additions and 116 deletions
@@ -29,7 +29,7 @@ import { BackstagePlugin, Extension, ExtensionDataRef } from '../../wiring';
export interface AppNodeSpec {
readonly id: string;
readonly attachTo: { id: string; input: string };
readonly extension: Extension<unknown>;
readonly extension: Extension<unknown, unknown>;
readonly disabled: boolean;
readonly config?: unknown;
readonly source?: BackstagePlugin;
@@ -28,6 +28,12 @@ export const IconBundleBlueprint = createExtensionBlueprint({
output: {
icons: iconsDataRef,
},
config: {
schema: {
icons: z => z.string().default('blob'),
test: z => z.string(),
},
},
factory: (params: { icons: { [key in string]: IconComponent } }) => params,
dataRefs: {
icons: iconsDataRef,
@@ -25,10 +25,19 @@ describe('createSchemaFromZod', () => {
}),
);
expect(() => parse({ derp: { bar: 'derp' } })).toThrow(
expect(() => {
// @ts-expect-error
return parse({ derp: { bar: 'derp' } });
}).toThrow(
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
);
expect(() => parse(undefined)).toThrow(`Missing required value`);
expect(() => parse('derp')).toThrow(`Expected object, received string`);
expect(() => {
// @ts-expect-error
return parse(undefined);
}).toThrow(`Missing required value`);
expect(() => {
// @ts-expect-error
return parse('derp');
}).toThrow(`Expected object, received string`);
});
});
@@ -25,7 +25,7 @@ import { PortableSchema } from './types';
*/
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput> {
): PortableSchema<TOutput, TInput> {
const schema = schemaCreator(z);
return {
// TODO: Types allow z.array etc here but it will break stuff
@@ -17,7 +17,7 @@
import { JsonObject } from '@backstage/types';
/** @public */
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
export type PortableSchema<TOutput, TInput = TOutput> = {
parse: (input: TInput) => TOutput;
schema: JsonObject;
};
@@ -334,6 +334,7 @@ describe('createExtension', () => {
foo: 'x',
bar: 'y',
baz: 'z',
// @ts-expect-error
qux: 'w',
}),
).toEqual({
@@ -349,8 +350,9 @@ describe('createExtension', () => {
foo: 'x',
bar: 'bar',
});
expect(() => extension.configSchema?.parse({})).toThrow(
"Missing required value at 'foo'",
);
expect(() => {
// @ts-expect-error
return extension.configSchema?.parse({});
}).toThrow("Missing required value at 'foo'");
});
});
@@ -82,6 +82,7 @@ export interface CreateExtensionOptions<
TOutput extends AnyExtensionDataMap,
TInputs extends AnyExtensionInputMap,
TConfig,
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
> {
kind?: string;
@@ -92,33 +93,38 @@ export interface CreateExtensionOptions<
inputs?: TInputs;
output: TOutput;
/** @deprecated - use `config.schema` instead */
configSchema?: PortableSchema<TConfig>;
configSchema?: PortableSchema<TConfig, TConfigInput>;
config?: {
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
config: TConfig & {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
};
config: TConfig &
(string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
});
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Expand<ExtensionDataValues<TOutput>>;
}
/** @public */
export interface ExtensionDefinition<TConfig> {
export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
$$type: '@backstage/ExtensionDefinition';
readonly kind?: string;
readonly namespace?: string;
readonly name?: string;
readonly attachTo: { id: string; input: string };
readonly disabled: boolean;
readonly configSchema?: PortableSchema<TConfig>;
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
}
/** @internal */
export interface InternalExtensionDefinition<TConfig>
extends ExtensionDefinition<TConfig> {
export interface InternalExtensionDefinition<TConfig, TConfigInput>
extends ExtensionDefinition<TConfig, TConfigInput> {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly output: AnyExtensionDataMap;
@@ -130,10 +136,13 @@ export interface InternalExtensionDefinition<TConfig>
}
/** @internal */
export function toInternalExtensionDefinition<TConfig>(
overrides: ExtensionDefinition<TConfig>,
): InternalExtensionDefinition<TConfig> {
const internal = overrides as InternalExtensionDefinition<TConfig>;
export function toInternalExtensionDefinition<TConfig, TConfigInput>(
overrides: ExtensionDefinition<TConfig, TConfigInput>,
): InternalExtensionDefinition<TConfig, TConfigInput> {
const internal = overrides as InternalExtensionDefinition<
TConfig,
TConfigInput
>;
if (internal.$$type !== '@backstage/ExtensionDefinition') {
throw new Error(
`Invalid extension definition instance, bad type '${internal.$$type}'`,
@@ -152,10 +161,32 @@ export function createExtension<
TOutput extends AnyExtensionDataMap,
TInputs extends AnyExtensionInputMap,
TConfig,
TConfigInput,
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig, TConfigSchema>,
): ExtensionDefinition<TConfig> {
options: CreateExtensionOptions<
TOutput,
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) {
throw new Error(`Cannot provide both configSchema and config.schema`);
@@ -205,5 +236,22 @@ export function createExtension<
parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`);
return `ExtensionDefinition{${parts.join(',')}}`;
},
} as InternalExtensionDefinition<TConfig>;
} as InternalExtensionDefinition<
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]>;
}>
>)
>;
}
@@ -67,6 +67,7 @@ export interface ExtensionBlueprint<
TInputs extends AnyExtensionInputMap,
TOutput extends AnyExtensionDataMap,
TConfig extends { [key in string]: unknown },
TConfigInput extends { [key in string]: unknown },
TDataRefs extends AnyExtensionDataMap,
> {
dataRefs: TDataRefs;
@@ -125,7 +126,15 @@ export interface ExtensionBlueprint<
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
>;
} & TConfig
} & TConfig,
z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
>;
}>
> &
TConfigInput
>;
}
@@ -199,7 +208,18 @@ class ExtensionBlueprintImpl<
>;
} & {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
}
},
z.input<
z.ZodObject<
{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}
>
>
> {
const schema = {
...this.options.config?.schema,
@@ -279,6 +299,13 @@ export function createExtensionBlueprint<
string extends keyof TConfigSchema
? {}
: { [key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>> },
string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>,
TDataRefs
> {
return new ExtensionBlueprintImpl(options) as ExtensionBlueprint<
@@ -290,6 +317,13 @@ export function createExtensionBlueprint<
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
},
string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>,
TDataRefs
>;
}
@@ -23,7 +23,7 @@ import { ExtensionOverrides, FeatureFlagConfig } from './types';
/** @public */
export interface ExtensionOverridesOptions {
extensions: ExtensionDefinition<unknown>[];
extensions: ExtensionDefinition<any, any>[];
featureFlags?: FeatureFlagConfig[];
}
@@ -34,7 +34,7 @@ export interface PluginOptions<
id: string;
routes?: Routes;
externalRoutes?: ExternalRoutes;
extensions?: ExtensionDefinition<unknown>[];
extensions?: ExtensionDefinition<any, any>[];
featureFlags?: FeatureFlagConfig[];
}
@@ -26,16 +26,17 @@ import {
import { PortableSchema } from '../schema';
/** @public */
export interface Extension<TConfig> {
export interface Extension<TConfig, TConfigInput = TConfig> {
$$type: '@backstage/Extension';
readonly id: string;
readonly attachTo: { id: string; input: string };
readonly disabled: boolean;
readonly configSchema?: PortableSchema<TConfig>;
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
}
/** @internal */
export interface InternalExtension<TConfig> extends Extension<TConfig> {
export interface InternalExtension<TConfig, TConfigInput>
extends Extension<TConfig, TConfigInput> {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly output: AnyExtensionDataMap;
@@ -47,10 +48,10 @@ export interface InternalExtension<TConfig> extends Extension<TConfig> {
}
/** @internal */
export function toInternalExtension<TConfig>(
overrides: Extension<TConfig>,
): InternalExtension<TConfig> {
const internal = overrides as InternalExtension<TConfig>;
export function toInternalExtension<TConfig, TConfigInput>(
overrides: Extension<TConfig, TConfigInput>,
): InternalExtension<TConfig, TConfigInput> {
const internal = overrides as InternalExtension<TConfig, TConfigInput>;
if (internal.$$type !== '@backstage/Extension') {
throw new Error(
`Invalid extension instance, bad type '${internal.$$type}'`,
@@ -65,10 +66,10 @@ export function toInternalExtension<TConfig>(
}
/** @internal */
export function resolveExtensionDefinition<TConfig>(
definition: ExtensionDefinition<TConfig>,
export function resolveExtensionDefinition<TConfig, TConfigInput>(
definition: ExtensionDefinition<TConfig, TConfigInput>,
context?: { namespace?: string },
): Extension<TConfig> {
): Extension<TConfig, TConfigInput> {
const internalDefinition = toInternalExtensionDefinition(definition);
const { name, kind, namespace: _, ...rest } = internalDefinition;
const namespace = internalDefinition.namespace ?? context?.namespace;
@@ -91,5 +92,5 @@ export function resolveExtensionDefinition<TConfig>(
toString() {
return `Extension{id=${id}}`;
},
} as Extension<TConfig>;
} as Extension<TConfig, TConfigInput>;
}