Migrate catalog-react and app blueprints to configSchema

Move EntityCardBlueprint and EntityContentBlueprint to the new
`configSchema` option using zod v3, and AppLanguageApi to zod v4.
Fix config schema merge in `makeWithOverrides` when mixing
`configSchema` and deprecated `config.schema` sources.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-10 10:39:50 +02:00
parent 8bb8560557
commit 1ef7954725
8 changed files with 74 additions and 482 deletions
@@ -738,19 +738,15 @@ export function createExtensionBlueprint(options: any): any {
if: args.if ?? options.if,
inputs: { ...args.inputs, ...options.inputs },
output: (args.output ?? options.output) as ExtensionDataRef[],
config:
options.config || args.config
? {
schema: {
...options.config?.schema,
...args.config?.schema,
},
}
: undefined,
configSchema:
options.configSchema || args.configSchema
options.configSchema ||
args.configSchema ||
options.config?.schema ||
args.config?.schema
? {
...options.config?.schema,
...options.configSchema,
...args.config?.schema,
...args.configSchema,
}
: (undefined as any),
+4 -5
View File
@@ -18,14 +18,13 @@
import { AppLanguageSelector } from '../../../../packages/core-app-api/src/apis/implementations/AppLanguageApi';
import { appLanguageApiRef } from '@backstage/frontend-plugin-api';
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
import { z } from 'zod';
export const AppLanguageApi = ApiBlueprint.makeWithOverrides({
name: 'app-language',
config: {
schema: {
defaultLanguage: z => z.string().optional(),
availableLanguages: z => z.array(z.string()).optional(),
},
configSchema: {
defaultLanguage: z.string().optional(),
availableLanguages: z.array(z.string()).optional(),
},
factory(originalFactory, { config }) {
return originalFactory(defineParams =>
+3 -3
View File
@@ -86,7 +86,8 @@
"qs": "^6.9.4",
"react-use": "^17.2.4",
"yaml": "^2.0.0",
"zen-observable": "^0.10.0"
"zen-observable": "^0.10.0",
"zod": "^3.25.76 || ^4.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
@@ -104,8 +105,7 @@
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.30.2",
"react-test-renderer": "^16.13.1",
"zod": "^3.25.76 || ^4.0.0"
"react-test-renderer": "^16.13.1"
},
"peerDependencies": {
"@backstage/frontend-test-utils": "workspace:^",
@@ -45,158 +45,20 @@ describe('EntityCardBlueprint', () => {
"input": "cards",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"filter": {
"anyOf": [
{
"type": "string",
},
{
"anyOf": [
{
"anyOf": [
{
"additionalProperties": {
"anyOf": [
{
"type": [
"string",
"number",
"boolean",
],
},
{
"additionalProperties": false,
"properties": {
"$exists": {
"type": "boolean",
},
},
"required": [
"$exists",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$in": {
"items": {
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
"type": "array",
},
},
"required": [
"$in",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$contains": {
"$ref": "#/properties/filter/anyOf/1",
},
},
"required": [
"$contains",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$hasPrefix": {
"type": "string",
},
},
"required": [
"$hasPrefix",
],
"type": "object",
},
],
},
"propertyNames": {
"pattern": "^(?!\\$).*$",
},
"type": "object",
},
{
"additionalProperties": {
"not": {},
},
"propertyNames": {
"pattern": "^\\$",
},
"type": "object",
},
],
},
{
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
{
"additionalProperties": false,
"properties": {
"$all": {
"items": {
"$ref": "#/properties/filter/anyOf/1",
},
"type": "array",
},
},
"required": [
"$all",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$any": {
"items": {
"$ref": "#/properties/filter/anyOf/1",
},
"type": "array",
},
},
"required": [
"$any",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$not": {
"$ref": "#/properties/filter/anyOf/1",
},
},
"required": [
"$not",
],
"type": "object",
},
],
},
],
},
"type": {
"enum": [
"info",
"content",
],
"type": "string",
},
"_fields": {
"filter": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
"schema": [Function],
},
"disabled": false,
"factory": [Function],
@@ -32,6 +32,7 @@ import {
} from '@backstage/filter-predicates';
import { resolveEntityFilterData } from './resolveEntityFilterData';
import { Entity } from '@backstage/catalog-model';
import { z } from 'zod/v3';
/**
* @alpha
@@ -51,12 +52,11 @@ export const EntityCardBlueprint = createExtensionBlueprint({
filterExpression: entityFilterExpressionDataRef,
type: entityCardTypeDataRef,
},
config: {
schema: {
filter: z =>
z.union([z.string(), createZodV3FilterPredicateSchema(z)]).optional(),
type: z => z.enum(entityCardTypes).optional(),
},
configSchema: {
filter: z
.union([z.string(), createZodV3FilterPredicateSchema(z)])
.optional(),
type: z.enum(entityCardTypes).optional(),
},
*factory(
{
@@ -47,171 +47,35 @@ describe('EntityContentBlueprint', () => {
"input": "contents",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"filter": {
"anyOf": [
{
"type": "string",
},
{
"anyOf": [
{
"anyOf": [
{
"additionalProperties": {
"anyOf": [
{
"type": [
"string",
"number",
"boolean",
],
},
{
"additionalProperties": false,
"properties": {
"$exists": {
"type": "boolean",
},
},
"required": [
"$exists",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$in": {
"items": {
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
"type": "array",
},
},
"required": [
"$in",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$contains": {
"$ref": "#/properties/filter/anyOf/1",
},
},
"required": [
"$contains",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$hasPrefix": {
"type": "string",
},
},
"required": [
"$hasPrefix",
],
"type": "object",
},
],
},
"propertyNames": {
"pattern": "^(?!\\$).*$",
},
"type": "object",
},
{
"additionalProperties": {
"not": {},
},
"propertyNames": {
"pattern": "^\\$",
},
"type": "object",
},
],
},
{
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
{
"additionalProperties": false,
"properties": {
"$all": {
"items": {
"$ref": "#/properties/filter/anyOf/1",
},
"type": "array",
},
},
"required": [
"$all",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$any": {
"items": {
"$ref": "#/properties/filter/anyOf/1",
},
"type": "array",
},
},
"required": [
"$any",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$not": {
"$ref": "#/properties/filter/anyOf/1",
},
},
"required": [
"$not",
],
"type": "object",
},
],
},
],
},
"group": {
"anyOf": [
{
"const": false,
"type": "boolean",
},
{
"type": "string",
},
],
},
"icon": {
"type": "string",
},
"path": {
"type": "string",
},
"title": {
"type": "string",
},
"_fields": {
"filter": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"group": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"icon": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"path": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"title": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
"schema": [Function],
},
"disabled": false,
"factory": [Function],
@@ -35,6 +35,7 @@ import {
import { resolveEntityFilterData } from './resolveEntityFilterData';
import { Entity } from '@backstage/catalog-model';
import { ReactElement } from 'react';
import { z } from 'zod/v3';
/**
* @alpha
@@ -60,15 +61,14 @@ export const EntityContentBlueprint = createExtensionBlueprint({
group: entityContentGroupDataRef,
icon: entityContentIconDataRef,
},
config: {
schema: {
path: z => z.string().optional(),
title: z => z.string().optional(),
filter: z =>
z.union([z.string(), createZodV3FilterPredicateSchema(z)]).optional(),
group: z => z.literal(false).or(z.string()).optional(),
icon: z => z.string().optional(),
},
configSchema: {
path: z.string().optional(),
title: z.string().optional(),
filter: z
.union([z.string(), createZodV3FilterPredicateSchema(z)])
.optional(),
group: z.literal(false).or(z.string()).optional(),
icon: z.string().optional(),
},
*factory(
params: {
@@ -63,144 +63,15 @@ describe('EntityContextMenuItemBlueprint', () => {
"input": "contextMenuItems",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"filter": {
"anyOf": [
{
"anyOf": [
{
"additionalProperties": {
"anyOf": [
{
"type": [
"string",
"number",
"boolean",
],
},
{
"additionalProperties": false,
"properties": {
"$exists": {
"type": "boolean",
},
},
"required": [
"$exists",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$in": {
"items": {
"$ref": "#/properties/filter/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
"type": "array",
},
},
"required": [
"$in",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$contains": {
"$ref": "#/properties/filter",
},
},
"required": [
"$contains",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$hasPrefix": {
"type": "string",
},
},
"required": [
"$hasPrefix",
],
"type": "object",
},
],
},
"propertyNames": {
"pattern": "^(?!\\$).*$",
},
"type": "object",
},
{
"additionalProperties": {
"not": {},
},
"propertyNames": {
"pattern": "^\\$",
},
"type": "object",
},
],
},
{
"$ref": "#/properties/filter/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
},
{
"additionalProperties": false,
"properties": {
"$all": {
"items": {
"$ref": "#/properties/filter",
},
"type": "array",
},
},
"required": [
"$all",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$any": {
"items": {
"$ref": "#/properties/filter",
},
"type": "array",
},
},
"required": [
"$any",
],
"type": "object",
},
{
"additionalProperties": false,
"properties": {
"$not": {
"$ref": "#/properties/filter",
},
},
"required": [
"$not",
],
"type": "object",
},
],
},
"_fields": {
"filter": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
"schema": [Function],
},
"disabled": false,
"factory": [Function],