Drop Zod v3 support from new configSchema path

The new `configSchema` option now strictly requires StandardSchemaV1
values (e.g. Zod v4 or `zod/v4` from the Zod v3 package). Direct Zod
v3 schemas are no longer silently converted and will throw an error.

The deprecated `config.schema` callback path continues to work with
Zod v3 through a separate `createDeprecatedConfigSchema` function.

Also adds `createZodV4FilterPredicateSchema` to `@backstage/filter-predicates`
as a v4 counterpart to the now-deprecated v3 variant.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-13 17:15:46 +02:00
parent 25392cab00
commit 8923d6def0
18 changed files with 256 additions and 173 deletions
@@ -2,4 +2,4 @@
'@backstage/plugin-catalog-react': patch
---
Migrated alpha entity blueprints to use the new `configSchema` option with direct zod schema values.
Migrated alpha entity blueprints to use the new `configSchema` option with zod v4 schema values.
@@ -0,0 +1,5 @@
---
'@backstage/filter-predicates': patch
---
Added `createZodV4FilterPredicateSchema` as a zod v4 counterpart to `createZodV3FilterPredicateSchema`.
@@ -2,4 +2,4 @@
'@backstage/frontend-plugin-api': patch
---
Added a new `configSchema` option for `createExtension` and `createExtensionBlueprint` that accepts direct schema values from any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible library. The old `config.schema` option is now deprecated. See the [1.50 migration documentation](https://backstage.io/docs/frontend-system/architecture/migrations#150) for more information.
Added a new `configSchema` option for `createExtension` and `createExtensionBlueprint` that accepts direct schema values from any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible library with JSON Schema support, such as zod v4 or the `zod/v4` subpath from zod v3. The old `config.schema` option is now deprecated. Note that direct zod v3 schemas are not supported by the new `configSchema` option — use `import { z } from 'zod/v4'` from the zod v3 package, or upgrade to zod v4. See the [1.50 migration documentation](https://backstage.io/docs/frontend-system/architecture/migrations#150) for more information.
@@ -275,7 +275,7 @@ In addition to being able to access data passed through the input, you also have
## Extension configuration
With the `app-config.yaml` there is already the option to pass configuration to plugins or the app to e.g. define the `baseURL` of your app. For extensions this concept would be limiting as an extension can be independent of the plugin & initiated several times. Therefore we created a possibility to configure each extension individually through config. The extension config schema is created using the [`zod`](https://zod.dev/) library, which in addition to TypeScript type checking also provides runtime validation and coercion. If we continue with the example of the `navigationExtension` and now want it to contain a configurable title, we could make it available like the following:
With the `app-config.yaml` there is already the option to pass configuration to plugins or the app to e.g. define the `baseURL` of your app. For extensions this concept would be limiting as an extension can be independent of the plugin & initiated several times. Therefore we created a possibility to configure each extension individually through config. The extension config schema is created using any schema library that implements the [Standard Schema](https://github.com/standard-schema/standard-schema) interface with JSON Schema support, such as [`zod`](https://zod.dev/) v4 (or `import { z } from 'zod/v4'` from the zod v3 package). In addition to TypeScript type checking, the schema also provides runtime validation and coercion. If we continue with the example of the `navigationExtension` and now want it to contain a configurable title, we could make it available like the following:
```tsx
import { z } from 'zod';
@@ -15,9 +15,9 @@ This guide is intended for app and plugin authors who have already migrated thei
### New `configSchema` option for extension config
The `config.schema` option for `createExtension` and `createExtensionBlueprint` is now deprecated in favor of a new top-level `configSchema` option. The new option accepts direct schema values from any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible library, rather than requiring factory functions. The `createSchemaFromZod` helper has also been removed.
The `config.schema` option for `createExtension` and `createExtensionBlueprint` is now deprecated in favor of a new top-level `configSchema` option. The new option accepts direct schema values from any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible library with JSON Schema support, rather than requiring factory functions. The `createSchemaFromZod` helper has also been removed.
The recommended migration target is [zod v4](https://zod.dev/), but any Standard Schema compatible library or version works, including zod v3.25+.
The `configSchema` option requires schemas that implement the Standard Schema interface with JSON Schema support. This means you need to use [zod v4](https://zod.dev/) or the `zod/v4` subpath export from the zod v3 package (v3.25+). Direct zod v3 schemas are **not** supported by the new `configSchema` option — they are only supported through the deprecated `config.schema` callback format.
For example, an extension previously declared like this:
@@ -36,10 +36,13 @@ createExtension({
});
```
Should now look like this:
Should now look like this, using zod v4 or the `zod/v4` subpath:
```tsx
// Either import from zod v4 directly:
import { z } from 'zod';
// Or use the v4 subpath from the zod v3 package:
// import { z } from 'zod/v4';
createExtension({
// ...
@@ -86,7 +89,7 @@ MyBlueprint.makeWithOverrides({
});
```
Each field in the `configSchema` record is a standalone schema value rather than a factory function. This decouples the config schema declaration from any specific zod version, and lets you use any schema library that implements the Standard Schema interface.
Each field in the `configSchema` record is a standalone schema value rather than a factory function. This decouples the config schema declaration from any specific zod version, and lets you use any schema library that implements the Standard Schema interface with JSON Schema support.
## 1.31
+1 -1
View File
@@ -27,10 +27,10 @@ export const coreComponentsTranslationRef: TranslationRef<
readonly 'signIn.title': 'Sign In';
readonly 'signIn.loginFailed': 'Login failed';
readonly 'signIn.customProvider.title': 'Custom User';
readonly 'signIn.customProvider.continue': 'Continue';
readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.';
readonly 'signIn.customProvider.userId': 'User ID';
readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token';
readonly 'signIn.customProvider.continue': 'Continue';
readonly 'signIn.customProvider.idToken': 'ID Token (optional)';
readonly 'signIn.guestProvider.title': 'Guest';
readonly 'signIn.guestProvider.enter': 'Enter';
+1 -1
View File
@@ -245,10 +245,10 @@ export const coreComponentsTranslationRef: TranslationRef<
readonly 'signIn.title': 'Sign In';
readonly 'signIn.loginFailed': 'Login failed';
readonly 'signIn.customProvider.title': 'Custom User';
readonly 'signIn.customProvider.continue': 'Continue';
readonly 'signIn.customProvider.subtitle': 'Enter your own User ID and credentials.\n This selection will not be stored.';
readonly 'signIn.customProvider.userId': 'User ID';
readonly 'signIn.customProvider.tokenInvalid': 'Token is not a valid OpenID Connect JWT Token';
readonly 'signIn.customProvider.continue': 'Continue';
readonly 'signIn.customProvider.idToken': 'ID Token (optional)';
readonly 'signIn.guestProvider.title': 'Guest';
readonly 'signIn.guestProvider.enter': 'Enter';
+7
View File
@@ -5,6 +5,7 @@
```ts
import { Config } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { z } from 'zod/v4';
import * as zodV3 from 'zod/v3';
// @public
@@ -12,6 +13,12 @@ export function createZodV3FilterPredicateSchema(
z: typeof zodV3.z,
): zodV3.ZodType<FilterPredicate>;
// @public
export function createZodV4FilterPredicateSchema(): z.ZodType<
FilterPredicate,
FilterPredicate
>;
// @public
export function evaluateFilterPredicate(
predicate: FilterPredicate,
@@ -26,6 +26,7 @@ export {
export { getJsonValueAtPath } from './getJsonValueAtPath';
export {
createZodV3FilterPredicateSchema,
createZodV4FilterPredicateSchema,
parseFilterPredicate,
} from './schema';
export type {
@@ -17,6 +17,7 @@
import { InputError } from '@backstage/errors';
import { fromZodError } from 'zod-validation-error/v3';
import * as zodV3 from 'zod/v3';
import { z as zodV4 } from 'zod/v4';
import {
FilterPredicate,
FilterPredicateExpression,
@@ -69,6 +70,57 @@ export function createZodV3FilterPredicateSchema(
return predicateSchema;
}
/**
* Creates a zod v4 schema for validating filter predicates.
*
* @public
*/
export function createZodV4FilterPredicateSchema(): zodV4.ZodType<
FilterPredicate,
FilterPredicate
> {
const z = zodV4;
const primitiveSchema = z.union([
z.string(),
z.number(),
z.boolean(),
]) as zodV4.ZodType<FilterPredicatePrimitive, FilterPredicatePrimitive>;
// eslint-disable-next-line prefer-const
let valuePredicateSchema: zodV4.ZodType<
FilterPredicateValue,
FilterPredicateValue
>;
const expressionSchema = z.lazy(() =>
z.union([
z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema),
z.record(z.string().regex(/^\$/), z.never()),
]),
) as zodV4.ZodType<FilterPredicateExpression, FilterPredicateExpression>;
const predicateSchema = z.lazy(() =>
z.union([
expressionSchema,
primitiveSchema,
z.object({ $all: z.array(predicateSchema) }),
z.object({ $any: z.array(predicateSchema) }),
z.object({ $not: predicateSchema }),
]),
) as zodV4.ZodType<FilterPredicate, FilterPredicate>;
valuePredicateSchema = z.union([
primitiveSchema,
z.object({ $exists: z.boolean() }),
z.object({ $in: z.array(primitiveSchema) }),
z.object({ $contains: predicateSchema }),
z.object({ $hasPrefix: z.string() }),
]) as zodV4.ZodType<FilterPredicateValue, FilterPredicateValue>;
return predicateSchema;
}
/**
* Parses a value to check that it's a valid filter predicate.
*
@@ -40,7 +40,7 @@ import {
resolveExtensionDefinition,
} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { createConfigSchema } from '../../../frontend-plugin-api/src/schema/createPortableSchema';
import { createDeprecatedConfigSchema } from '../../../frontend-plugin-api/src/schema/createPortableSchema';
import { TestApiRegistry, withLogCollector } from '@backstage/test-utils';
import { createErrorCollector } from '../wiring/createErrorCollector';
@@ -181,7 +181,7 @@ describe('instantiateAppNodeTree', () => {
test: testDataRef,
other: otherDataRef.optional(),
},
configSchema: createConfigSchema({
configSchema: createDeprecatedConfigSchema({
output: z => z.string().default('test'),
other: z => z.number().optional(),
}),
@@ -14,77 +14,15 @@
* limitations under the License.
*/
import { z as zodV3 } from 'zod/v3';
import { z as zodV4 } from 'zod/v4';
import {
createConfigSchema,
createDeprecatedConfigSchema,
mergePortableSchemas,
} from './createPortableSchema';
describe('createConfigSchema', () => {
describe('zod v3 schemas', () => {
it('should report a missing required field', () => {
const schema = createConfigSchema({ name: z => z.string() });
expect(() => schema.parse({})).toThrow(
"Missing required value at 'name'",
);
expect(() => schema.parse(undefined)).toThrow(
"Missing required value at 'name'",
);
});
it('should report a type mismatch', () => {
const schema = createConfigSchema({ count: z => z.number() });
expect(() => schema.parse({ count: 'not a number' })).toThrow(
"Expected number, received string at 'count'",
);
});
it('should report nested object errors with the full path', () => {
const schema = createConfigSchema({
settings: z => z.object({ port: z.number() }),
});
expect(() => schema.parse({ settings: { port: 'abc' } })).toThrow(
"Expected number, received string at 'settings.port'",
);
});
it('should report errors for union types', () => {
const schema = createConfigSchema({
value: z => z.union([z.string(), z.number()]),
});
expect(() => schema.parse({})).toThrow(
"Missing required value at 'value'",
);
});
it('should combine errors from multiple fields', () => {
const schema = createConfigSchema({
name: z => z.string(),
count: z => z.number(),
});
expect(() => schema.parse({})).toThrow(
"Missing required value at 'name'; Missing required value at 'count'",
);
});
it('should apply defaults for optional fields with defaults', () => {
const schema = createConfigSchema({
name: z => z.string(),
mode: z => z.enum(['fast', 'slow']).default('fast'),
});
expect(schema.parse({ name: 'test' })).toEqual({
name: 'test',
mode: 'fast',
});
});
});
describe('zod v4 schemas', () => {
it('should report a missing required field', () => {
const schema = createConfigSchema({ name: zodV4.string() });
@@ -150,57 +88,22 @@ describe('createConfigSchema', () => {
});
});
describe('mixed zod v3 and v4 schemas', () => {
it('should validate fields from both schema versions', () => {
const schema = createConfigSchema({
v3field: z => z.string(),
v4field: zodV4.number(),
});
expect(schema.parse({ v3field: 'hello', v4field: 42 })).toEqual({
v3field: 'hello',
v4field: 42,
});
});
it('should report errors from both schema versions', () => {
const schema = createConfigSchema({
v3field: z => z.string(),
v4field: zodV4.number(),
});
expect(() => schema.parse({})).toThrow(
"Missing required value at 'v3field'; " +
"Invalid input: expected number, received undefined at 'v4field'",
describe('zod v3 rejection', () => {
it('should reject a direct zod v3 schema', () => {
expect(() => createConfigSchema({ name: zodV3.string() as any })).toThrow(
"Config schema for field 'name' uses a Zod v3 schema, which is " +
'not supported by the `configSchema` option. Either use ' +
"`import { z } from 'zod/v4'` from the zod v3 package, or " +
'upgrade to zod v4.',
);
});
it('should produce correct JSON Schema for mixed schemas', () => {
const schema = createConfigSchema({
v3field: z => z.string(),
v4field: zodV4.number().optional(),
});
const result = schema.schema();
expect(result.schema).toMatchObject({
type: 'object',
properties: {
v3field: { type: 'string' },
v4field: { type: 'number' },
},
required: ['v3field'],
additionalProperties: false,
});
});
});
describe('schema creation errors', () => {
it('should reject a schema that is not a valid Standard Schema or zod schema', () => {
it('should reject a schema that is not a valid Standard Schema', () => {
expect(() =>
createConfigSchema({ bad: { notASchema: true } as any }),
).toThrow(
"Config schema for field 'bad' is not a valid Standard Schema or zod schema",
);
).toThrow("Config schema for field 'bad' is not a valid Standard Schema");
});
it('should reject a Standard Schema without JSON Schema support', () => {
@@ -223,8 +126,8 @@ describe('createConfigSchema', () => {
describe('JSON Schema generation', () => {
it('should generate JSON Schema lazily via schema()', () => {
const schema = createConfigSchema({
title: z => z.string(),
count: z => z.number().optional(),
title: zodV4.string(),
count: zodV4.number().optional(),
});
const result = schema.schema();
@@ -242,7 +145,7 @@ describe('createConfigSchema', () => {
it('should support backward-compatible property access on schema', () => {
const schema = createConfigSchema({
title: z => z.string(),
title: zodV4.string(),
});
expect(schema.schema.type).toBe('object');
@@ -251,9 +154,9 @@ describe('createConfigSchema', () => {
});
describe('merging schemas', () => {
it('should merge two zod v3 schemas and parse correctly', () => {
const a = createConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: z => z.number().default(0) });
it('should merge two zod v4 schemas and parse correctly', () => {
const a = createConfigSchema({ name: zodV4.string() });
const b = createConfigSchema({ count: zodV4.number().default(0) });
const merged = mergePortableSchemas(a, b)!;
expect(merged.parse({ name: 'hello' })).toEqual({
@@ -262,8 +165,8 @@ describe('createConfigSchema', () => {
});
});
it('should merge zod v3 and v4 schemas', () => {
const a = createConfigSchema({ name: z => z.string() });
it('should merge deprecated v3 and new v4 schemas', () => {
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number().default(0) });
const merged = mergePortableSchemas(a, b)!;
@@ -274,7 +177,7 @@ describe('createConfigSchema', () => {
});
it('should produce combined errors after merge', () => {
const a = createConfigSchema({ name: z => z.string() });
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
@@ -286,7 +189,7 @@ describe('createConfigSchema', () => {
});
it('should produce combined errors for type mismatches after merge', () => {
const a = createConfigSchema({ name: z => z.string() });
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
@@ -298,7 +201,7 @@ describe('createConfigSchema', () => {
});
it('should produce correct JSON Schema after merge', () => {
const a = createConfigSchema({ name: z => z.string() });
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number().optional() });
const merged = mergePortableSchemas(a, b)!;
@@ -316,7 +219,7 @@ describe('createConfigSchema', () => {
});
it('should handle merge with undefined', () => {
const a = createConfigSchema({ name: z => z.string() });
const a = createConfigSchema({ name: zodV4.string() });
expect(mergePortableSchemas(a, undefined)).toBe(a);
expect(mergePortableSchemas(undefined, a)).toBe(a);
@@ -324,7 +227,7 @@ describe('createConfigSchema', () => {
});
it('should let later fields win when merging overlapping keys', () => {
const a = createConfigSchema({ x: z => z.string() });
const a = createDeprecatedConfigSchema({ x: z => z.string() });
const b = createConfigSchema({ x: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
@@ -351,7 +254,7 @@ describe('createConfigSchema', () => {
});
it('should throw a clear error for non-object input', () => {
const schema = createConfigSchema({ title: z => z.string() });
const schema = createConfigSchema({ title: zodV4.string() });
expect(() => schema.parse('not an object')).toThrow(
'Invalid config input, expected object but got string',
@@ -369,9 +272,9 @@ describe('createConfigSchema', () => {
it('should not produce undefined keys for absent optional fields', () => {
const schema = createConfigSchema({
name: z => z.string(),
title: z => z.string().optional(),
count: z => z.number().default(42),
name: zodV4.string(),
title: zodV4.string().optional(),
count: zodV4.number().default(42),
});
const result = schema.parse({ name: 'hello' });
@@ -379,11 +282,11 @@ describe('createConfigSchema', () => {
expect(Object.keys(result as object)).toEqual(['name', 'count']);
});
it('should not mark defaulted zod v3 fields as required in JSON Schema', () => {
it('should not mark defaulted fields as required in JSON Schema', () => {
const schema = createConfigSchema({
name: z => z.string(),
title: z => z.string().default('hello'),
count: z => z.number().optional(),
name: zodV4.string(),
title: zodV4.string().default('hello'),
count: zodV4.number().optional(),
});
const result = schema.schema();
@@ -394,3 +297,85 @@ describe('createConfigSchema', () => {
});
});
});
describe('createDeprecatedConfigSchema', () => {
it('should report a missing required field', () => {
const schema = createDeprecatedConfigSchema({ name: z => z.string() });
expect(() => schema.parse({})).toThrow("Missing required value at 'name'");
expect(() => schema.parse(undefined)).toThrow(
"Missing required value at 'name'",
);
});
it('should report a type mismatch', () => {
const schema = createDeprecatedConfigSchema({ count: z => z.number() });
expect(() => schema.parse({ count: 'not a number' })).toThrow(
"Expected number, received string at 'count'",
);
});
it('should report nested object errors with the full path', () => {
const schema = createDeprecatedConfigSchema({
settings: z => z.object({ port: z.number() }),
});
expect(() => schema.parse({ settings: { port: 'abc' } })).toThrow(
"Expected number, received string at 'settings.port'",
);
});
it('should report errors for union types', () => {
const schema = createDeprecatedConfigSchema({
value: z => z.union([z.string(), z.number()]),
});
expect(() => schema.parse({})).toThrow("Missing required value at 'value'");
});
it('should apply defaults for optional fields with defaults', () => {
const schema = createDeprecatedConfigSchema({
name: z => z.string(),
mode: z => z.enum(['fast', 'slow']).default('fast'),
});
expect(schema.parse({ name: 'test' })).toEqual({
name: 'test',
mode: 'fast',
});
});
it('should generate JSON Schema lazily via schema()', () => {
const schema = createDeprecatedConfigSchema({
title: z => z.string(),
count: z => z.number().optional(),
});
const result = schema.schema();
expect(result).toHaveProperty('schema');
expect(result.schema).toMatchObject({
type: 'object',
properties: {
title: { type: 'string' },
count: { type: 'number' },
},
required: ['title'],
additionalProperties: false,
});
});
it('should not mark defaulted fields as required in JSON Schema', () => {
const schema = createDeprecatedConfigSchema({
name: z => z.string(),
title: z => z.string().default('hello'),
count: z => z.number().optional(),
});
const result = schema.schema();
expect(result.schema).toMatchObject({
type: 'object',
required: ['name'],
});
});
});
@@ -26,6 +26,19 @@ import { PortableSchema } from './types';
export type { StandardSchemaV1 } from '@standard-schema/spec';
import { type StandardSchemaV1 } from '@standard-schema/spec';
/** @internal */
export function createDeprecatedConfigSchema(
fields: Record<string, (zImpl: typeof zodV3) => ZodType>,
): MergeablePortableSchema {
const resolved: Record<string, ResolvedField> = {};
for (const [key, field] of Object.entries(fields)) {
resolved[key] = resolveZodField(key, field(zodV3));
}
return buildPortableSchema(resolved);
}
/**
* Per-field resolved schema validation is eager, JSON Schema is lazy.
* @internal
@@ -53,13 +66,12 @@ export interface MergeablePortableSchema<TOutput = any, TInput = any>
* @internal
*/
export function createConfigSchema(
fields: Record<string, StandardSchemaV1 | ((zImpl: typeof zodV3) => ZodType)>,
fields: Record<string, StandardSchemaV1>,
): MergeablePortableSchema {
const resolved: Record<string, ResolvedField> = {};
for (const [key, field] of Object.entries(fields)) {
const schema = typeof field === 'function' ? field(zodV3) : field;
resolved[key] = resolveField(key, schema);
resolved[key] = resolveField(key, field);
}
return buildPortableSchema(resolved);
@@ -165,20 +177,25 @@ function buildPortableSchema<TOutput = unknown>(
*/
function resolveField(key: string, schema: unknown): ResolvedField {
if (isZodV3Type(schema)) {
return resolveZodField(key, schema);
throw new Error(
`Config schema for field '${key}' uses a Zod v3 schema, which is ` +
`not supported by the \`configSchema\` option. Either use ` +
`\`import { z } from 'zod/v4'\` from the zod v3 package, or ` +
`upgrade to zod v4.`,
);
}
if (isStandardSchema(schema)) {
if (!hasJsonSchemaConverter(schema)) {
throw new Error(
`Config schema for field '${key}' does not support JSON Schema ` +
`conversion. Use a schema library that implements the Standard ` +
`JSON Schema interface (like zod v4+), or use a zod v3 schema.`,
`JSON Schema interface (like zod v4+).`,
);
}
return resolveStandardField(key, schema);
}
throw new Error(
`Config schema for field '${key}' is not a valid Standard Schema or zod schema`,
`Config schema for field '${key}' is not a valid Standard Schema`,
);
}
@@ -340,7 +357,8 @@ export function warnConfigSchemaPropDeprecation(callSite: string) {
console.warn(
`DEPRECATION WARNING: The \`config.schema\` option for extension config is deprecated. ` +
`Use the \`configSchema\` option instead with Standard Schema values, for example ` +
`\`configSchema: { title: z.string() }\` using zod v3.25+ or v4. ` +
`\`configSchema: { title: z.string() }\` using zod v4 ` +
`(or \`import { z } from 'zod/v4'\` from the zod v3 package). ` +
`Declared at ${callSite}`,
);
}
@@ -29,6 +29,8 @@ import { ExtensionInput } from './createExtensionInput';
import type { z } from 'zod/v3';
import {
createConfigSchema,
createDeprecatedConfigSchema,
mergePortableSchemas,
warnConfigSchemaPropDeprecation,
} from '../schema/createPortableSchema';
import { describeParentCallSite } from '../routing/describeParentCallSite';
@@ -618,13 +620,15 @@ export function createExtension<
export function createExtension(
options: any,
): OverridableExtensionDefinition<any> {
const schemaDeclaration =
options.configSchema ?? (options.config?.schema as any);
if (options.config?.schema) {
warnConfigSchemaPropDeprecation(describeParentCallSite());
}
const resolvedConfigSchema =
schemaDeclaration && createConfigSchema(schemaDeclaration);
const resolvedConfigSchema = mergePortableSchemas(
options.config?.schema
? createDeprecatedConfigSchema(options.config.schema)
: undefined,
options.configSchema ? createConfigSchema(options.configSchema) : undefined,
);
return OpaqueExtensionDefinition.createInstance('v2', {
T: undefined as any,
@@ -730,15 +734,19 @@ export function createExtension(
),
output: (overrideOptions.output ??
options.output) as ExtensionDataRef[],
configSchema:
options.config ||
options.configSchema ||
overrideOptions.config ||
overrideOptions.configSchema
config:
options.config?.schema || overrideOptions.config?.schema
? {
schema: {
...options.config?.schema,
...overrideOptions.config?.schema,
},
}
: undefined,
configSchema:
options.configSchema || overrideOptions.configSchema
? {
...options.config?.schema,
...options.configSchema,
...overrideOptions.config?.schema,
...overrideOptions.configSchema,
}
: undefined,
@@ -31,7 +31,7 @@ import {
import { createExtensionInput } from './createExtensionInput';
import { RouteRef } from '../routing';
import { createExtension, ExtensionDefinition } from './createExtension';
import { z as zodV3 } from 'zod/v3';
import { z as zodV4 } from 'zod/v4';
import {
createExtensionDataContainer,
OpaqueExtensionDefinition,
@@ -316,7 +316,7 @@ describe('createExtensionBlueprint', () => {
attachTo: { id: 'test', input: 'default' },
output: [coreExtensionData.reactElement],
configSchema: {
title: zodV3.string().default('default title'),
title: zodV4.string().default('default title'),
},
factory(_, { config }) {
return [
@@ -743,15 +743,19 @@ export function createExtensionBlueprint(options: any): any {
if: args.if ?? options.if,
inputs: { ...args.inputs, ...options.inputs },
output: (args.output ?? options.output) as ExtensionDataRef[],
configSchema:
options.configSchema ||
args.configSchema ||
options.config?.schema ||
args.config?.schema
config:
options.config?.schema || args.config?.schema
? {
schema: {
...options.config?.schema,
...args.config?.schema,
},
}
: undefined,
configSchema:
options.configSchema || args.configSchema
? {
...options.config?.schema,
...options.configSchema,
...args.config?.schema,
...args.configSchema,
}
: (undefined as any),
@@ -28,11 +28,11 @@ import {
} from './extensionData';
import {
FilterPredicate,
createZodV3FilterPredicateSchema,
createZodV4FilterPredicateSchema,
} from '@backstage/filter-predicates';
import { resolveEntityFilterData } from './resolveEntityFilterData';
import { Entity } from '@backstage/catalog-model';
import { z } from 'zod/v3';
import { z } from 'zod/v4';
/**
* @alpha
@@ -54,7 +54,7 @@ export const EntityCardBlueprint = createExtensionBlueprint({
},
configSchema: {
filter: z
.union([z.string(), createZodV3FilterPredicateSchema(z)])
.union([z.string(), createZodV4FilterPredicateSchema()])
.optional(),
type: z.enum(entityCardTypes).optional(),
},
@@ -30,12 +30,12 @@ import {
} from './extensionData';
import {
FilterPredicate,
createZodV3FilterPredicateSchema,
createZodV4FilterPredicateSchema,
} from '@backstage/filter-predicates';
import { resolveEntityFilterData } from './resolveEntityFilterData';
import { Entity } from '@backstage/catalog-model';
import { ReactElement } from 'react';
import { z } from 'zod/v3';
import { z } from 'zod/v4';
/**
* @alpha
@@ -65,7 +65,7 @@ export const EntityContentBlueprint = createExtensionBlueprint({
path: z.string().optional(),
title: z.string().optional(),
filter: z
.union([z.string(), createZodV3FilterPredicateSchema(z)])
.union([z.string(), createZodV4FilterPredicateSchema()])
.optional(),
group: z.literal(false).or(z.string()).optional(),
icon: z.string().optional(),