Merge pull request #33836 from backstage/rugvip/explore-standard-schema-decoupling

frontend-plugin-api: decouple zod dependency using Standard Schema
This commit is contained in:
Patrik Oldsberg
2026-04-14 12:27:53 +02:00
committed by GitHub
49 changed files with 2258 additions and 951 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-app': patch
---
Migrated `AppLanguageApi` extension to use the new `configSchema` option.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-graph': patch
---
Updated `README-alpha.md` extension examples to use current APIs.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
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`.
@@ -1,5 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-plugin-api': minor
---
Refactored the internal `createSchemaFromZod` helper to use a schema-first generic pattern, replacing the `ZodSchema<TOutput, ZodTypeDef, TInput>` constraint with `TSchema extends ZodType`. This avoids "excessively deep" type inference errors when multiple Zod copies are resolved.
**BREAKING**: Removed the deprecated `createSchemaFromZod` helper. Use the new `configSchema` option instead. See the [1.50 migration documentation](https://backstage.io/docs/frontend-system/architecture/migrations#150) for more information.
@@ -0,0 +1,5 @@
---
'@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 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.
+34 -107
View File
@@ -71,30 +71,25 @@ app:
### Customizations
Plugin developers can use the `createSearchResultItemExtension` factory provided by the `@backstage/plugin-search-react` for building their own custom `Search` result item extensions.
Plugin developers can use the `SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha` for building their own custom search result item extensions.
_Example creating a custom `TechDocsSearchResultItemExtension`_
```tsx
// plugins/techdocs/alpha.tsx
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
// plugins/techdocs/src/alpha.tsx
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
/** @alpha */
export const TechDocsSearchResultListItemExtension =
createSearchResultListItemExtension({
id: 'techdocs',
configSchema: createSchemaFromZod(z =>
z.object({
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
}),
),
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
'./components/TechDocsSearchResultListItem'
);
return props => <TechDocsSearchResultListItem {...props} {...config} />;
SearchResultListItemBlueprint.make({
name: 'techdocs',
params: {
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
'./components/TechDocsSearchResultListItem'
);
return props => <TechDocsSearchResultListItem {...props} {...config} />;
},
},
});
```
@@ -107,110 +102,42 @@ When a Backstage adopter doesn't want to use the custom `TechDocs` search result
# app-config.yaml
app:
extensions:
- plugin.search.result.item.techdocs: false # ✨
- search-result-list-item:techdocs: false
```
Because a configuration schema was provided to the extension factory, Backstage adopters will be able to customize `TechDocs` search results **line clamp** that defaults to 3 and also **disable automatic analytics events tracking**:
The `SearchResultListItemBlueprint` includes a built-in `noTrack` config option that can be used to **disable automatic analytics events tracking**:
```yaml
# app-config.yaml
app:
extensions:
- plugin.search.result.item.techdocs:
config: # ✨
- search-result-list-item:techdocs:
config:
noTrack: true
lineClamp: 3
```
[comment]: <> (TODO: Extract this explanation to a more central place in the future)
The `createSearchResultItemExtension` function returns a Backstage's extension representation as follows:
To complete the development cycle for creating a custom search result item extension, provide the extension via the `TechDocs` plugin. You can also take a look at the [actual implementation](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/alpha/index.tsx) of a custom `TechDocs` search result item:
```ts
{
"$$type": "@backstage/Extension", // [1]
"id": "plugin.search.result.item.techdocs", // [2]
"at": "plugin.search.page/items", // [3]
"inputs": {} // [4]
"output": { // [5]
"item": {
"$$type": "@backstage/ExtensionDataRef",
"id": "plugin.search.result.item.data",
"config": {}
}
},
"configSchema": { // [6]
"schema": {
"type": "object",
"properties": {
"noTrack": {
"type": "boolean",
"default": false
},
"lineClamp": {
"type": "number",
"default": 5
}
```tsx
// plugins/techdocs/src/alpha.tsx
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
const TechDocsSearchResultListItemExtension =
SearchResultListItemBlueprint.make({
name: 'techdocs',
params: {
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
'./components/TechDocsSearchResultListItem'
);
return props => <TechDocsSearchResultListItem {...props} {...config} />;
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
"disabled": false, // [7]
}
```
In this object, you can see exactly what will happen once the custom extension is installed:
- **[1] `$$type`**: declares that the object represents an extension;
- **[2] `id`**: Is a unique identification for the extension, the `plugin.search.result.item.techdocs` is the key used to configure the extension in the `app-config.yaml` file;
- **[3] `at`**: It represents the extension attachment point, so the value `plugin.search.page/items` says that the `TechDocs`'s search result item output will be injected as input on the "items" attachment expected by the search page extension;
- **[4] `inputs`**: in this case is an empty object because this extension doesn't expect inputs;
- **[5] `output`**: Object representing the artifact produced by the `TechDocs` result item extension, on the example, it is a react component reference;
- **[6] `configSchema`**: represents the `TechDocs` search result item configuration definition, this is the same schema that adopters will use for customizing the extension via `app-config.yaml` file;
- **[7] `disable`**: Says that the result item extension will be enable by default when the `TechDocs` plugin is installed in the app.
To complete the development cycle for creating a custom search result item extension, we should provide the extension via `TechDocs` plugin:
```tsx
// plugins/techdocs/alpha.tsx
import { createPlugin } from "@backstage/frontend-plugin-api";
// plugins should be always exported as default
export default createPlugin({
id: 'techdocs'
extensions: [TechDocsSearchResultItemExtension]
})
```
Here is the `plugins/techdocs/alpha/index.tsx` final version, and you can also take a look at the [actual implementation](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/alpha/index.tsx) of a custom `TechDocs` search result item:
```tsx
// plugins/techdocs/alpha.tsx
import { createPlugin } from '@backstage/frontend-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
/** @alpha */
export const TechDocsSearchResultListItemExtension =
createSearchResultListItemExtension({
id: 'techdocs',
configSchema: createSchemaFromZod(z =>
z.object({
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
}),
),
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
'./components/TechDocsSearchResultListItem'
);
return props => <TechDocsSearchResultListItem {...props} {...config} />;
},
});
/** @alpha */
export default createPlugin({
// plugins should be always exported as default
export default createFrontendPlugin({
id: 'techdocs',
extensions: [TechDocsSearchResultListItemExtension],
});
@@ -275,18 +275,18 @@ 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';
const navigationExtension = createExtension({
// ...
namespace: 'app',
name: 'nav',
// [3]: Extension `id` will be `app/nav` following the extension naming pattern
config: {
schema: {
title: z => z.string().default('Sidebar Title'),
},
configSchema: {
title: z.string().default('Sidebar Title'),
},
factory({ config }) {
return [
@@ -321,12 +321,12 @@ In all examples so far we have defined the extension factory as a regular functi
For example, this is how we could define an extension where its output depends on the configuration:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
// ...
config: {
schema: {
disableIcon: z.boolean().default(false),
},
configSchema: {
disableIcon: z.boolean().default(false),
},
output: [coreExtensionData.reactElement, iconDataRef.optional()],
*factory({ config }) {
@@ -31,12 +31,12 @@ Every extension blueprint also provides a `makeWithOverrides` method. It is usef
The following is an example of how one might use the blueprint `makeWithOverrides` method to create a new extension:
```tsx
import { z } from 'zod';
const myPageExtension = PageBlueprint.makeWithOverrides({
// This defines additional configuration options for the extension.
config: {
schema: {
layout: z => z.enum(['grid', 'rows']).default('grid'),
},
configSchema: {
layout: z.enum(['grid', 'rows']).default('grid'),
},
// This defines additional inputs for the extension.
inputs: {
@@ -118,10 +118,8 @@ export interface MyWidgetBlueprintParams {
export const MyWidgetBlueprint = createExtensionBlueprint({
kind: 'my-widget',
attachTo: { id: 'page:my-plugin', input: 'widgets' },
config: {
schema: {
title: z.string().optional(),
},
configSchema: {
title: z.string().optional(),
},
output: [coreExtensionData.reactElement],
factory(params: MyWidgetBlueprintParams, { config }) {
@@ -196,10 +194,8 @@ const widgetTitleRef = createExtensionDataRef<string>().with({
export const MyWidgetBlueprint = createExtensionBlueprint({
kind: 'my-widget',
attachTo: { id: 'page:my-plugin', input: 'widgets' },
config: {
schema: {
title: z.string().optional(),
},
configSchema: {
title: z.string().optional(),
},
output: [widgetTitleRef, coreExtensionData.reactElement],
factory(params: MyWidgetBlueprintParams, { config }) {
@@ -182,20 +182,18 @@ const myOverrideExtension = myExtension.override({
Overriding the configuration schema works very similarly to overriding the declared inputs. You can define new configuration fields that will be merged with the existing ones, but you can not re-declare existing fields. The following example shows how to override an extension and add a new configuration field:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
config: {
schema: {
foo: z => z.string(),
},
configSchema: {
foo: z.string(),
},
// ...
});
const overrideExtension = exampleExtension.override({
config: {
schema: {
bar: z => z.string(),
},
configSchema: {
bar: z.string(),
},
factory(originalFactory, { config }) {
//
@@ -210,12 +208,12 @@ const overrideExtension = exampleExtension.override({
In all examples so far we have called the `originalFactory` callback without any arguments. It is however possible to override parts of the factory context for the original factory using the first parameter of the original factory. This can be useful if you want to override the provided configuration or change the inputs in some way. Note that if you are implementing a `factory` for a blueprint, the override factory context will instead be the second parameter of the original factory function. The following is an example of how to override the configuration for the original factory:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
name: 'example',
config: {
schema: {
layout: z => z.enum(['grid', 'list']).optional(),
},
configSchema: {
layout: z.enum(['grid', 'list']).optional(),
},
output: [coreExtensionData.reactElement],
factory: ({ config }) => [
@@ -11,6 +11,86 @@ This section provides migration guides for different versions of the frontend sy
This guide is intended for app and plugin authors who have already migrated their code to the new frontend system, and are looking to keep it up to date with the latest changes. These guides do not cover trivial migrations that can be explained in a deprecation message, such as a renamed export.
## 1.50
### 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 with JSON Schema support, rather than requiring factory functions. The `createSchemaFromZod` helper has also been removed.
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:
```tsx
createExtension({
// ...
config: {
schema: {
title: z => z.string().default('Hello'),
count: z => z.number().optional(),
},
},
factory({ config }) {
// ...
},
});
```
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({
// ...
configSchema: {
title: z.string().default('Hello'),
count: z.number().optional(),
},
factory({ config }) {
// ...
},
});
```
The same applies to `createExtensionBlueprint`:
```tsx
import { z } from 'zod';
const MyBlueprint = createExtensionBlueprint({
// ...
configSchema: {
title: z.string().default('Hello'),
},
factory(params, { config }) {
// ...
},
});
```
And when adding config through `makeWithOverrides`:
```tsx
import { z } from 'zod';
MyBlueprint.makeWithOverrides({
configSchema: {
extra: z.string(),
},
factory(originalFactory, { config }) {
return originalFactory({
// ...
});
},
});
```
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
### `namespace` parameter should be removed
@@ -94,11 +94,11 @@ The extension ID of the work API will be the kind `api:` followed by the plugin
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's refactor the example above to also accept configuration, which will require us to use the [override method of the blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides).
```tsx title="in @internal/plugin-example"
import { z } from 'zod';
const exampleWorkApi = ApiBlueprint.makeWithOverrides({
config: {
schema: {
goSlow: z => z.boolean().default(false),
},
configSchema: {
goSlow: z.boolean().default(false),
},
factory(originalFactory, { config }) {
return originalFactory({
+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 { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod';
import { createDeprecatedConfigSchema } from '../../../frontend-plugin-api/src/schema/createPortableSchema';
import { TestApiRegistry, withLogCollector } from '@backstage/test-utils';
import { createErrorCollector } from '../wiring/createErrorCollector';
@@ -181,12 +181,10 @@ describe('instantiateAppNodeTree', () => {
test: testDataRef,
other: otherDataRef.optional(),
},
configSchema: createSchemaFromZod(z =>
z.object({
output: z.string().default('test'),
other: z.number().optional(),
}),
),
configSchema: createDeprecatedConfigSchema({
output: z => z.string().default('test'),
other: z => z.number().optional(),
}),
factory({ config }) {
return { test: config.output, other: config.other };
},
@@ -48,6 +48,7 @@
"@backstage/filter-predicates": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@standard-schema/spec": "^1.1.0",
"zod": "^3.25.76 || ^4.0.0",
"zod-to-json-schema": "^3.25.1"
},
@@ -14,6 +14,7 @@ import { FilterPredicate } from '@backstage/filter-predicates';
import { JsonObject } from '@backstage/types';
import { JSX as JSX_2 } from 'react';
import { ReactNode } from 'react';
import { StandardSchemaV1 } from '@standard-schema/spec';
import type { z } from 'zod/v3';
// @public
@@ -160,6 +161,91 @@ export interface ExtensionBlueprint<
inputs: T['inputs'];
params: T['params'];
}>;
makeWithOverrides<
TName extends string | undefined,
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
UParentInputs extends ExtensionDataRef,
TExtraInputs extends {
[inputName in string]: ExtensionInput;
} = {},
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(args: {
name?: TName;
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory(
originalFactory: <
TParamsInput extends AnyParamsInput<NonNullable<T['params']>>,
>(
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: T['params'],
context?: {
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
},
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>;
}): OverridableExtensionDefinition<{
config: Expand<
{
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
} & T['config']
>;
configInput: Expand<
{
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
} & T['configInput']
>;
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: Expand<T['inputs'] & TExtraInputs>;
kind: T['kind'];
name: string | undefined extends TName ? undefined : TName;
params: T['params'];
}>;
// @deprecated (undocumented)
makeWithOverrides<
TName extends string | undefined,
TExtensionConfigSchema extends {
@@ -187,6 +273,7 @@ export interface ExtensionBlueprint<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -212,7 +299,7 @@ export interface ExtensionBlueprint<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -230,7 +317,9 @@ export interface ExtensionBlueprint<
? {}
: {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
}) &
T['config']
@@ -241,7 +330,7 @@ export interface ExtensionBlueprint<
: z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>) &
@@ -471,6 +560,104 @@ export interface OverridableExtensionDefinition<
>;
};
// (undocumented)
override<
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
TExtraInputs extends {
[inputName in string]: ExtensionInput;
},
TParamsInput extends AnyParamsInput_2<NonNullable<T['params']>>,
UParentInputs extends ExtensionDataRef,
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(
args: Expand<
{
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory?(
originalFactory: <
TFactoryParamsReturn extends AnyParamsInput_2<
NonNullable<T['params']>
>,
>(
context?: Expand<
{
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
} & ([T['params']] extends [never]
? {}
: {
params?: TFactoryParamsReturn extends ExtensionBlueprintDefineParams
? TFactoryParamsReturn
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
>,
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput>;
} & ([T['params']] extends [never]
? {}
: {
params?: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>,
): OverridableExtensionDefinition<{
kind: T['kind'];
name: T['name'];
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
configInput: T['configInput'] & {
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
};
}>;
// @deprecated (undocumented)
override<
TExtensionConfigSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
@@ -499,6 +686,7 @@ export interface OverridableExtensionDefinition<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -531,7 +719,9 @@ export interface OverridableExtensionDefinition<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -560,14 +750,14 @@ export interface OverridableExtensionDefinition<
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
configInput: T['configInput'] &
z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>;
@@ -629,9 +819,14 @@ export type PluginWrapperDefinition<TValue = unknown | never> = {
};
// @public (undocumented)
export type PortableSchema<TOutput, TInput = TOutput> = {
export type PortableSchema<TOutput = unknown, TInput = TOutput> = {
parse: (input: TInput) => TOutput;
schema: JsonObject;
schema: {
(): {
schema: JsonObject;
};
[key: string]: any;
};
};
// @public
@@ -644,6 +839,8 @@ export interface RouteRef<
readonly T: TParams;
}
export { StandardSchemaV1 };
// @public
export interface SubRouteRef<
TParams extends AnyRouteRefParams = AnyRouteRefParams,
+397 -26
View File
@@ -22,6 +22,7 @@ import { JSX as JSX_3 } from 'react/jsx-runtime';
import { Observable } from '@backstage/types';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
import { StandardSchemaV1 } from '@standard-schema/spec';
import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api';
import type { z } from 'zod/v3';
@@ -458,6 +459,56 @@ export function createApiRef<T>(): {
};
// @public
export function createExtension<
UOutput extends ExtensionDataRef,
TInputs extends {
[inputName in string]: ExtensionInput;
},
UFactoryOutput extends ExtensionDataValue<any, any>,
const TKind extends string | undefined = undefined,
const TName extends string | undefined = undefined,
UParentInputs extends ExtensionDataRef = ExtensionDataRef,
TNewConfigSchema extends {
[key: string]: StandardSchemaV1;
} = {},
>(
options: CreateExtensionOptions<
TKind,
TName,
UOutput,
TInputs,
{},
UFactoryOutput,
UParentInputs,
TNewConfigSchema
> & {
config?: never;
},
): OverridableExtensionDefinition<{
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
configInput: {
[key in keyof TNewConfigSchema]?: StandardSchemaV1.InferInput<
TNewConfigSchema[key]
>;
};
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
infer IConfig
>
? ExtensionDataRef<IData, IId, IConfig>
: never;
inputs: TInputs;
params: never;
kind: string | undefined extends TKind ? undefined : TKind;
name: string | undefined extends TName ? undefined : TName;
}>;
// @public @deprecated (undocumented)
export function createExtension<
UOutput extends ExtensionDataRef,
TInputs extends {
@@ -478,19 +529,26 @@ export function createExtension<
TInputs,
TConfigSchema,
UFactoryOutput,
UParentInputs
>,
UParentInputs,
{}
> & {
configSchema?: never;
},
): OverridableExtensionDefinition<{
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
[key in keyof TConfigSchema]: ReturnType<
((...args: any[]) => any) & TConfigSchema[key]
>;
}>
>;
output: UOutput extends ExtensionDataRef<
@@ -507,6 +565,77 @@ export function createExtension<
}>;
// @public
export function createExtensionBlueprint<
TParams extends object | ExtensionBlueprintDefineParams,
UOutput extends ExtensionDataRef,
TInputs extends {
[inputName in string]: ExtensionInput;
},
UFactoryOutput extends ExtensionDataValue<any, any>,
TKind extends string,
UParentInputs extends ExtensionDataRef,
TDataRefs extends {
[name in string]: ExtensionDataRef;
} = never,
TNewConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(
options: {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
config?: never;
configSchema?: TNewConfigSchema;
defineParams?: TParams extends ExtensionBlueprintDefineParams
? TParams
: 'The defineParams option must be a function if provided, see the docs for details';
factory(
params: TParams extends ExtensionBlueprintDefineParams
? ReturnType<TParams>['T']
: TParams,
context: {
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
): Iterable<UFactoryOutput>;
dataRefs?: TDataRefs;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>,
): ExtensionBlueprint<{
kind: TKind;
params: TParams;
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
infer IConfig
>
? ExtensionDataRef<IData, IId, IConfig>
: never;
inputs: string extends keyof TInputs ? {} : TInputs;
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
configInput: {
[key in keyof TNewConfigSchema]?: StandardSchemaV1.InferInput<
TNewConfigSchema[key]
>;
};
dataRefs: TDataRefs;
}>;
// @public @deprecated (undocumented)
export function createExtensionBlueprint<
TParams extends object | ExtensionBlueprintDefineParams,
UOutput extends ExtensionDataRef,
@@ -523,16 +652,38 @@ export function createExtensionBlueprint<
[name in string]: ExtensionDataRef;
} = never,
>(
options: CreateExtensionBlueprintOptions<
TKind,
TParams,
UOutput,
TInputs,
TConfigSchema,
UFactoryOutput,
TDataRefs,
UParentInputs
>,
options: {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: never;
config?: {
schema: TConfigSchema;
};
defineParams?: TParams extends ExtensionBlueprintDefineParams
? TParams
: 'The defineParams option must be a function if provided, see the docs for details';
factory(
params: TParams extends ExtensionBlueprintDefineParams
? ReturnType<TParams>['T']
: TParams,
context: {
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
): Iterable<UFactoryOutput>;
dataRefs?: TDataRefs;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>,
): ExtensionBlueprint<{
kind: TKind;
params: TParams;
@@ -547,13 +698,17 @@ export function createExtensionBlueprint<
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
[key in keyof TConfigSchema]: ReturnType<
((...args: any[]) => any) & TConfigSchema[key]
>;
}>
>;
dataRefs: TDataRefs;
@@ -575,6 +730,9 @@ export type CreateExtensionBlueprintOptions<
[name in string]: ExtensionDataRef;
},
UParentInputs extends ExtensionDataRef,
TNewConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
> = {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
@@ -583,6 +741,7 @@ export type CreateExtensionBlueprintOptions<
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: TNewConfigSchema;
config?: {
schema: TConfigSchema;
};
@@ -597,7 +756,13 @@ export type CreateExtensionBlueprintOptions<
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
@@ -661,6 +826,9 @@ export type CreateExtensionOptions<
},
UFactoryOutput extends ExtensionDataValue<any, any>,
UParentInputs extends ExtensionDataRef,
TNewConfigSchema extends {
[key: string]: StandardSchemaV1;
} = {},
> = {
kind?: TKind;
name?: TName;
@@ -670,6 +838,7 @@ export type CreateExtensionOptions<
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: TNewConfigSchema;
config?: {
schema: TConfigSchema;
};
@@ -677,7 +846,13 @@ export type CreateExtensionOptions<
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Iterable<UFactoryOutput>;
@@ -1046,6 +1221,91 @@ export interface ExtensionBlueprint<
inputs: T['inputs'];
params: T['params'];
}>;
makeWithOverrides<
TName extends string | undefined,
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
UParentInputs extends ExtensionDataRef,
TExtraInputs extends {
[inputName in string]: ExtensionInput;
} = {},
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(args: {
name?: TName;
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory(
originalFactory: <
TParamsInput extends AnyParamsInput_2<NonNullable<T['params']>>,
>(
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: T['params'],
context?: {
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
},
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>;
}): OverridableExtensionDefinition<{
config: Expand<
{
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
} & T['config']
>;
configInput: Expand<
{
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
} & T['configInput']
>;
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: Expand<T['inputs'] & TExtraInputs>;
kind: T['kind'];
name: string | undefined extends TName ? undefined : TName;
params: T['params'];
}>;
// @deprecated (undocumented)
makeWithOverrides<
TName extends string | undefined,
TExtensionConfigSchema extends {
@@ -1073,6 +1333,7 @@ export interface ExtensionBlueprint<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -1098,7 +1359,7 @@ export interface ExtensionBlueprint<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -1116,7 +1377,9 @@ export interface ExtensionBlueprint<
? {}
: {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
}) &
T['config']
@@ -1127,7 +1390,7 @@ export interface ExtensionBlueprint<
: z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>) &
@@ -1687,6 +1950,104 @@ export interface OverridableExtensionDefinition<
>;
};
// (undocumented)
override<
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
TExtraInputs extends {
[inputName in string]: ExtensionInput;
},
TParamsInput extends AnyParamsInput<NonNullable<T['params']>>,
UParentInputs extends ExtensionDataRef,
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(
args: Expand<
{
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory?(
originalFactory: <
TFactoryParamsReturn extends AnyParamsInput<
NonNullable<T['params']>
>,
>(
context?: Expand<
{
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
} & ([T['params']] extends [never]
? {}
: {
params?: TFactoryParamsReturn extends ExtensionBlueprintDefineParams
? TFactoryParamsReturn
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
>,
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput>;
} & ([T['params']] extends [never]
? {}
: {
params?: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>,
): OverridableExtensionDefinition<{
kind: T['kind'];
name: T['name'];
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
configInput: T['configInput'] & {
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
};
}>;
// @deprecated (undocumented)
override<
TExtensionConfigSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
@@ -1715,6 +2076,7 @@ export interface OverridableExtensionDefinition<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -1747,7 +2109,9 @@ export interface OverridableExtensionDefinition<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -1776,14 +2140,14 @@ export interface OverridableExtensionDefinition<
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
configInput: T['configInput'] &
z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>;
@@ -2039,9 +2403,14 @@ export type PluginWrapperDefinition<TValue = unknown | never> = {
};
// @public (undocumented)
export type PortableSchema<TOutput, TInput = TOutput> = {
export type PortableSchema<TOutput = unknown, TInput = TOutput> = {
parse: (input: TInput) => TOutput;
schema: JsonObject;
schema: {
(): {
schema: JsonObject;
};
[key: string]: any;
};
};
// @public
@@ -2126,6 +2495,8 @@ export namespace SessionState {
export type SignedOut = typeof SessionState.SignedOut;
}
export { StandardSchemaV1 };
// @public
export interface StorageApi {
forBucket(name: string): StorageApi;
+1 -1
View File
@@ -47,7 +47,7 @@ export type {
AppNodeSpec,
AppTree,
} from './apis';
export type { PortableSchema } from './schema';
export type { PortableSchema, StandardSchemaV1 } from './schema';
export type {
AnyRouteRefParams,
RouteRef,
@@ -182,18 +182,14 @@ describe('ApiBlueprint', () => {
"input": "apis",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"test": {
"default": "test",
"type": "string",
},
"_fields": {
"test": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
},
"disabled": false,
"factory": [Function],
@@ -39,17 +39,14 @@ describe('NavItemBlueprint', () => {
"input": "items",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
},
"_fields": {
"title": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
},
"disabled": false,
"factory": [Function],
@@ -48,20 +48,19 @@ describe('PageBlueprint', () => {
"input": "routes",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
},
"title": {
"type": "string",
},
"_fields": {
"path": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"title": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
},
"disabled": false,
"factory": [Function],
@@ -0,0 +1,381 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* 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 v4 schemas', () => {
it('should report a missing required field', () => {
const schema = createConfigSchema({ name: zodV4.string() });
expect(() => schema.parse({})).toThrow(
"Invalid input: expected string, received undefined at 'name'",
);
});
it('should report a type mismatch', () => {
const schema = createConfigSchema({ count: zodV4.number() });
expect(() => schema.parse({ count: 'not a number' })).toThrow(
"Invalid input: expected number, received string at 'count'",
);
});
it('should report nested object errors with the full path', () => {
const schema = createConfigSchema({
settings: zodV4.object({ port: zodV4.number() }),
});
expect(() => schema.parse({ settings: { port: 'abc' } })).toThrow(
"Invalid input: expected number, received string at 'settings.port'",
);
});
it('should combine errors from multiple fields', () => {
const schema = createConfigSchema({
name: zodV4.string(),
count: zodV4.number(),
});
expect(() => schema.parse({})).toThrow(
"Invalid input: expected string, received undefined at 'name'; " +
"Invalid input: expected number, received undefined at 'count'",
);
});
it('should apply defaults for optional fields with defaults', () => {
const schema = createConfigSchema({
name: zodV4.string(),
mode: zodV4.enum(['fast', 'slow']).default('fast'),
});
expect(schema.parse({ name: 'test' })).toEqual({
name: 'test',
mode: 'fast',
});
});
it('should parse valid config', () => {
const schema = createConfigSchema({
name: zodV4.string(),
count: zodV4.number().optional(),
});
expect(schema.parse({ name: 'hello' })).toEqual({ name: 'hello' });
expect(schema.parse({ name: 'hello', count: 5 })).toEqual({
name: 'hello',
count: 5,
});
});
});
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.',
);
});
});
describe('schema creation errors', () => {
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");
});
it('should reject a Standard Schema without JSON Schema support', () => {
const fakeStandardSchema = {
'~standard': {
version: 1,
vendor: 'fake',
validate: () => ({ value: 'ok' }),
},
};
expect(() =>
createConfigSchema({ field: fakeStandardSchema as any }),
).toThrow(
"Config schema for field 'field' does not support JSON Schema conversion",
);
});
});
describe('JSON Schema generation', () => {
it('should generate JSON Schema lazily via schema()', () => {
const schema = createConfigSchema({
title: zodV4.string(),
count: zodV4.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 support backward-compatible property access on schema', () => {
const schema = createConfigSchema({
title: zodV4.string(),
});
expect(schema.schema.type).toBe('object');
expect(schema.schema.properties).toBeDefined();
});
});
describe('merging schemas', () => {
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({
name: 'hello',
count: 0,
});
});
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)!;
expect(merged.parse({ name: 'hello' })).toEqual({
name: 'hello',
count: 0,
});
});
it('should produce combined errors after merge', () => {
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
expect(() => merged.parse({})).toThrow(
"Missing required value at 'name'; " +
"Invalid input: expected number, received undefined at 'count'",
);
});
it('should produce combined errors for type mismatches after merge', () => {
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
expect(() => merged.parse({ name: 123, count: 'not a number' })).toThrow(
"Expected string, received number at 'name'; " +
"Invalid input: expected number, received string at 'count'",
);
});
it('should produce correct JSON Schema after merge', () => {
const a = createDeprecatedConfigSchema({ name: z => z.string() });
const b = createConfigSchema({ count: zodV4.number().optional() });
const merged = mergePortableSchemas(a, b)!;
const result = merged.schema();
expect(result.schema).toMatchObject({
type: 'object',
properties: {
name: { type: 'string' },
count: { type: 'number' },
},
required: ['name'],
additionalProperties: false,
});
});
it('should handle merge with undefined', () => {
const a = createConfigSchema({ name: zodV4.string() });
expect(mergePortableSchemas(a, undefined)).toBe(a);
expect(mergePortableSchemas(undefined, a)).toBe(a);
expect(mergePortableSchemas(undefined, undefined)).toBeUndefined();
});
it('should let later fields win when merging overlapping keys', () => {
const a = createDeprecatedConfigSchema({ x: z => z.string() });
const b = createConfigSchema({ x: zodV4.number() });
const merged = mergePortableSchemas(a, b)!;
expect(merged.parse({ x: 42 })).toEqual({ x: 42 });
expect(() => merged.parse({ x: 'hello' })).toThrow(
"Invalid input: expected number, received string at 'x'",
);
});
});
describe('edge cases', () => {
it('should handle an empty configSchema', () => {
const schema = createConfigSchema({});
expect(schema.parse({})).toEqual({});
expect(schema.parse(undefined)).toEqual({});
const result = schema.schema();
expect(result.schema).toEqual({
type: 'object',
properties: {},
additionalProperties: false,
});
});
it('should throw a clear error for non-object input', () => {
const schema = createConfigSchema({ title: zodV4.string() });
expect(() => schema.parse('not an object')).toThrow(
'Invalid config input, expected object but got string',
);
expect(() => schema.parse(42)).toThrow(
'Invalid config input, expected object but got number',
);
expect(() => schema.parse([1, 2])).toThrow(
'Invalid config input, expected object but got array',
);
expect(() => schema.parse(true)).toThrow(
'Invalid config input, expected object but got boolean',
);
});
it('should not produce undefined keys for absent optional fields', () => {
const schema = createConfigSchema({
name: zodV4.string(),
title: zodV4.string().optional(),
count: zodV4.number().default(42),
});
const result = schema.parse({ name: 'hello' });
expect(result).toEqual({ name: 'hello', count: 42 });
expect(Object.keys(result as object)).toEqual(['name', 'count']);
});
it('should not mark defaulted fields as required in JSON Schema', () => {
const schema = createConfigSchema({
name: zodV4.string(),
title: zodV4.string().default('hello'),
count: zodV4.number().optional(),
});
const result = schema.schema();
expect(result.schema).toMatchObject({
type: 'object',
required: ['name'],
});
});
});
});
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'],
});
});
});
@@ -0,0 +1,364 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { z as zodV3, type ZodType } from 'zod/v3';
import zodToJsonSchema from 'zod-to-json-schema';
import { PortableSchema } from './types';
/**
* The Standard Schema interface.
* @public
*/
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
*/
interface ResolvedField {
validate(value: unknown): { value: unknown } | { errors: string[] };
toJsonSchema(): JsonObject;
required: boolean;
}
/**
* Internal representation that carries per-field resolvers alongside the
* public PortableSchema surface, enabling schema merging.
* @internal
*/
export interface MergeablePortableSchema<TOutput = any, TInput = any>
extends PortableSchema<TOutput, TInput> {
/** @internal */
readonly _fields: Record<string, ResolvedField>;
}
/**
* Resolves each field, eagerly validates JSON Schema support, and returns
* a PortableSchema whose JSON Schema conversion is lazy.
* @internal
*/
export function createConfigSchema(
fields: Record<string, StandardSchemaV1>,
): MergeablePortableSchema {
const resolved: Record<string, ResolvedField> = {};
for (const [key, field] of Object.entries(fields)) {
resolved[key] = resolveField(key, field);
}
return buildPortableSchema(resolved);
}
/**
* Combines schemas from different sources for blueprint + override
* composition. Each source may use a completely different schema library.
* Because we track per-field resolvers, merging is just combining the
* field maps.
* @internal
*/
export function mergePortableSchemas<A, B>(
a: MergeablePortableSchema<A> | undefined,
b: MergeablePortableSchema<B> | undefined,
): MergeablePortableSchema<A & B> | undefined {
if (!a && !b) {
return undefined;
}
if (!a) {
return b as MergeablePortableSchema<A & B>;
}
if (!b) {
return a as MergeablePortableSchema<A & B>;
}
return buildPortableSchema<A & B>({
...a._fields,
...b._fields,
});
}
/**
* Assembles resolved fields into a PortableSchema with per-field
* validation (eager) and lazy JSON Schema generation.
*/
function buildPortableSchema<TOutput = unknown>(
fields: Record<string, ResolvedField>,
): MergeablePortableSchema<TOutput> {
function parse(input: unknown) {
if (
input !== undefined &&
input !== null &&
(typeof input !== 'object' || Array.isArray(input))
) {
throw new Error(
`Invalid config input, expected object but got ${
Array.isArray(input) ? 'array' : typeof input
}`,
);
}
const inputObj = (input ?? {}) as Record<string, unknown>;
const result: Record<string, unknown> = {};
const errors: string[] = [];
for (const [key, field] of Object.entries(fields)) {
const validated = field.validate(inputObj[key]);
if ('errors' in validated) {
errors.push(...validated.errors);
} else if (validated.value !== undefined || key in inputObj) {
result[key] = validated.value;
}
}
if (errors.length > 0) {
throw new Error(errors.join('; '));
}
return result as TOutput;
}
const result: MergeablePortableSchema<TOutput> = {
parse,
schema: undefined as any,
_fields: fields,
};
// Lazy getter — computes JSON Schema on first access, then caches it.
let cached: PortableSchema['schema'] | undefined;
Object.defineProperty(result, 'schema', {
get() {
if (!cached) {
const jsonSchema = buildObjectJsonSchema(fields);
const callable = Object.assign(
() => ({ schema: jsonSchema }),
jsonSchema,
);
cached = callable as PortableSchema['schema'];
}
return cached;
},
configurable: true,
enumerable: false,
});
return result;
}
/**
* Wraps a single schema into a ResolvedField. Eagerly validates that
* JSON Schema conversion will be possible, but defers the actual
* conversion until toJsonSchema() is called.
*/
function resolveField(key: string, schema: unknown): ResolvedField {
if (isZodV3Type(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+).`,
);
}
return resolveStandardField(key, schema);
}
throw new Error(
`Config schema for field '${key}' is not a valid Standard Schema`,
);
}
function resolveZodField(key: string, schema: ZodType): ResolvedField {
const wrapper = zodV3.object({ [key]: schema });
return {
validate(value) {
const result = wrapper.safeParse({ [key]: value });
if (result.success) {
return { value: result.data[key] };
}
return { errors: result.error.issues.map(formatZodIssue) };
},
toJsonSchema() {
const wholeJsonSchema = zodToJsonSchema(wrapper) as Record<string, any>;
return (wholeJsonSchema.properties?.[key] ?? {}) as JsonObject;
},
required: !schema.isOptional(),
};
}
function resolveStandardField(
key: string,
schema: StandardSchemaV1 & {
'~standard': { jsonSchema: { input: Function } };
},
): ResolvedField {
const required = isFieldRequired(schema);
return {
validate(value) {
const result = schema['~standard'].validate(value);
if (result instanceof Promise) {
throw new Error(
`Config schema for '${key}' returned a Promise — async schemas are not supported`,
);
}
if (result.issues) {
return {
errors: Array.from(result.issues).map(issue =>
formatStandardIssue(key, issue),
),
};
}
return { value: result.value };
},
toJsonSchema() {
const raw = schema['~standard'].jsonSchema.input({ target: 'draft-07' });
const { $schema: _, ...rest } = raw;
return rest as JsonObject;
},
required,
};
}
/** Assembles per-field JSON Schemas into a single object-level JSON Schema. */
function buildObjectJsonSchema(
fields: Record<string, ResolvedField>,
): JsonObject {
const properties: Record<string, JsonObject> = {};
const required: string[] = [];
for (const [key, field] of Object.entries(fields)) {
properties[key] = field.toJsonSchema();
if (field.required) {
required.push(key);
}
}
const schema: Record<string, unknown> = {
type: 'object',
properties,
additionalProperties: false,
};
if (required.length > 0) {
schema.required = required;
}
return schema as JsonObject;
}
function isZodV3Type(value: unknown): value is ZodType {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any)._parse === 'function' &&
'_def' in value
);
}
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
return (
typeof value === 'object' &&
value !== null &&
'~standard' in value &&
typeof (value as any)['~standard']?.validate === 'function'
);
}
function hasJsonSchemaConverter(
schema: StandardSchemaV1,
): schema is StandardSchemaV1 & {
'~standard': { jsonSchema: { input: Function } };
} {
const std = schema['~standard'] as any;
return typeof std?.jsonSchema?.input === 'function';
}
function isFieldRequired(schema: StandardSchemaV1): boolean {
const result = schema['~standard'].validate(undefined);
if (result instanceof Promise) {
return true;
}
return (result.issues?.length ?? 0) > 0;
}
function formatZodIssue(issue: {
code: string;
message: string;
path: Array<string | number>;
unionErrors?: Array<{ issues: Array<any> }>;
}): string {
if (issue.code === 'invalid_union' && issue.unionErrors?.[0]?.issues?.[0]) {
return formatZodIssue(issue.unionErrors[0].issues[0]);
}
let message = issue.message;
if (message === 'Required') {
message = 'Missing required value';
}
if (issue.path.length) {
message += ` at '${issue.path.join('.')}'`;
}
return message;
}
function formatStandardIssue(
fieldKey: string,
issue: StandardSchemaV1.Issue,
): string {
let message = issue.message;
if (message === 'Required') {
message = 'Missing required value';
}
const path = issue.path?.length
? `${fieldKey}.${issue.path
.map((p: PropertyKey | StandardSchemaV1.PathSegment) =>
typeof p === 'object' ? p.key : p,
)
.join('.')}`
: fieldKey;
return `${message} at '${path}'`;
}
/** @internal */
export function warnConfigSchemaPropDeprecation(callSite: string) {
// eslint-disable-next-line no-console
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 v4 ` +
`(or \`import { z } from 'zod/v4'\` from the zod v3 package). ` +
`Declared at ${callSite}`,
);
}
@@ -1,43 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSchemaFromZod } from './createSchemaFromZod';
describe('createSchemaFromZod', () => {
it('should provide nice parse errors', () => {
const { parse } = createSchemaFromZod(z =>
z.object({
foo: z.union([z.string(), z.number()]),
derp: z.object({ bar: z.number() }),
}),
);
expect(() => {
// @ts-expect-error
return parse({ derp: { bar: 'derp' } });
}).toThrow(
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
);
expect(() => {
// @ts-expect-error
return parse(undefined);
}).toThrow(`Missing required value`);
expect(() => {
// @ts-expect-error
return parse('derp');
}).toThrow(`Expected object, received string`);
});
});
@@ -1,56 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { z, type ZodIssue, type ZodType } from 'zod/v3';
import zodToJsonSchema from 'zod-to-json-schema';
import { PortableSchema } from './types';
/**
* @internal
*/
export function createSchemaFromZod<TSchema extends ZodType>(
schemaCreator: (zImpl: typeof z) => TSchema,
): PortableSchema<z.output<TSchema>, z.input<TSchema>> {
const schema = schemaCreator(z);
return {
// TODO: Types allow z.array etc here but it will break stuff
parse: input => {
const result = schema.safeParse(input);
if (result.success) {
return result.data;
}
throw new Error(result.error.issues.map(formatIssue).join('; '));
},
// TODO: Verify why we are not compatible with the latest zodToJsonSchema.
schema: zodToJsonSchema(schema) as JsonObject,
};
}
function formatIssue(issue: ZodIssue): string {
if (issue.code === 'invalid_union') {
return formatIssue(issue.unionErrors[0].issues[0]);
}
let message = issue.message;
if (message === 'Required') {
message = `Missing required value`;
}
if (issue.path.length) {
message += ` at '${issue.path.join('.')}'`;
}
return message;
}
@@ -15,3 +15,4 @@
*/
export { type PortableSchema } from './types';
export { type StandardSchemaV1 } from './createPortableSchema';
@@ -17,7 +17,16 @@
import { JsonObject } from '@backstage/types';
/** @public */
export type PortableSchema<TOutput, TInput = TOutput> = {
export type PortableSchema<TOutput = unknown, TInput = TOutput> = {
parse: (input: TInput) => TOutput;
schema: JsonObject;
/**
* The JSON Schema for this portable schema.
*
* @remarks
* Can be accessed as a property for backward compatibility (returns the
* JSON Schema object directly), or called as a method which returns
* `{ schema: JsonObject }`. Both forms compute the schema lazily on
* first access. The property form is deprecated prefer `schema()`.
*/
schema: { (): { schema: JsonObject }; [key: string]: any };
};
@@ -27,7 +27,14 @@ import {
import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef';
import { ExtensionInput } from './createExtensionInput';
import type { z } from 'zod/v3';
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
import {
createConfigSchema,
createDeprecatedConfigSchema,
mergePortableSchemas,
warnConfigSchemaPropDeprecation,
} from '../schema/createPortableSchema';
import { describeParentCallSite } from '../routing/describeParentCallSite';
import { type StandardSchemaV1 } from '@standard-schema/spec';
import { OpaqueExtensionDefinition } from '@internal/frontend';
import { ExtensionDataContainer } from './types';
import {
@@ -169,6 +176,7 @@ export type CreateExtensionOptions<
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
UFactoryOutput extends ExtensionDataValue<any, any>,
UParentInputs extends ExtensionDataRef,
TNewConfigSchema extends { [key: string]: StandardSchemaV1 } = {},
> = {
kind?: TKind;
name?: TName;
@@ -178,14 +186,27 @@ export type CreateExtensionOptions<
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: TNewConfigSchema;
/**
* @deprecated Use {@link CreateExtensionOptions.configSchema} instead.
*/
config?: {
/**
* @deprecated Use {@link CreateExtensionOptions.configSchema} instead.
*/
schema: TConfigSchema;
};
factory(context: {
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
}): Iterable<UFactoryOutput>;
@@ -237,6 +258,105 @@ export interface OverridableExtensionDefinition<
>;
};
override<
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
TExtraInputs extends { [inputName in string]: ExtensionInput },
TParamsInput extends AnyParamsInput<NonNullable<T['params']>>,
UParentInputs extends ExtensionDataRef,
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(
args: Expand<
{
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory?(
originalFactory: <
TFactoryParamsReturn extends AnyParamsInput<
NonNullable<T['params']>
>,
>(
context?: Expand<
{
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
} & ([T['params']] extends [never]
? {}
: {
params?: TFactoryParamsReturn extends ExtensionBlueprintDefineParams
? TFactoryParamsReturn
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
>,
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput>;
} & ([T['params']] extends [never]
? {}
: {
params?: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: Partial<T['params']>;
})
> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>,
): OverridableExtensionDefinition<{
kind: T['kind'];
name: T['name'];
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
configInput: T['configInput'] & {
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
};
}>;
/**
* @deprecated Use the `configSchema` option instead of `config.schema`.
*/
override<
TExtensionConfigSchema extends {
[key in string]: (zImpl: typeof z) => z.ZodType;
@@ -263,6 +383,7 @@ export interface OverridableExtensionDefinition<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -295,7 +416,9 @@ export interface OverridableExtensionDefinition<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -324,14 +447,14 @@ export interface OverridableExtensionDefinition<
inputs: T['inputs'] & TExtraInputs;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
configInput: T['configInput'] &
z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>;
@@ -397,6 +520,53 @@ function bindInputs(
*
* @public
*/
export function createExtension<
UOutput extends ExtensionDataRef,
TInputs extends { [inputName in string]: ExtensionInput },
UFactoryOutput extends ExtensionDataValue<any, any>,
const TKind extends string | undefined = undefined,
const TName extends string | undefined = undefined,
UParentInputs extends ExtensionDataRef = ExtensionDataRef,
TNewConfigSchema extends { [key: string]: StandardSchemaV1 } = {},
>(
options: CreateExtensionOptions<
TKind,
TName,
UOutput,
TInputs,
{},
UFactoryOutput,
UParentInputs,
TNewConfigSchema
> & { config?: never },
): OverridableExtensionDefinition<{
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
configInput: {
[key in keyof TNewConfigSchema]?: StandardSchemaV1.InferInput<
TNewConfigSchema[key]
>;
};
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
infer IConfig
>
? ExtensionDataRef<IData, IId, IConfig>
: never;
inputs: TInputs;
params: never;
kind: string | undefined extends TKind ? undefined : TKind;
name: string | undefined extends TName ? undefined : TName;
}>;
/**
* @deprecated Use the top-level `configSchema` option instead of `config.schema`.
* @public
*/
export function createExtension<
UOutput extends ExtensionDataRef,
TInputs extends { [inputName in string]: ExtensionInput },
@@ -413,22 +583,26 @@ export function createExtension<
TInputs,
TConfigSchema,
UFactoryOutput,
UParentInputs
>,
UParentInputs,
{}
> & { configSchema?: never },
): OverridableExtensionDefinition<{
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
[key in keyof TConfigSchema]: ReturnType<
((...args: any[]) => any) & TConfigSchema[key]
>;
}>
>;
// This inference and remapping back to ExtensionDataRef eliminates any occurrences ConfigurationExtensionDataRef
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
@@ -440,39 +614,24 @@ export function createExtension<
params: never;
kind: string | undefined extends TKind ? undefined : TKind;
name: string | undefined extends TName ? undefined : TName;
}> {
const schemaDeclaration = options.config?.schema;
const configSchema =
schemaDeclaration &&
createSchemaFromZod(innerZ =>
innerZ.object(
Object.fromEntries(
Object.entries(schemaDeclaration).map(([k, v]) => [k, v(innerZ)]),
),
),
);
}>;
/** @internal */
export function createExtension(
options: any,
): OverridableExtensionDefinition<any> {
if (options.config?.schema) {
warnConfigSchemaPropDeprecation(describeParentCallSite());
}
const resolvedConfigSchema = mergePortableSchemas(
options.config?.schema
? createDeprecatedConfigSchema(options.config.schema)
: undefined,
options.configSchema ? createConfigSchema(options.configSchema) : undefined,
);
return OpaqueExtensionDefinition.createInstance('v2', {
T: undefined as unknown as {
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>;
output: UOutput;
inputs: TInputs;
kind: string | undefined extends TKind ? undefined : TKind;
name: string | undefined extends TName ? undefined : TName;
},
T: undefined as any,
kind: options.kind,
name: options.name,
attachTo: options.attachTo,
@@ -480,7 +639,7 @@ export function createExtension<
if: options.if,
inputs: bindInputs(options.inputs, options.kind, options.name),
output: options.output,
configSchema,
configSchema: resolvedConfigSchema,
factory: options.factory,
toString() {
const parts: string[] = [];
@@ -527,13 +686,17 @@ export function createExtension<
parts.push(`attachTo=${attachTo}`);
return `ExtensionDefinition{${parts.join(',')}}`;
},
override(overrideOptions) {
override(overrideOptions: any) {
if (!Array.isArray(options.output)) {
throw new Error(
'Cannot override an extension that is not declared using the new format with outputs as an array',
);
}
if (overrideOptions.config?.schema) {
warnConfigSchemaPropDeprecation(describeParentCallSite());
}
// TODO(Rugvip): Making this a type check would be optimal, but it seems
// like it's tricky to add that and still have the type
// inference work correctly for the factory output.
@@ -572,7 +735,7 @@ export function createExtension<
output: (overrideOptions.output ??
options.output) as ExtensionDataRef[],
config:
options.config || overrideOptions.config
options.config?.schema || overrideOptions.config?.schema
? {
schema: {
...options.config?.schema,
@@ -580,6 +743,13 @@ export function createExtension<
},
}
: undefined,
configSchema:
options.configSchema || overrideOptions.configSchema
? {
...options.configSchema,
...overrideOptions.configSchema,
}
: undefined,
factory: ({ node, apis, config, inputs }) => {
if (!overrideOptions.factory) {
return options.factory({
@@ -591,8 +761,8 @@ export function createExtension<
});
}
const parentResult = overrideOptions.factory(
(innerContext): ExtensionDataContainer<UOutput> => {
return createExtensionDataContainer<UOutput>(
(innerContext: any): ExtensionDataContainer<any> => {
return createExtensionDataContainer<any>(
options.factory({
node,
apis,
@@ -31,6 +31,7 @@ import {
import { createExtensionInput } from './createExtensionInput';
import { RouteRef } from '../routing';
import { createExtension, ExtensionDefinition } from './createExtension';
import { z as zodV4 } from 'zod/v4';
import {
createExtensionDataContainer,
OpaqueExtensionDefinition,
@@ -248,6 +249,7 @@ describe('createExtensionBlueprint', () => {
},
});
// @ts-expect-error: overlapping config key 'text'
TestExtensionBlueprint.makeWithOverrides({
name: 'my-extension',
params: {
@@ -255,9 +257,8 @@ describe('createExtensionBlueprint', () => {
},
config: {
schema: {
// @ts-expect-error
text: z => z.number(),
something: z => z.string(),
text: (z: any) => z.number(),
something: (z: any) => z.string(),
},
},
});
@@ -309,6 +310,73 @@ describe('createExtensionBlueprint', () => {
);
});
it('should merge configSchema from blueprint with deprecated config.schema from override', () => {
const TestBlueprint = createExtensionBlueprint({
kind: 'test-extension',
attachTo: { id: 'test', input: 'default' },
output: [coreExtensionData.reactElement],
configSchema: {
title: zodV4.string().default('default title'),
},
factory(_, { config }) {
return [
coreExtensionData.reactElement(<div>{String(config.title)}</div>),
];
},
});
const extension = TestBlueprint.makeWithOverrides({
name: 'my-extension',
config: {
schema: {
extra: z => z.string(),
},
},
factory(origFactory, { config }) {
const c = config as { title: string; extra: string };
expect(c.title).toBe('default title');
expect(c.extra).toBe('extra value');
return origFactory({});
},
});
expect.assertions(2);
renderInTestApp(
createExtensionTester(extension, {
config: { extra: 'extra value' },
}).reactElement(),
);
});
it('should emit a deprecation warning when using config.schema', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
try {
createExtension({
name: 'test-deprecated-warning',
attachTo: { id: 'test', input: 'default' },
output: [coreExtensionData.reactElement],
config: {
schema: {
title: z => z.string().default('hello'),
},
},
factory() {
return [coreExtensionData.reactElement(<div />)];
},
});
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
'DEPRECATION WARNING: The `config.schema` option for extension config is deprecated',
),
);
} finally {
warnSpy.mockRestore();
}
});
it('should allow getting inputs properly', () => {
createExtensionBlueprint({
kind: 'test-extension',
@@ -28,6 +28,7 @@ import {
} from './createExtension';
import type { z } from 'zod/v3';
import { ExtensionInput } from './createExtensionInput';
import { type StandardSchemaV1 } from '@standard-schema/spec';
import { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef';
import { createExtensionDataContainer } from '@internal/frontend';
import {
@@ -37,6 +38,8 @@ import {
import { ExtensionDataContainer } from './types';
import { PageBlueprint } from '../blueprints/PageBlueprint';
import { FilterPredicate } from '@backstage/filter-predicates';
import { warnConfigSchemaPropDeprecation } from '../schema/createPortableSchema';
import { describeParentCallSite } from '../routing/describeParentCallSite';
/**
* A function used to define a parameter mapping function in order to facilitate
@@ -110,6 +113,7 @@ export type CreateExtensionBlueprintOptions<
UFactoryOutput extends ExtensionDataValue<any, any>,
TDataRefs extends { [name in string]: ExtensionDataRef },
UParentInputs extends ExtensionDataRef,
TNewConfigSchema extends { [key in string]: StandardSchemaV1 } = {},
> = {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
@@ -118,7 +122,14 @@ export type CreateExtensionBlueprintOptions<
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: TNewConfigSchema;
/**
* @deprecated Use {@link CreateExtensionBlueprintOptions.configSchema} instead.
*/
config?: {
/**
* @deprecated Use {@link CreateExtensionBlueprintOptions.configSchema} instead.
*/
schema: TConfigSchema;
};
/**
@@ -170,7 +181,13 @@ export type CreateExtensionBlueprintOptions<
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
} & {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
@@ -245,6 +262,92 @@ export interface ExtensionBlueprint<
* You must either pass `params` directly, or define a `factory` that can
* optionally call the original factory with the same params.
*/
makeWithOverrides<
TName extends string | undefined,
UFactoryOutput extends ExtensionDataValue<any, any>,
UNewOutput extends ExtensionDataRef,
UParentInputs extends ExtensionDataRef,
TExtraInputs extends { [inputName in string]: ExtensionInput } = {},
TNewExtensionConfigSchema extends {
[key in string]: StandardSchemaV1;
} = {},
>(args: {
name?: TName;
attachTo?: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UParentInputs
>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TExtraInputs & {
[KName in keyof T['inputs']]?: `Error: Input '${KName &
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
config?: never;
configSchema?: TNewExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
string}' is already defined in parent schema`;
};
factory(
originalFactory: <
TParamsInput extends AnyParamsInput<NonNullable<T['params']>>,
>(
params: TParamsInput extends ExtensionBlueprintDefineParams
? TParamsInput
: T['params'] extends ExtensionBlueprintDefineParams
? 'Error: This blueprint uses advanced parameter types and requires you to pass parameters as using the following callback syntax: `originalFactory(defineParams => defineParams(<params>))`'
: T['params'],
context?: {
config?: T['config'];
inputs?: ResolvedInputValueOverrides<NonNullable<T['inputs']>>;
},
) => ExtensionDataContainer<NonNullable<T['output']>>,
context: {
node: AppNode;
apis: ApiHolder;
config: T['config'] & {
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
},
): Iterable<UFactoryOutput> &
VerifyExtensionFactoryOutput<
ExtensionDataRef extends UNewOutput
? NonNullable<T['output']>
: UNewOutput,
UFactoryOutput
>;
}): OverridableExtensionDefinition<{
config: Expand<
{
[key in keyof TNewExtensionConfigSchema]: StandardSchemaV1.InferOutput<
TNewExtensionConfigSchema[key]
>;
} & T['config']
>;
configInput: Expand<
{
[key in keyof TNewExtensionConfigSchema]?: StandardSchemaV1.InferInput<
TNewExtensionConfigSchema[key]
>;
} & T['configInput']
>;
output: ExtensionDataRef extends UNewOutput ? T['output'] : UNewOutput;
inputs: Expand<T['inputs'] & TExtraInputs>;
kind: T['kind'];
name: string | undefined extends TName ? undefined : TName;
params: T['params'];
}>;
/**
* @deprecated Use the `configSchema` option instead of `config.schema`.
*/
makeWithOverrides<
TName extends string | undefined,
TExtensionConfigSchema extends {
@@ -270,6 +373,7 @@ export interface ExtensionBlueprint<
string}' is already defined in parent definition`;
};
output?: Array<UNewOutput>;
configSchema?: never;
config?: {
schema: TExtensionConfigSchema & {
[KName in keyof T['config']]?: `Error: Config key '${KName &
@@ -295,7 +399,7 @@ export interface ExtensionBlueprint<
apis: ApiHolder;
config: T['config'] & {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<((...args: any[]) => any) & TExtensionConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<T['inputs'] & TExtraInputs>>;
@@ -313,7 +417,9 @@ export interface ExtensionBlueprint<
? {}
: {
[key in keyof TExtensionConfigSchema]: z.infer<
ReturnType<TExtensionConfigSchema[key]>
ReturnType<
((...args: any[]) => any) & TExtensionConfigSchema[key]
>
>;
}) &
T['config']
@@ -324,7 +430,7 @@ export interface ExtensionBlueprint<
: z.input<
z.ZodObject<{
[key in keyof TExtensionConfigSchema]: ReturnType<
TExtensionConfigSchema[key]
((...args: any[]) => any) & TExtensionConfigSchema[key]
>;
}>
>) &
@@ -419,7 +525,7 @@ function unwrapParams<TParams extends object>(
* in the frontend system documentation.
*
* Extension blueprints make it much easier for users to create new extensions
* for your plugin. Rather than letting them use {@link createExtension}
* for your plugin. Rather than letting them use `createExtension`
* directly, you can define a set of parameters and default factory for your
* blueprint, removing a lot of the boilerplate and complexity that is otherwise
* needed to create an extension.
@@ -457,6 +563,74 @@ function unwrapParams<TParams extends object>(
* ```
* @public
*/
export function createExtensionBlueprint<
TParams extends object | ExtensionBlueprintDefineParams,
UOutput extends ExtensionDataRef,
TInputs extends { [inputName in string]: ExtensionInput },
UFactoryOutput extends ExtensionDataValue<any, any>,
TKind extends string,
UParentInputs extends ExtensionDataRef,
TDataRefs extends { [name in string]: ExtensionDataRef } = never,
TNewConfigSchema extends { [key in string]: StandardSchemaV1 } = {},
>(
options: {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
config?: never;
configSchema?: TNewConfigSchema;
defineParams?: TParams extends ExtensionBlueprintDefineParams
? TParams
: 'The defineParams option must be a function if provided, see the docs for details';
factory(
params: TParams extends ExtensionBlueprintDefineParams
? ReturnType<TParams>['T']
: TParams,
context: {
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
): Iterable<UFactoryOutput>;
dataRefs?: TDataRefs;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>,
): ExtensionBlueprint<{
kind: TKind;
params: TParams;
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
infer IConfig
>
? ExtensionDataRef<IData, IId, IConfig>
: never;
inputs: string extends keyof TInputs ? {} : TInputs;
config: {
[key in keyof TNewConfigSchema]: StandardSchemaV1.InferOutput<
TNewConfigSchema[key]
>;
};
configInput: {
[key in keyof TNewConfigSchema]?: StandardSchemaV1.InferInput<
TNewConfigSchema[key]
>;
};
dataRefs: TDataRefs;
}>;
/**
* @deprecated Use the top-level `configSchema` option instead of `config.schema`.
* @public
*/
export function createExtensionBlueprint<
TParams extends object | ExtensionBlueprintDefineParams,
UOutput extends ExtensionDataRef,
@@ -467,20 +641,41 @@ export function createExtensionBlueprint<
UParentInputs extends ExtensionDataRef,
TDataRefs extends { [name in string]: ExtensionDataRef } = never,
>(
options: CreateExtensionBlueprintOptions<
TKind,
TParams,
UOutput,
TInputs,
TConfigSchema,
UFactoryOutput,
TDataRefs,
UParentInputs
>,
options: {
kind: TKind;
attachTo: ExtensionDefinitionAttachTo<UParentInputs> &
VerifyExtensionAttachTo<UOutput, UParentInputs>;
disabled?: boolean;
if?: FilterPredicate;
inputs?: TInputs;
output: Array<UOutput>;
configSchema?: never;
config?: {
schema: TConfigSchema;
};
defineParams?: TParams extends ExtensionBlueprintDefineParams
? TParams
: 'The defineParams option must be a function if provided, see the docs for details';
factory(
params: TParams extends ExtensionBlueprintDefineParams
? ReturnType<TParams>['T']
: TParams,
context: {
node: AppNode;
apis: ApiHolder;
config: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
},
): Iterable<UFactoryOutput>;
dataRefs?: TDataRefs;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>,
): ExtensionBlueprint<{
kind: TKind;
params: TParams;
// This inference and remapping back to ExtensionDataRef eliminates any occurrences ConfigurationExtensionDataRef
output: UOutput extends ExtensionDataRef<
infer IData,
infer IId,
@@ -491,16 +686,25 @@ export function createExtensionBlueprint<
inputs: string extends keyof TInputs ? {} : TInputs;
config: string extends keyof TConfigSchema
? {}
: { [key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>> };
: {
[key in keyof TConfigSchema]: z.infer<
ReturnType<((...args: any[]) => any) & TConfigSchema[key]>
>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
[key in keyof TConfigSchema]: ReturnType<
((...args: any[]) => any) & TConfigSchema[key]
>;
}>
>;
dataRefs: TDataRefs;
}> {
}>;
/** @internal */
export function createExtensionBlueprint(options: any): any {
const defineParams = options.defineParams as
| ExtensionBlueprintDefineParams
| undefined;
@@ -518,14 +722,18 @@ export function createExtensionBlueprint<
inputs: options.inputs,
output: options.output as ExtensionDataRef[],
config: options.config,
configSchema: options.configSchema as any,
factory: ctx =>
options.factory(
unwrapParams(args.params, ctx, defineParams, options.kind),
ctx,
ctx as any,
) as Iterable<ExtensionDataValue<any, any>>,
}) as OverridableExtensionDefinition;
},
makeWithOverrides(args) {
makeWithOverrides(args: any) {
if (args.config?.schema) {
warnConfigSchemaPropDeprecation(describeParentCallSite());
}
return createExtension({
kind: options.kind,
name: args.name,
@@ -536,7 +744,7 @@ export function createExtensionBlueprint<
inputs: { ...args.inputs, ...options.inputs },
output: (args.output ?? options.output) as ExtensionDataRef[],
config:
options.config || args.config
options.config?.schema || args.config?.schema
? {
schema: {
...options.config?.schema,
@@ -544,19 +752,18 @@ export function createExtensionBlueprint<
},
}
: undefined,
configSchema:
options.configSchema || args.configSchema
? {
...options.configSchema,
...args.configSchema,
}
: (undefined as any),
factory: ctx => {
const { node, config, inputs, apis } = ctx;
return args.factory(
(innerParams, innerContext) => {
return createExtensionDataContainer<
UOutput extends ExtensionDataRef<
infer IData,
infer IId,
infer IConfig
>
? ExtensionDataRef<IData, IId, IConfig>
: never
>(
(innerParams: any, innerContext: any) => {
return createExtensionDataContainer<any>(
options.factory(
unwrapParams(innerParams, ctx, defineParams, options.kind),
{
@@ -584,23 +791,5 @@ export function createExtensionBlueprint<
},
}) as OverridableExtensionDefinition;
},
} as ExtensionBlueprint<{
kind: TKind;
params: TParams;
output: any;
inputs: string extends keyof TInputs ? {} : TInputs;
config: string extends keyof TConfigSchema
? {}
: {
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
};
configInput: string extends keyof TConfigSchema
? {}
: z.input<
z.ZodObject<{
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
}>
>;
dataRefs: TDataRefs;
}>;
} as ExtensionBlueprint<any>;
}
+4 -4
View File
@@ -343,9 +343,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
@@ -413,9 +413,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+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 =>
+21 -32
View File
@@ -189,31 +189,20 @@ app:
Overriding the card extension allows you to modify how it is implemented.
> [!CAUTION]
> To maintain the same level of configuration, you should define the same or an extended configuration schema.
Here is an example overriding the card extension with a custom component:
```tsx
import { createFrontendModule } from '@backstage/backstage-plugin-api';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha';
export default createFrontendModule({
pluginId: 'catalog-graph',
extensions: [
EntityCardBlueprint.makeWithOverrides({
name: 'entity-relations',
config: {
schema: {
filter: z => z.string().optional(),
// Omitting the rest of default configs for simplicity in this example
},
},
factory(originalFactory, { config }) {
return originalFactory({
loader: () =>
import('./components').then(m => <m.MyEntityRelationsCard />),
});
EntityCardBlueprint.make({
name: 'relations',
params: {
loader: () =>
import('./components').then(m => <m.MyEntityRelationsCard />),
},
}),
],
@@ -291,29 +280,29 @@ app:
Overriding the page extension allows you to modify how it is implemented.
> [!CAUTION]
> To maintain the same level of configuration, you need to define the same or an extended configuration schema. In order to avoid side effects on external plugins that expect this page to be associated with the default path and route reference, remember to use the same default path so that applications that use the default path still point to the same page and the same route reference.
> In order to avoid side effects on external plugins that expect this page to be associated with the default path and route reference, remember to use the same default path so that applications that use the default path still point to the same page and the same route reference.
Here is example overriding the page extension with a custom component:
Here is an example overriding the page extension with a custom component:
```tsx
import { createFrontendModule, createPageExtension, createSchemaFromZod } from '@backstage/backstage-plugin-api';
import { convertLegacyRouteRef } from '@backstage/core-compat-api';
import {
createFrontendModule,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { catalogGraphRouteRef } from '@backstage/plugin-catalog-graph';
export default createFrontendModule({
pluginId: 'catalog-graph',
extensions: [
createPageExtension({
// Omitting name since it is an index page
path: '/catalog-graph',
routeRef: convertLegacyRouteRef(catalogGraphRouteRef),
createSchemaFromZod(z => z.object({
path: z.string().default('/catalog-graph')
// Omitting the rest of default configs for simplicity in this example
})),
loader: () => import('./components').then(m => <m.CustomEntityRelationsPage />)
})
]
PageBlueprint.make({
params: {
path: '/catalog-graph',
routeRef: catalogGraphRouteRef,
loader: () =>
import('./components').then(m => <m.CustomEntityRelationsPage />),
},
}),
],
});
```
+3 -3
View File
@@ -87,7 +87,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:^",
@@ -105,8 +106,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:^",
+2 -2
View File
@@ -339,9 +339,9 @@ export const EntityContentBlueprint: ExtensionBlueprint<{
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
@@ -45,158 +45,19 @@ 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],
},
"disabled": false,
"factory": [Function],
@@ -28,10 +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/v4';
/**
* @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(), createZodV4FilterPredicateSchema()])
.optional(),
type: z.enum(entityCardTypes).optional(),
},
*factory(
{
@@ -47,171 +47,34 @@ 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],
},
"disabled": false,
"factory": [Function],
@@ -30,11 +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/v4';
/**
* @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(), createZodV4FilterPredicateSchema()])
.optional(),
group: z.literal(false).or(z.string()).optional(),
icon: z.string().optional(),
},
*factory(
params: {
@@ -63,144 +63,14 @@ 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],
},
"disabled": false,
"factory": [Function],
+2 -2
View File
@@ -808,9 +808,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -2
View File
@@ -101,9 +101,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
@@ -45,18 +45,14 @@ describe('SearchResultListItemBlueprint', () => {
"input": "items",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"noTrack": {
"default": false,
"type": "boolean",
},
"_fields": {
"noTrack": {
"required": false,
"toJsonSchema": [Function],
"validate": [Function],
},
"type": "object",
},
"parse": [Function],
},
"disabled": false,
"factory": [Function],
+2 -2
View File
@@ -132,9 +132,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -1
View File
@@ -3816,6 +3816,7 @@ __metadata:
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@backstage/version-bridge": "workspace:^"
"@standard-schema/spec": "npm:^1.1.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
"@types/react": "npm:^18.0.0"
@@ -19105,7 +19106,7 @@ __metadata:
languageName: node
linkType: hard
"@standard-schema/spec@npm:^1.0.0":
"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0":
version: 1.1.0
resolution: "@standard-schema/spec@npm:1.1.0"
checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763