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