schema-components 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +63 -3
- package/dist/openapi/components.d.mts +17 -4
- package/dist/openapi/components.mjs +43 -53
- package/dist/openapi/resolve.d.mts +42 -0
- package/dist/openapi/resolve.mjs +72 -0
- package/dist/react/SchemaComponent.d.mts +27 -4
- package/dist/react/SchemaComponent.mjs +25 -11
- package/dist/react/SchemaView.d.mts +5 -1
- package/dist/react/SchemaView.mjs +25 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [1.4.0](https://github.com/Mearman/schema-components/compare/v1.3.3...v1.4.0) (2026-05-14)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add scoped widget resolution at instance, context, and global levels ([b330baf](https://github.com/Mearman/schema-components/commit/b330baf183d00bebf999108f16c9dabd40c243be))
|
|
6
|
+
|
|
7
|
+
### Documentation
|
|
8
|
+
|
|
9
|
+
* update README with scoped widget resolution documentation ([21ac50d](https://github.com/Mearman/schema-components/commit/21ac50dc9a55144fc319fcf482ffad6a16585a27))
|
|
10
|
+
|
|
11
|
+
## [1.3.3](https://github.com/Mearman/schema-components/compare/v1.3.2...v1.3.3) (2026-05-14)
|
|
12
|
+
|
|
13
|
+
### Refactoring
|
|
14
|
+
|
|
15
|
+
* extract pure resolution layer from openapi components ([5e0ab32](https://github.com/Mearman/schema-components/commit/5e0ab32de44b41b23e74082886a5ae881b418883))
|
|
16
|
+
|
|
17
|
+
### Tests
|
|
18
|
+
|
|
19
|
+
* add guards unit tests ([39f53ab](https://github.com/Mearman/schema-components/commit/39f53abf4cf30f1751a09267f34990c4a2532567))
|
|
20
|
+
* add openapi resolution and component unit tests ([75a5892](https://github.com/Mearman/schema-components/commit/75a589220f05b5649a531bec946a63093e824687))
|
|
21
|
+
* add renderer utility unit tests ([3ca3677](https://github.com/Mearman/schema-components/commit/3ca367709acf6324a33a79adf3799528971defc2))
|
|
22
|
+
* expand html and streaming coverage, tighten vitest config ([ac480cd](https://github.com/Mearman/schema-components/commit/ac480cdeb39cf2ae0fa3107de2abae39102cfa26))
|
|
23
|
+
|
|
1
24
|
## [1.3.2](https://github.com/Mearman/schema-components/compare/v1.3.1...v1.3.2) (2026-05-14)
|
|
2
25
|
|
|
3
26
|
### Refactoring
|
package/README.md
CHANGED
|
@@ -434,7 +434,15 @@ Custom resolvers fall back to the default for any type you don't override.
|
|
|
434
434
|
|
|
435
435
|
## Custom widgets
|
|
436
436
|
|
|
437
|
-
|
|
437
|
+
Widgets let you override rendering for specific fields using `.meta({ component: name })`. Three scopes are available, checked in order:
|
|
438
|
+
|
|
439
|
+
1. **Instance** — `widgets` prop on `<SchemaComponent>`
|
|
440
|
+
2. **Context** — `widgets` prop on `<SchemaProvider>`
|
|
441
|
+
3. **Global** — `registerWidget()` for app-wide defaults
|
|
442
|
+
|
|
443
|
+
If none match, the theme adapter or headless default handles the field.
|
|
444
|
+
|
|
445
|
+
### Global registration
|
|
438
446
|
|
|
439
447
|
```tsx
|
|
440
448
|
import { registerWidget } from "schema-components/react/SchemaComponent";
|
|
@@ -443,13 +451,65 @@ registerWidget("richtext", ({ value, onChange }) => (
|
|
|
443
451
|
<RichTextEditor value={value} onChange={onChange} />
|
|
444
452
|
));
|
|
445
453
|
|
|
446
|
-
// In schema
|
|
447
454
|
const schema = z.object({
|
|
448
455
|
bio: z.string().meta({ component: "richtext" }),
|
|
449
456
|
});
|
|
450
457
|
```
|
|
451
458
|
|
|
452
|
-
|
|
459
|
+
### Context-scoped widgets
|
|
460
|
+
|
|
461
|
+
Share widgets across a subtree via `<SchemaProvider>`:
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { SchemaProvider } from "schema-components/react/SchemaComponent";
|
|
465
|
+
import type { WidgetMap } from "schema-components/react/SchemaComponent";
|
|
466
|
+
|
|
467
|
+
const adminWidgets: WidgetMap = new Map([
|
|
468
|
+
["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
|
|
469
|
+
["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
<SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
|
|
473
|
+
<SchemaComponent schema={userSchema} value={user} onChange={setUser} />
|
|
474
|
+
<SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
|
|
475
|
+
</SchemaProvider>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Instance-scoped widgets
|
|
479
|
+
|
|
480
|
+
Override widgets for a single form:
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
const formWidgets: WidgetMap = new Map([
|
|
484
|
+
["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
|
|
485
|
+
]);
|
|
486
|
+
|
|
487
|
+
<SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Resolution order
|
|
491
|
+
|
|
492
|
+
```.meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Instance overrides context. Context overrides global. Unhinted fields skip the widget layer entirely.
|
|
496
|
+
|
|
497
|
+
### `WidgetMap` type
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
import type { WidgetMap } from "schema-components/react/SchemaComponent";
|
|
501
|
+
|
|
502
|
+
// ReadonlyMap<string, (props: RenderProps) => unknown>
|
|
503
|
+
const widgets: WidgetMap = new Map([
|
|
504
|
+
["name", (props) => <MyInput {...props} />],
|
|
505
|
+
]);
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React context available):
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
<SchemaView schema={schema} value={data} widgets={serverWidgets} />
|
|
512
|
+
```
|
|
453
513
|
|
|
454
514
|
## Validation
|
|
455
515
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-DDCD6Xnx.mjs";
|
|
2
|
+
import { WidgetMap } from "../react/SchemaComponent.mjs";
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/openapi/components.d.ts
|
|
@@ -11,6 +12,8 @@ interface ApiOperationProps<Doc = unknown, Path extends string = string, Method
|
|
|
11
12
|
responseValue?: unknown;
|
|
12
13
|
meta?: SchemaMeta;
|
|
13
14
|
requestBodyFields?: Doc extends Record<string, unknown> ? InferRequestBodyFields<Doc, Path, Method> : Record<string, FieldOverride>;
|
|
15
|
+
/** Instance-scoped widgets. */
|
|
16
|
+
widgets?: WidgetMap;
|
|
14
17
|
}
|
|
15
18
|
declare function ApiOperation<Doc = unknown, Path extends string = string, Method extends string = string>({
|
|
16
19
|
schema: doc,
|
|
@@ -20,7 +23,8 @@ declare function ApiOperation<Doc = unknown, Path extends string = string, Metho
|
|
|
20
23
|
onRequestBodyChange,
|
|
21
24
|
responseValue,
|
|
22
25
|
meta,
|
|
23
|
-
requestBodyFields
|
|
26
|
+
requestBodyFields,
|
|
27
|
+
widgets
|
|
24
28
|
}: ApiOperationProps<Doc, Path, Method>): ReactNode;
|
|
25
29
|
interface ApiParametersProps<Doc = unknown, Path extends string = string, Method extends string = string> {
|
|
26
30
|
schema: Doc;
|
|
@@ -28,13 +32,16 @@ interface ApiParametersProps<Doc = unknown, Path extends string = string, Method
|
|
|
28
32
|
method: Method;
|
|
29
33
|
meta?: SchemaMeta;
|
|
30
34
|
overrides?: Doc extends Record<string, unknown> ? InferParameterOverrides<Doc, Path, Method> : Record<string, FieldOverride>;
|
|
35
|
+
/** Instance-scoped widgets. */
|
|
36
|
+
widgets?: WidgetMap;
|
|
31
37
|
}
|
|
32
38
|
declare function ApiParameters<Doc = unknown, Path extends string = string, Method extends string = string>({
|
|
33
39
|
schema: doc,
|
|
34
40
|
path,
|
|
35
41
|
method,
|
|
36
42
|
meta,
|
|
37
|
-
overrides
|
|
43
|
+
overrides,
|
|
44
|
+
widgets
|
|
38
45
|
}: ApiParametersProps<Doc, Path, Method>): ReactNode;
|
|
39
46
|
interface ApiRequestBodyProps<Doc = unknown, Path extends string = string, Method extends string = string> {
|
|
40
47
|
schema: Doc;
|
|
@@ -44,6 +51,8 @@ interface ApiRequestBodyProps<Doc = unknown, Path extends string = string, Metho
|
|
|
44
51
|
onChange?: (value: unknown) => void;
|
|
45
52
|
meta?: SchemaMeta;
|
|
46
53
|
fields?: Doc extends Record<string, unknown> ? InferRequestBodyFields<Doc, Path, Method> : Record<string, FieldOverride>;
|
|
54
|
+
/** Instance-scoped widgets. */
|
|
55
|
+
widgets?: WidgetMap;
|
|
47
56
|
}
|
|
48
57
|
declare function ApiRequestBody<Doc = unknown, Path extends string = string, Method extends string = string>({
|
|
49
58
|
schema: doc,
|
|
@@ -52,7 +61,8 @@ declare function ApiRequestBody<Doc = unknown, Path extends string = string, Met
|
|
|
52
61
|
value,
|
|
53
62
|
onChange,
|
|
54
63
|
meta,
|
|
55
|
-
fields
|
|
64
|
+
fields,
|
|
65
|
+
widgets
|
|
56
66
|
}: ApiRequestBodyProps<Doc, Path, Method>): ReactNode;
|
|
57
67
|
interface ApiResponseProps<Doc = unknown, Path extends string = string, Method extends string = string, Status extends string = string> {
|
|
58
68
|
schema: Doc;
|
|
@@ -62,6 +72,8 @@ interface ApiResponseProps<Doc = unknown, Path extends string = string, Method e
|
|
|
62
72
|
value?: unknown;
|
|
63
73
|
meta?: SchemaMeta;
|
|
64
74
|
fields?: Doc extends Record<string, unknown> ? InferResponseFields<Doc, Path, Method, Status> : Record<string, FieldOverride>;
|
|
75
|
+
/** Instance-scoped widgets. */
|
|
76
|
+
widgets?: WidgetMap;
|
|
65
77
|
}
|
|
66
78
|
declare function ApiResponse<Doc = unknown, Path extends string = string, Method extends string = string, Status extends string = string>({
|
|
67
79
|
schema: doc,
|
|
@@ -70,7 +82,8 @@ declare function ApiResponse<Doc = unknown, Path extends string = string, Method
|
|
|
70
82
|
status,
|
|
71
83
|
value,
|
|
72
84
|
meta,
|
|
73
|
-
fields
|
|
85
|
+
fields,
|
|
86
|
+
widgets
|
|
74
87
|
}: ApiResponseProps<Doc, Path, Method, Status>): ReactNode;
|
|
75
88
|
//#endregion
|
|
76
89
|
export { ApiOperation, ApiOperationProps, ApiParameters, ApiParametersProps, ApiRequestBody, ApiRequestBodyProps, ApiResponse, ApiResponseProps };
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toRecordOrUndefined } from "../core/guards.mjs";
|
|
2
2
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
3
3
|
import { SchemaNormalisationError } from "../core/errors.mjs";
|
|
4
4
|
import { walk } from "../core/walker.mjs";
|
|
5
|
-
import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
|
|
6
5
|
import { renderField } from "../react/SchemaComponent.mjs";
|
|
6
|
+
import { resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
|
|
7
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/openapi/components.tsx
|
|
9
|
-
const docCache = /* @__PURE__ */ new WeakMap();
|
|
10
|
-
function getParsed(doc) {
|
|
11
|
-
const cached = docCache.get(doc);
|
|
12
|
-
if (cached !== void 0) return cached;
|
|
13
|
-
const parsed = parseOpenApiDocument(doc);
|
|
14
|
-
docCache.set(doc, parsed);
|
|
15
|
-
return parsed;
|
|
16
|
-
}
|
|
17
9
|
function noop() {}
|
|
18
|
-
function toDoc(value) {
|
|
19
|
-
return isObject(value) ? value : {};
|
|
20
|
-
}
|
|
21
10
|
function renderSchema(schema, rootDocument, options) {
|
|
22
11
|
let jsonSchema;
|
|
23
12
|
let rootMeta;
|
|
@@ -38,65 +27,62 @@ function renderSchema(schema, rootDocument, options) {
|
|
|
38
27
|
rootDocument
|
|
39
28
|
};
|
|
40
29
|
const tree = walk(jsonSchema, walkOpts);
|
|
41
|
-
const renderChild = (childTree, childValue, childOnChange) => renderField(childTree, childValue, childOnChange, void 0, renderChild);
|
|
42
|
-
return renderField(tree, options.value, options.onChange ?? noop, void 0, renderChild);
|
|
30
|
+
const renderChild = (childTree, childValue, childOnChange) => renderField(childTree, childValue, childOnChange, void 0, renderChild, options.widgets);
|
|
31
|
+
return renderField(tree, options.value, options.onChange ?? noop, void 0, renderChild, options.widgets);
|
|
43
32
|
}
|
|
44
|
-
function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields }) {
|
|
45
|
-
const parsed = getParsed(toDoc(doc));
|
|
33
|
+
function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields, widgets }) {
|
|
46
34
|
const rootDoc = toDoc(doc);
|
|
47
|
-
const
|
|
48
|
-
if (operation === void 0) throw new SchemaNormalisationError(`Operation not found: ${method.toUpperCase()} ${path}`, doc, "openapi-invalid");
|
|
49
|
-
const params = getParameters(parsed, path, method);
|
|
50
|
-
const requestBody = getRequestBody(parsed, path, method);
|
|
51
|
-
const responses = getResponses(parsed, path, method);
|
|
35
|
+
const resolved = resolveOperation(rootDoc, path, method);
|
|
52
36
|
return /* @__PURE__ */ jsxs("section", {
|
|
53
37
|
"data-operation": `${method.toUpperCase()} ${path}`,
|
|
54
38
|
children: [
|
|
55
|
-
/* @__PURE__ */ jsx(OperationHeader, { operation }),
|
|
56
|
-
|
|
39
|
+
/* @__PURE__ */ jsx(OperationHeader, { operation: resolved.operation }),
|
|
40
|
+
resolved.parameters.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
57
41
|
"data-parameters": true,
|
|
58
42
|
children: [/* @__PURE__ */ jsx("h4", { children: "Parameters" }), /* @__PURE__ */ jsx(ParameterList, {
|
|
59
|
-
parameters:
|
|
43
|
+
parameters: resolved.parameters,
|
|
60
44
|
rootDoc,
|
|
61
|
-
meta
|
|
45
|
+
meta,
|
|
46
|
+
widgets
|
|
62
47
|
})]
|
|
63
48
|
}),
|
|
64
|
-
requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
|
|
49
|
+
resolved.requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
|
|
65
50
|
"data-request-body": true,
|
|
66
51
|
children: [
|
|
67
|
-
/* @__PURE__ */ jsxs("h4", { children: ["Request Body", requestBody.required && /* @__PURE__ */ jsx("span", {
|
|
52
|
+
/* @__PURE__ */ jsxs("h4", { children: ["Request Body", resolved.requestBody.required && /* @__PURE__ */ jsx("span", {
|
|
68
53
|
"data-required": true,
|
|
69
54
|
children: "*"
|
|
70
55
|
})] }),
|
|
71
|
-
requestBody.description && /* @__PURE__ */ jsx("p", { children: requestBody.description }),
|
|
72
|
-
requestBody.contentTypes.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
56
|
+
resolved.requestBody.description && /* @__PURE__ */ jsx("p", { children: resolved.requestBody.description }),
|
|
57
|
+
resolved.requestBody.contentTypes.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
73
58
|
"data-content-type": true,
|
|
74
|
-
children: requestBody.contentTypes[0]
|
|
59
|
+
children: resolved.requestBody.contentTypes[0]
|
|
75
60
|
}),
|
|
76
|
-
renderSchema(requestBody.schema, rootDoc, {
|
|
61
|
+
renderSchema(resolved.requestBody.schema, rootDoc, {
|
|
77
62
|
value: requestBodyValue,
|
|
78
63
|
onChange: onRequestBodyChange,
|
|
79
64
|
fields: requestBodyFields,
|
|
80
|
-
meta
|
|
65
|
+
meta,
|
|
66
|
+
widgets
|
|
81
67
|
})
|
|
82
68
|
]
|
|
83
69
|
}),
|
|
84
|
-
responses.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
70
|
+
resolved.responses.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
85
71
|
"data-responses": true,
|
|
86
|
-
children: [/* @__PURE__ */ jsx("h4", { children: "Responses" }), responses.map((response) => /* @__PURE__ */ jsx(ResponseCard, {
|
|
72
|
+
children: [/* @__PURE__ */ jsx("h4", { children: "Responses" }), resolved.responses.map((response) => /* @__PURE__ */ jsx(ResponseCard, {
|
|
87
73
|
response,
|
|
88
74
|
rootDoc,
|
|
89
75
|
value: responseValue,
|
|
90
|
-
meta
|
|
76
|
+
meta,
|
|
77
|
+
widgets
|
|
91
78
|
}, response.statusCode))]
|
|
92
79
|
})
|
|
93
80
|
]
|
|
94
81
|
});
|
|
95
82
|
}
|
|
96
|
-
function ApiParameters({ schema: doc, path, method, meta, overrides }) {
|
|
97
|
-
const parsed = getParsed(toDoc(doc));
|
|
83
|
+
function ApiParameters({ schema: doc, path, method, meta, overrides, widgets }) {
|
|
98
84
|
const rootDoc = toDoc(doc);
|
|
99
|
-
const params =
|
|
85
|
+
const params = resolveParameters(rootDoc, path, method);
|
|
100
86
|
if (params.length === 0) return null;
|
|
101
87
|
return /* @__PURE__ */ jsxs("section", {
|
|
102
88
|
"data-parameters": true,
|
|
@@ -104,14 +90,14 @@ function ApiParameters({ schema: doc, path, method, meta, overrides }) {
|
|
|
104
90
|
parameters: params,
|
|
105
91
|
rootDoc,
|
|
106
92
|
overrides,
|
|
107
|
-
meta
|
|
93
|
+
meta,
|
|
94
|
+
widgets
|
|
108
95
|
})]
|
|
109
96
|
});
|
|
110
97
|
}
|
|
111
|
-
function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields }) {
|
|
112
|
-
const parsed = getParsed(toDoc(doc));
|
|
98
|
+
function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields, widgets }) {
|
|
113
99
|
const rootDoc = toDoc(doc);
|
|
114
|
-
const requestBody =
|
|
100
|
+
const requestBody = resolveRequestBody(rootDoc, path, method);
|
|
115
101
|
if (requestBody?.schema === void 0) return null;
|
|
116
102
|
return /* @__PURE__ */ jsxs("section", {
|
|
117
103
|
"data-request-body": true,
|
|
@@ -129,16 +115,15 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
|
|
|
129
115
|
value,
|
|
130
116
|
onChange,
|
|
131
117
|
fields,
|
|
132
|
-
meta
|
|
118
|
+
meta,
|
|
119
|
+
widgets
|
|
133
120
|
})
|
|
134
121
|
]
|
|
135
122
|
});
|
|
136
123
|
}
|
|
137
|
-
function ApiResponse({ schema: doc, path, method, status, value, meta, fields }) {
|
|
138
|
-
const parsed = getParsed(toDoc(doc));
|
|
124
|
+
function ApiResponse({ schema: doc, path, method, status, value, meta, fields, widgets }) {
|
|
139
125
|
const rootDoc = toDoc(doc);
|
|
140
|
-
const response =
|
|
141
|
-
if (response === void 0) throw new SchemaNormalisationError(`Response not found: ${status}`, doc, "openapi-invalid");
|
|
126
|
+
const response = resolveResponse(rootDoc, path, method, status);
|
|
142
127
|
if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
|
|
143
128
|
"data-status": status,
|
|
144
129
|
children: [
|
|
@@ -152,7 +137,8 @@ function ApiResponse({ schema: doc, path, method, status, value, meta, fields })
|
|
|
152
137
|
rootDoc,
|
|
153
138
|
value,
|
|
154
139
|
fields,
|
|
155
|
-
meta
|
|
140
|
+
meta,
|
|
141
|
+
widgets
|
|
156
142
|
});
|
|
157
143
|
}
|
|
158
144
|
function OperationHeader({ operation }) {
|
|
@@ -169,7 +155,7 @@ function OperationHeader({ operation }) {
|
|
|
169
155
|
})
|
|
170
156
|
] });
|
|
171
157
|
}
|
|
172
|
-
function ParameterList({ parameters, rootDoc, overrides, meta }) {
|
|
158
|
+
function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
|
|
173
159
|
return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((param) => /* @__PURE__ */ jsxs("div", {
|
|
174
160
|
"data-parameter": param.name,
|
|
175
161
|
children: [
|
|
@@ -181,11 +167,14 @@ function ParameterList({ parameters, rootDoc, overrides, meta }) {
|
|
|
181
167
|
"data-description": true,
|
|
182
168
|
children: param.description
|
|
183
169
|
}),
|
|
184
|
-
renderSchema(param.schema ?? { type: "string" }, rootDoc, {
|
|
170
|
+
renderSchema(param.schema ?? { type: "string" }, rootDoc, {
|
|
171
|
+
meta: buildParamMeta(param, overrides, meta),
|
|
172
|
+
widgets
|
|
173
|
+
})
|
|
185
174
|
]
|
|
186
175
|
}, param.name)) });
|
|
187
176
|
}
|
|
188
|
-
function ResponseCard({ response, rootDoc, value, fields, meta }) {
|
|
177
|
+
function ResponseCard({ response, rootDoc, value, fields, meta, widgets }) {
|
|
189
178
|
if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
|
|
190
179
|
"data-status": response.statusCode,
|
|
191
180
|
children: [
|
|
@@ -205,7 +194,8 @@ function ResponseCard({ response, rootDoc, value, fields, meta }) {
|
|
|
205
194
|
meta: {
|
|
206
195
|
readOnly: true,
|
|
207
196
|
...meta
|
|
208
|
-
}
|
|
197
|
+
},
|
|
198
|
+
widgets
|
|
209
199
|
})
|
|
210
200
|
]
|
|
211
201
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OpenApiDocument, OperationInfo, ParameterInfo, ResponseInfo, getRequestBody } from "./parser.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/openapi/resolve.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Parse and cache an OpenAPI document. Returns cached version if
|
|
6
|
+
* the same object identity has been seen before.
|
|
7
|
+
*/
|
|
8
|
+
declare function getParsed(doc: Record<string, unknown>): OpenApiDocument;
|
|
9
|
+
/**
|
|
10
|
+
* Coerce an unknown value to a record, returning an empty record
|
|
11
|
+
* for non-objects.
|
|
12
|
+
*/
|
|
13
|
+
declare function toDoc(value: unknown): Record<string, unknown>;
|
|
14
|
+
interface ResolvedOperation {
|
|
15
|
+
operation: OperationInfo;
|
|
16
|
+
parameters: ParameterInfo[];
|
|
17
|
+
requestBody: ReturnType<typeof getRequestBody>;
|
|
18
|
+
responses: ResponseInfo[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an operation from an OpenAPI document by path and method.
|
|
22
|
+
* Throws if the operation is not found.
|
|
23
|
+
*/
|
|
24
|
+
declare function resolveOperation(doc: Record<string, unknown>, path: string, method: string): ResolvedOperation;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve parameters for an operation. Returns empty array if none.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveParameters(doc: Record<string, unknown>, path: string, method: string): ParameterInfo[];
|
|
29
|
+
/**
|
|
30
|
+
* Resolve request body for an operation. Returns undefined if none.
|
|
31
|
+
*/
|
|
32
|
+
declare function resolveRequestBody(doc: Record<string, unknown>, path: string, method: string): ReturnType<typeof getRequestBody>;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a specific response by status code. Throws if not found.
|
|
35
|
+
*/
|
|
36
|
+
declare function resolveResponse(doc: Record<string, unknown>, path: string, method: string, statusCode: string): ResponseInfo;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve all responses for an operation.
|
|
39
|
+
*/
|
|
40
|
+
declare function resolveResponses(doc: Record<string, unknown>, path: string, method: string): ResponseInfo[];
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ResolvedOperation, getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, resolveResponses, toDoc };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
|
|
3
|
+
//#region src/openapi/resolve.ts
|
|
4
|
+
/**
|
|
5
|
+
* OpenAPI document resolution and caching.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions for looking up operations, parameters, request bodies,
|
|
8
|
+
* and responses from parsed OpenAPI documents. Extracted from components
|
|
9
|
+
* for testability without React.
|
|
10
|
+
*/
|
|
11
|
+
const docCache = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
/**
|
|
13
|
+
* Parse and cache an OpenAPI document. Returns cached version if
|
|
14
|
+
* the same object identity has been seen before.
|
|
15
|
+
*/
|
|
16
|
+
function getParsed(doc) {
|
|
17
|
+
const cached = docCache.get(doc);
|
|
18
|
+
if (cached !== void 0) return cached;
|
|
19
|
+
const parsed = parseOpenApiDocument(doc);
|
|
20
|
+
docCache.set(doc, parsed);
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Coerce an unknown value to a record, returning an empty record
|
|
25
|
+
* for non-objects.
|
|
26
|
+
*/
|
|
27
|
+
function toDoc(value) {
|
|
28
|
+
return isObject(value) ? value : {};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an operation from an OpenAPI document by path and method.
|
|
32
|
+
* Throws if the operation is not found.
|
|
33
|
+
*/
|
|
34
|
+
function resolveOperation(doc, path, method) {
|
|
35
|
+
const parsed = getParsed(doc);
|
|
36
|
+
const operation = listOperations(parsed).find((op) => op.path === path && op.method === method);
|
|
37
|
+
if (operation === void 0) throw new Error(`Operation not found: ${method.toUpperCase()} ${path}`);
|
|
38
|
+
return {
|
|
39
|
+
operation,
|
|
40
|
+
parameters: getParameters(parsed, path, method),
|
|
41
|
+
requestBody: getRequestBody(parsed, path, method),
|
|
42
|
+
responses: getResponses(parsed, path, method)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve parameters for an operation. Returns empty array if none.
|
|
47
|
+
*/
|
|
48
|
+
function resolveParameters(doc, path, method) {
|
|
49
|
+
return getParameters(getParsed(doc), path, method);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve request body for an operation. Returns undefined if none.
|
|
53
|
+
*/
|
|
54
|
+
function resolveRequestBody(doc, path, method) {
|
|
55
|
+
return getRequestBody(getParsed(doc), path, method);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a specific response by status code. Throws if not found.
|
|
59
|
+
*/
|
|
60
|
+
function resolveResponse(doc, path, method, statusCode) {
|
|
61
|
+
const response = getResponses(getParsed(doc), path, method).find((r) => r.statusCode === statusCode);
|
|
62
|
+
if (response === void 0) throw new Error(`Response not found: ${statusCode}`);
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve all responses for an operation.
|
|
67
|
+
*/
|
|
68
|
+
function resolveResponses(doc, path, method) {
|
|
69
|
+
return getResponses(getParsed(doc), path, method);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, resolveResponses, toDoc };
|
|
@@ -5,13 +5,33 @@ import { ReactNode } from "react";
|
|
|
5
5
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
6
6
|
|
|
7
7
|
//#region src/react/SchemaComponent.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Widget map — maps component hints (from `.meta({ component })`) to render
|
|
10
|
+
* functions. Scoped at three levels:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Per-instance** — `widgets` prop on `<SchemaComponent>`
|
|
13
|
+
* 2. **Context-scoped** — `widgets` prop on `<SchemaProvider>`
|
|
14
|
+
* 3. **Global** — `registerWidget()` (app-wide defaults)
|
|
15
|
+
*
|
|
16
|
+
* Resolution order: instance → context → global → resolver → headless.
|
|
17
|
+
*/
|
|
18
|
+
type WidgetMap = ReadonlyMap<string, (props: RenderProps) => unknown>;
|
|
8
19
|
declare function SchemaProvider({
|
|
9
20
|
resolver,
|
|
21
|
+
widgets,
|
|
10
22
|
children
|
|
11
23
|
}: {
|
|
12
|
-
resolver: ComponentResolver;
|
|
24
|
+
resolver: ComponentResolver; /** Scoped widgets available to all SchemaComponents in this subtree. */
|
|
25
|
+
widgets?: WidgetMap;
|
|
13
26
|
children: ReactNode;
|
|
14
27
|
}): _$react_jsx_runtime0.JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* Register a widget globally. The widget is resolved when a schema field
|
|
30
|
+
* has `.meta({ component: name })`.
|
|
31
|
+
*
|
|
32
|
+
* For scoped registration, use the `widgets` prop on `<SchemaComponent>`
|
|
33
|
+
* or `<SchemaProvider>` instead.
|
|
34
|
+
*/
|
|
15
35
|
declare function registerWidget(name: string, render: (props: RenderProps) => unknown): void;
|
|
16
36
|
type InferFields<T, Ref extends string | undefined> = T extends z.ZodType ? FieldOverrides<z.infer<T>> : T extends {
|
|
17
37
|
openapi: unknown;
|
|
@@ -41,6 +61,8 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
|
|
|
41
61
|
writeOnly?: boolean;
|
|
42
62
|
/** Convenience: sets description on the root. */
|
|
43
63
|
description?: string;
|
|
64
|
+
/** Instance-scoped widgets — override context and global widgets. */
|
|
65
|
+
widgets?: WidgetMap;
|
|
44
66
|
}
|
|
45
67
|
declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined>({
|
|
46
68
|
schema: schemaInput,
|
|
@@ -54,9 +76,10 @@ declare function SchemaComponent<T = unknown, Ref extends string | undefined = u
|
|
|
54
76
|
meta: componentMeta,
|
|
55
77
|
readOnly,
|
|
56
78
|
writeOnly,
|
|
57
|
-
description
|
|
79
|
+
description,
|
|
80
|
+
widgets: instanceWidgets
|
|
58
81
|
}: SchemaComponentProps<T, Ref>): ReactNode;
|
|
59
|
-
declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => ReactNode): ReactNode;
|
|
82
|
+
declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => ReactNode, instanceWidgets?: WidgetMap, contextWidgets?: WidgetMap): ReactNode;
|
|
60
83
|
/**
|
|
61
84
|
* Infer the schema's output type for SchemaField path inference.
|
|
62
85
|
*/
|
|
@@ -93,4 +116,4 @@ declare function SchemaField<T = unknown, Ref extends string | undefined = undef
|
|
|
93
116
|
onValidationError
|
|
94
117
|
}: SchemaFieldProps<T, Ref, P>): ReactNode;
|
|
95
118
|
//#endregion
|
|
96
|
-
export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, registerWidget, renderField };
|
|
119
|
+
export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, registerWidget, renderField };
|
|
@@ -23,18 +23,31 @@ import { jsx } from "react/jsx-runtime";
|
|
|
23
23
|
* - Runtime schemas → Record<string, FieldOverride> (no autocomplete)
|
|
24
24
|
*/
|
|
25
25
|
const UserResolverContext = createContext(void 0);
|
|
26
|
-
|
|
26
|
+
const WidgetsContext = createContext(void 0);
|
|
27
|
+
function SchemaProvider({ resolver, widgets, children }) {
|
|
27
28
|
return /* @__PURE__ */ jsx(UserResolverContext.Provider, {
|
|
28
29
|
value: resolver,
|
|
29
|
-
children
|
|
30
|
+
children: /* @__PURE__ */ jsx(WidgetsContext.Provider, {
|
|
31
|
+
value: widgets,
|
|
32
|
+
children
|
|
33
|
+
})
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
|
-
|
|
36
|
+
/** Global widget registry — app-wide defaults. */
|
|
37
|
+
const globalWidgets = /* @__PURE__ */ new Map();
|
|
38
|
+
/**
|
|
39
|
+
* Register a widget globally. The widget is resolved when a schema field
|
|
40
|
+
* has `.meta({ component: name })`.
|
|
41
|
+
*
|
|
42
|
+
* For scoped registration, use the `widgets` prop on `<SchemaComponent>`
|
|
43
|
+
* or `<SchemaProvider>` instead.
|
|
44
|
+
*/
|
|
33
45
|
function registerWidget(name, render) {
|
|
34
|
-
|
|
46
|
+
globalWidgets.set(name, render);
|
|
35
47
|
}
|
|
36
|
-
function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, fields, meta: componentMeta, readOnly, writeOnly, description }) {
|
|
48
|
+
function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets }) {
|
|
37
49
|
const userResolver = useContext(UserResolverContext);
|
|
50
|
+
const contextWidgets = useContext(WidgetsContext);
|
|
38
51
|
const mergedMeta = useMemo(() => {
|
|
39
52
|
const merged = { ...componentMeta };
|
|
40
53
|
if (readOnly === true) merged.readOnly = true;
|
|
@@ -82,9 +95,9 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
|
|
|
82
95
|
rootDocument
|
|
83
96
|
});
|
|
84
97
|
const renderChild = (childTree, childValue, childOnChange) => {
|
|
85
|
-
return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
|
|
98
|
+
return renderField(childTree, childValue, childOnChange, userResolver, renderChild, instanceWidgets, contextWidgets);
|
|
86
99
|
};
|
|
87
|
-
return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild);
|
|
100
|
+
return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets);
|
|
88
101
|
}
|
|
89
102
|
function runValidation(zodSchema, jsonSchema, value, onError) {
|
|
90
103
|
if (zodSchema !== void 0 && isObject(zodSchema)) {
|
|
@@ -107,10 +120,10 @@ function runValidation(zodSchema, jsonSchema, value, onError) {
|
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
|
-
function renderField(tree, value, onChange, userResolver, renderChild) {
|
|
123
|
+
function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets) {
|
|
111
124
|
const componentHint = tree.meta.component;
|
|
112
125
|
if (typeof componentHint === "string") {
|
|
113
|
-
const widget =
|
|
126
|
+
const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
|
|
114
127
|
if (widget !== void 0) {
|
|
115
128
|
const result = widget(buildRenderProps(tree, value, onChange, renderChild));
|
|
116
129
|
if (result !== void 0 && result !== null) {
|
|
@@ -160,6 +173,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
|
|
|
160
173
|
}
|
|
161
174
|
function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
|
|
162
175
|
const userResolver = useContext(UserResolverContext);
|
|
176
|
+
const contextWidgets = useContext(WidgetsContext);
|
|
163
177
|
let jsonSchema;
|
|
164
178
|
let zodSchema;
|
|
165
179
|
let rootMeta;
|
|
@@ -197,9 +211,9 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
|
|
|
197
211
|
onValidationError
|
|
198
212
|
]);
|
|
199
213
|
const renderChild = (childTree, childValue, childOnChange) => {
|
|
200
|
-
return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
|
|
214
|
+
return renderField(childTree, childValue, childOnChange, userResolver, renderChild, void 0, contextWidgets);
|
|
201
215
|
};
|
|
202
|
-
return renderField(fieldTree, fieldValue, handleChange, userResolver, renderChild);
|
|
216
|
+
return renderField(fieldTree, fieldValue, handleChange, userResolver, renderChild, void 0, contextWidgets);
|
|
203
217
|
}
|
|
204
218
|
function resolvePath(tree, path) {
|
|
205
219
|
if (path.length === 0) return tree;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { b as ComponentResolver, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
|
|
2
|
+
import { WidgetMap } from "./SchemaComponent.mjs";
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/react/SchemaView.d.ts
|
|
@@ -21,6 +22,8 @@ interface SchemaViewProps {
|
|
|
21
22
|
* Falls back to the headless resolver if omitted.
|
|
22
23
|
*/
|
|
23
24
|
resolver?: ComponentResolver;
|
|
25
|
+
/** Instance-scoped widgets. */
|
|
26
|
+
widgets?: WidgetMap;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Server-safe schema renderer — no hooks, no context, no state.
|
|
@@ -35,7 +38,8 @@ declare function SchemaView({
|
|
|
35
38
|
fields,
|
|
36
39
|
meta: componentMeta,
|
|
37
40
|
description,
|
|
38
|
-
resolver
|
|
41
|
+
resolver,
|
|
42
|
+
widgets
|
|
39
43
|
}: SchemaViewProps): ReactNode;
|
|
40
44
|
//#endregion
|
|
41
45
|
export { SchemaView, SchemaViewProps };
|
|
@@ -37,7 +37,7 @@ function noop() {}
|
|
|
37
37
|
* Always renders in read-only mode. For editable forms, use
|
|
38
38
|
* `<SchemaComponent>` with `"use client"`.
|
|
39
39
|
*/
|
|
40
|
-
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver }) {
|
|
40
|
+
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets }) {
|
|
41
41
|
const mergedMeta = {
|
|
42
42
|
...componentMeta,
|
|
43
43
|
readOnly: true
|
|
@@ -61,10 +61,31 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
|
|
|
61
61
|
rootDocument
|
|
62
62
|
});
|
|
63
63
|
const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
|
|
64
|
-
const renderChild = (childTree, childValue) => renderFieldServer(childTree, childValue, userResolver, renderChild);
|
|
65
|
-
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild);
|
|
64
|
+
const renderChild = (childTree, childValue) => renderFieldServer(childTree, childValue, userResolver, renderChild, widgets);
|
|
65
|
+
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
|
|
66
66
|
}
|
|
67
|
-
function renderFieldServer(tree, value, resolver, renderChild) {
|
|
67
|
+
function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
68
|
+
const componentHint = tree.meta.component;
|
|
69
|
+
if (typeof componentHint === "string") {
|
|
70
|
+
const widget = widgets?.get(componentHint);
|
|
71
|
+
if (widget !== void 0) {
|
|
72
|
+
const result = widget({
|
|
73
|
+
value,
|
|
74
|
+
onChange: noop,
|
|
75
|
+
readOnly: true,
|
|
76
|
+
writeOnly: false,
|
|
77
|
+
meta: tree.meta,
|
|
78
|
+
constraints: tree.constraints,
|
|
79
|
+
path: "",
|
|
80
|
+
tree,
|
|
81
|
+
renderChild: (childTree, childValue) => renderChild(childTree, childValue)
|
|
82
|
+
});
|
|
83
|
+
if (result !== void 0 && result !== null) {
|
|
84
|
+
if (isValidElement(result)) return result;
|
|
85
|
+
if (typeof result === "string" || typeof result === "number") return result;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
68
89
|
const renderFn = getRenderFunction(tree.type, resolver);
|
|
69
90
|
if (renderFn !== void 0) {
|
|
70
91
|
const props = {
|