schema-components 1.21.0 → 1.22.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/README.md +1 -1
- package/dist/core/adapter.d.mts +20 -3
- package/dist/core/adapter.mjs +209 -28
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +22 -1
- package/dist/core/formats.mjs +21 -0
- package/dist/core/limits.d.mts +2 -0
- package/dist/core/limits.mjs +23 -0
- package/dist/core/merge.d.mts +1 -1
- package/dist/core/normalise.d.mts +29 -3
- package/dist/core/normalise.mjs +2 -2
- package/dist/core/openapi30.mjs +1 -1
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/ref.mjs +1 -0
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/renderer.mjs +0 -2
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +2 -2
- package/dist/core/types.d.mts +2 -2
- package/dist/core/types.mjs +1 -4
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +3 -3
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +79 -2
- package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-D0QCYGv0.d.mts} +1 -1
- package/dist/{errors-QEwOtQAA.d.mts → errors-DpFwqs5C.d.mts} +1 -1
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/a11y.mjs +10 -3
- package/dist/html/renderToHtml.d.mts +10 -3
- package/dist/html/renderToHtml.mjs +13 -3
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderers.d.mts +2 -2
- package/dist/html/renderers.mjs +0 -5
- package/dist/html/streamRenderers.d.mts +5 -4
- package/dist/html/streamRenderers.mjs +91 -30
- package/dist/limits-Cw5QZND8.d.mts +29 -0
- package/dist/{normalise-DaSrnr8g.mjs → normalise-DVEJQmF7.mjs} +468 -115
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/ApiSecurity.mjs +16 -2
- package/dist/openapi/components.d.mts +150 -18
- package/dist/openapi/components.mjs +129 -15
- package/dist/openapi/parser.d.mts +1 -1
- package/dist/openapi/parser.mjs +35 -3
- package/dist/openapi/resolve.d.mts +12 -5
- package/dist/openapi/resolve.mjs +183 -23
- package/dist/react/SchemaComponent.d.mts +100 -35
- package/dist/react/SchemaComponent.mjs +59 -45
- package/dist/react/SchemaView.d.mts +3 -3
- package/dist/react/SchemaView.mjs +2 -2
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headless.mjs +1 -2
- package/dist/react/headlessRenderers.d.mts +3 -4
- package/dist/react/headlessRenderers.mjs +10 -30
- package/dist/{ref-si8ViYun.d.mts → ref-D-_JBZkF.d.mts} +1 -1
- package/dist/{renderer-DI6ZYf7a.d.mts → renderer-BaRlQIuN.d.mts} +2 -2
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/typeInference-DkcUHfaM.d.mts +982 -0
- package/dist/{types-BnxPEElk.d.mts → types-BrRMV0en.d.mts} +3 -10
- package/package.json +1 -3
- package/dist/typeInference-Bxw3NOG1.d.mts +0 -647
- /package/dist/{version-D-u7aMfy.d.mts → version-D2jfdX6E.d.mts} +0 -0
package/dist/openapi/resolve.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getProperty, isObject } from "../core/guards.mjs";
|
|
2
|
+
import "../core/limits.mjs";
|
|
3
|
+
import { emitDiagnostic } from "../core/diagnostics.mjs";
|
|
2
4
|
import { isPrototypePollutingKey } from "../core/uri.mjs";
|
|
3
5
|
import { detectOpenApiVersion } from "../core/version.mjs";
|
|
4
|
-
import { a as normaliseOpenApiSchemas } from "../normalise-
|
|
5
|
-
import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
|
|
6
|
+
import { a as normaliseOpenApiSchemas } from "../normalise-DVEJQmF7.mjs";
|
|
7
|
+
import { getParameters, getRequestBody, getResponses, listOperations, listWebhooks, parseOpenApiDocument } from "./parser.mjs";
|
|
6
8
|
//#region src/openapi/resolve.ts
|
|
7
9
|
/**
|
|
8
10
|
* OpenAPI document resolution and caching.
|
|
@@ -43,7 +45,20 @@ function getParsed(doc, diagnostics) {
|
|
|
43
45
|
if (cached !== void 0) return cached;
|
|
44
46
|
}
|
|
45
47
|
const version = detectOpenApiVersion(doc);
|
|
48
|
+
if (diagnostics !== void 0 && version?.major === 3 && docHasXmlAnywhere(doc)) emitDiagnostic(diagnostics, {
|
|
49
|
+
code: "dropped-swagger-feature",
|
|
50
|
+
message: `OpenAPI ${String(version.major)}.${String(version.minor)} xml Schema Object metadata is not rendered and will be ignored`,
|
|
51
|
+
pointer: "",
|
|
52
|
+
detail: {
|
|
53
|
+
feature: "xml",
|
|
54
|
+
source: "openapi-3.x"
|
|
55
|
+
}
|
|
56
|
+
});
|
|
46
57
|
const normalisedDoc = version !== void 0 ? normaliseOpenApiSchemas(doc, version, diagnostics) : doc;
|
|
58
|
+
if (diagnostics !== void 0) {
|
|
59
|
+
validateSecuritySchemeTypes(normalisedDoc, diagnostics);
|
|
60
|
+
detectUnsupportedCrossSchemaRefs(normalisedDoc, diagnostics);
|
|
61
|
+
}
|
|
47
62
|
const parsed = parseOpenApiDocument(normalisedDoc);
|
|
48
63
|
if (diagnostics === void 0) {
|
|
49
64
|
docCache.set(doc, parsed);
|
|
@@ -52,11 +67,133 @@ function getParsed(doc, diagnostics) {
|
|
|
52
67
|
return parsed;
|
|
53
68
|
}
|
|
54
69
|
/**
|
|
55
|
-
* Coerce an unknown value to a record, returning
|
|
56
|
-
*
|
|
70
|
+
* Coerce an unknown value to a record, returning `undefined` when the
|
|
71
|
+
* value is not a plain object. Callers MUST handle the `undefined` case
|
|
72
|
+
* explicitly — typically by rendering a "doc not an object" diagnostic
|
|
73
|
+
* and short-circuiting, never by silently substituting `{}`.
|
|
74
|
+
*
|
|
75
|
+
* A previous implementation fell back to `{}` for non-objects, which
|
|
76
|
+
* masked configuration mistakes (passing a string, `null`, an array, or
|
|
77
|
+
* `undefined` as the OpenAPI document) as an empty document with no
|
|
78
|
+
* operations.
|
|
57
79
|
*/
|
|
58
80
|
function toDoc(value) {
|
|
59
|
-
return isObject(value) ? value :
|
|
81
|
+
return isObject(value) ? value : void 0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Known security scheme types per the OpenAPI 3.0/3.1 specification.
|
|
85
|
+
* `mutualTLS` was added in OpenAPI 3.1. Unknown values surface a
|
|
86
|
+
* `unknown-security-scheme-type` diagnostic so authors notice typos
|
|
87
|
+
* (e.g. `mutalTLS`) that would otherwise render with no warning.
|
|
88
|
+
*/
|
|
89
|
+
const KNOWN_SECURITY_SCHEME_TYPES = new Set([
|
|
90
|
+
"apiKey",
|
|
91
|
+
"http",
|
|
92
|
+
"oauth2",
|
|
93
|
+
"openIdConnect",
|
|
94
|
+
"mutualTLS"
|
|
95
|
+
]);
|
|
96
|
+
/**
|
|
97
|
+
* Validate every `components.securitySchemes.<name>.type` against the
|
|
98
|
+
* canonical OpenAPI security scheme types and emit
|
|
99
|
+
* `unknown-security-scheme-type` for each entry whose type is not
|
|
100
|
+
* recognised. Runs after normalisation so Swagger 2.0 documents (which
|
|
101
|
+
* are already translated to OAS 3.x shapes by `translateSwaggerSecurityScheme`)
|
|
102
|
+
* are validated alongside native 3.x documents.
|
|
103
|
+
*/
|
|
104
|
+
function validateSecuritySchemeTypes(doc, diagnostics) {
|
|
105
|
+
const components = doc.components;
|
|
106
|
+
if (!isObject(components)) return;
|
|
107
|
+
const schemes = components.securitySchemes;
|
|
108
|
+
if (!isObject(schemes)) return;
|
|
109
|
+
for (const [name, scheme] of Object.entries(schemes)) {
|
|
110
|
+
if (!isObject(scheme)) continue;
|
|
111
|
+
const type = scheme.type;
|
|
112
|
+
if (typeof type !== "string") {
|
|
113
|
+
emitDiagnostic(diagnostics, {
|
|
114
|
+
code: "unknown-security-scheme-type",
|
|
115
|
+
message: `Security scheme "${name}" has no type or a non-string type`,
|
|
116
|
+
pointer: `/components/securitySchemes/${name}/type`,
|
|
117
|
+
detail: {
|
|
118
|
+
name,
|
|
119
|
+
type
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!KNOWN_SECURITY_SCHEME_TYPES.has(type)) emitDiagnostic(diagnostics, {
|
|
125
|
+
code: "unknown-security-scheme-type",
|
|
126
|
+
message: `Security scheme "${name}" declares unknown type "${type}"`,
|
|
127
|
+
pointer: `/components/securitySchemes/${name}/type`,
|
|
128
|
+
detail: {
|
|
129
|
+
name,
|
|
130
|
+
type
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Detect any `$ref` strings that survived normalisation in a non-
|
|
137
|
+
* fragment shape (anything not starting with `#/` or `#`). After
|
|
138
|
+
* `normaliseOpenApiSchemas` runs `resolveRelativeRefs`, every relative
|
|
139
|
+
* `$ref` within a Schema Object is rewritten to an absolute fragment.
|
|
140
|
+
* Refs that *cross* Schema Object boundaries — for example, a relative
|
|
141
|
+
* ref inside one component schema pointing into another via a sibling
|
|
142
|
+
* `$id` — cannot be resolved by the current pipeline (this is a
|
|
143
|
+
* documented limitation; see the JSDoc on this function).
|
|
144
|
+
*
|
|
145
|
+
* Emit a single diagnostic per offending ref so consumers notice
|
|
146
|
+
* silently broken references rather than discovering them only when
|
|
147
|
+
* the walker fails to render the target.
|
|
148
|
+
*
|
|
149
|
+
* NOTE: We can't determine "crossing" cleanly from the parser alone —
|
|
150
|
+
* doing so would require modelling every Schema Object's $id scope.
|
|
151
|
+
* As a pragmatic approximation, any surviving non-`#`-prefixed `$ref`
|
|
152
|
+
* is treated as cross-Schema-Object unsupported. False positives
|
|
153
|
+
* (legitimate external refs that the consumer planned to bundle later)
|
|
154
|
+
* are still useful — they confirm an unresolved reference is present.
|
|
155
|
+
*/
|
|
156
|
+
function detectUnsupportedCrossSchemaRefs(doc, diagnostics) {
|
|
157
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
158
|
+
const walk = (node, pointer) => {
|
|
159
|
+
if (Array.isArray(node)) {
|
|
160
|
+
for (const [index, item] of node.entries()) walk(item, `${pointer}/${String(index)}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!isObject(node)) return;
|
|
164
|
+
const ref = node.$ref;
|
|
165
|
+
if (typeof ref === "string" && !ref.startsWith("#") && !seenRefs.has(ref)) {
|
|
166
|
+
seenRefs.add(ref);
|
|
167
|
+
emitDiagnostic(diagnostics, {
|
|
168
|
+
code: "cross-schema-relative-ref-unsupported",
|
|
169
|
+
message: `Relative \`$ref\` "${ref}" was not resolved during normalisation; cross-Schema-Object relative refs are not currently supported`,
|
|
170
|
+
pointer,
|
|
171
|
+
detail: { ref }
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const [key, value] of Object.entries(node)) walk(value, `${pointer}/${key}`);
|
|
176
|
+
};
|
|
177
|
+
walk(doc, "");
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Recursively check whether any node in an OpenAPI document carries an
|
|
181
|
+
* `xml` annotation. Walks both objects and arrays so the check works
|
|
182
|
+
* for schemas in `components/schemas`, inline `paths`/`webhooks`
|
|
183
|
+
* schemas, request bodies, responses, headers, and parameters. Used
|
|
184
|
+
* by `getParsed` to surface the dropped-feature diagnostic for OAS
|
|
185
|
+
* 3.0/3.1 — the Swagger 2.0 path has its own detection in
|
|
186
|
+
* `swagger2.ts`.
|
|
187
|
+
*/
|
|
188
|
+
function docHasXmlAnywhere(node) {
|
|
189
|
+
if (Array.isArray(node)) {
|
|
190
|
+
for (const item of node) if (docHasXmlAnywhere(item)) return true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (!isObject(node)) return false;
|
|
194
|
+
if ("xml" in node && isObject(node.xml)) return true;
|
|
195
|
+
for (const value of Object.values(node)) if (docHasXmlAnywhere(value)) return true;
|
|
196
|
+
return false;
|
|
60
197
|
}
|
|
61
198
|
/**
|
|
62
199
|
* Look up a Path Item Object on the (already-normalised) parsed document,
|
|
@@ -67,23 +204,46 @@ function toDoc(value) {
|
|
|
67
204
|
* Implemented inside `resolve.ts` to avoid touching `parser.ts` while
|
|
68
205
|
* still surfacing path-item-level metadata to the React layer.
|
|
69
206
|
*/
|
|
70
|
-
function lookupPathItemNode(parsed, path) {
|
|
71
|
-
|
|
207
|
+
function lookupPathItemNode(parsed, path, diagnostics) {
|
|
208
|
+
const fromPaths = resolvePathItemNode(parsed, getProperty(getProperty(parsed.doc, "paths"), path), diagnostics);
|
|
209
|
+
if (fromPaths !== void 0) return fromPaths;
|
|
210
|
+
return resolvePathItemNode(parsed, getProperty(getProperty(parsed.doc, "webhooks"), path), diagnostics);
|
|
72
211
|
}
|
|
73
|
-
function resolvePathItemNode(parsed, pathItem) {
|
|
212
|
+
function resolvePathItemNode(parsed, pathItem, diagnostics) {
|
|
74
213
|
if (!isObject(pathItem)) return void 0;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
214
|
+
const visited = /* @__PURE__ */ new Set();
|
|
215
|
+
let current = pathItem;
|
|
216
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
217
|
+
const ref = getProperty(current, "$ref");
|
|
218
|
+
if (typeof ref !== "string") return current;
|
|
219
|
+
if (!ref.startsWith("#/")) return current;
|
|
220
|
+
if (visited.has(ref)) {
|
|
221
|
+
emitDiagnostic(diagnostics, {
|
|
222
|
+
code: "cyclic-path-item-ref",
|
|
223
|
+
message: `Cyclic Path Item Object $ref "${ref}"`,
|
|
224
|
+
pointer: ref,
|
|
225
|
+
detail: { ref }
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
visited.add(ref);
|
|
230
|
+
const parts = ref.slice(2).split("/");
|
|
231
|
+
let node = parsed.doc;
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
if (!isObject(node)) return void 0;
|
|
234
|
+
const decoded = part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
235
|
+
if (isPrototypePollutingKey(decoded)) return void 0;
|
|
236
|
+
node = node[decoded];
|
|
237
|
+
}
|
|
238
|
+
if (!isObject(node)) return current;
|
|
239
|
+
current = node;
|
|
85
240
|
}
|
|
86
|
-
|
|
241
|
+
emitDiagnostic(diagnostics, {
|
|
242
|
+
code: "path-item-ref-too-deep",
|
|
243
|
+
message: `Path Item Object $ref chain exceeded ${String(8)} hops`,
|
|
244
|
+
pointer: "",
|
|
245
|
+
detail: { maxHops: 8 }
|
|
246
|
+
});
|
|
87
247
|
}
|
|
88
248
|
function extractPathItemInfo(pathItem) {
|
|
89
249
|
const summary = pathItem.summary;
|
|
@@ -103,10 +263,10 @@ function extractPathItemInfo(pathItem) {
|
|
|
103
263
|
* normalisation pipeline (every re-run would emit each diagnostic
|
|
104
264
|
* again into the sink).
|
|
105
265
|
*/
|
|
106
|
-
function resolveOperationFromParsed(parsed, path, method) {
|
|
107
|
-
const
|
|
266
|
+
function resolveOperationFromParsed(parsed, path, method, diagnostics) {
|
|
267
|
+
const pathItemNode = lookupPathItemNode(parsed, path, diagnostics);
|
|
268
|
+
const operation = [...listOperations(parsed), ...listWebhooks(parsed).flatMap((w) => w.operations)].find((op) => op.path === path && op.method === method);
|
|
108
269
|
if (operation === void 0) throw new Error(`Operation not found: ${method.toUpperCase()} ${path}`);
|
|
109
|
-
const pathItemNode = lookupPathItemNode(parsed, path);
|
|
110
270
|
if (pathItemNode === void 0) throw new Error(`Path item missing for ${method.toUpperCase()} ${path}`);
|
|
111
271
|
return {
|
|
112
272
|
operation,
|
|
@@ -124,7 +284,7 @@ function resolveOperationFromParsed(parsed, path, method) {
|
|
|
124
284
|
* events surface to the caller's sink.
|
|
125
285
|
*/
|
|
126
286
|
function resolveOperation(doc, path, method, diagnostics) {
|
|
127
|
-
return resolveOperationFromParsed(getParsed(doc, diagnostics), path, method);
|
|
287
|
+
return resolveOperationFromParsed(getParsed(doc, diagnostics), path, method, diagnostics);
|
|
128
288
|
}
|
|
129
289
|
/**
|
|
130
290
|
* Resolve parameters against an already-parsed document. See
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as Diagnostic } from "../diagnostics-
|
|
3
|
-
import { t as SchemaError } from "../errors-
|
|
4
|
-
import { l as RenderProps, r as ComponentResolver } from "../renderer-
|
|
5
|
-
import {
|
|
1
|
+
import { d as FieldOverrides, j as WalkedField, u as FieldOverride, w as SchemaMeta } from "../types-BrRMV0en.mjs";
|
|
2
|
+
import { t as Diagnostic } from "../diagnostics-D0QCYGv0.mjs";
|
|
3
|
+
import { t as SchemaError } from "../errors-DpFwqs5C.mjs";
|
|
4
|
+
import { l as RenderProps, r as ComponentResolver } from "../renderer-BaRlQIuN.mjs";
|
|
5
|
+
import { d as RejectUnrepresentableZod, f as ResolveOpenAPIRef, i as FromJSONSchemaMode, p as TypeAtPath, r as FromJSONSchema, u as PathOfType } from "../typeInference-DkcUHfaM.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
8
8
|
import { ReactNode } from "react";
|
|
@@ -39,7 +39,54 @@ declare function registerWidget(name: string, render: (props: RenderProps) => un
|
|
|
39
39
|
type InferFields<T, Ref extends string | undefined> = T extends z.ZodType ? FieldOverrides<z.infer<T>> : T extends {
|
|
40
40
|
openapi: unknown;
|
|
41
41
|
} ? Ref extends string ? FieldOverrides<ResolveOpenAPIRef<T & Record<string, unknown>, Ref>> : Record<string, FieldOverride> : T extends object ? unknown extends FromJSONSchema<T> ? Record<string, FieldOverride> : FieldOverrides<FromJSONSchema<T>> : Record<string, FieldOverride>;
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Infer the data type carried by the schema input.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors {@link InferFields}'s dispatch order: Zod schema → `z.infer`,
|
|
46
|
+
* OpenAPI doc + ref → `ResolveOpenAPIRef`, plain JSON Schema object →
|
|
47
|
+
* `FromJSONSchema`, everything else → `unknown`. The `Mode` parameter
|
|
48
|
+
* is plumbed through to `FromJSONSchema` / `ResolveOpenAPIRef` so
|
|
49
|
+
* `readOnly` / `writeOnly` keywords participate in the inferred
|
|
50
|
+
* object shape — `"output"` for the rendered value, `"input"` for the
|
|
51
|
+
* `onChange` argument.
|
|
52
|
+
*
|
|
53
|
+
* When the schema's value type cannot be statically determined (e.g.
|
|
54
|
+
* a runtime `Record<string, unknown>` JSON Schema, or an OpenAPI doc
|
|
55
|
+
* without a ref), the result falls back to `unknown` so callers can
|
|
56
|
+
* still supply arbitrary values.
|
|
57
|
+
*/
|
|
58
|
+
type InferSchemaValue<T, Ref extends string | undefined, Mode extends FromJSONSchemaMode> = T extends z.ZodType ? Mode extends "input" ? z.input<T> : z.output<T> : T extends {
|
|
59
|
+
openapi: unknown;
|
|
60
|
+
} ? Ref extends string ? ResolveOpenAPIRef<T & Record<string, unknown>, Ref, [], Mode> : unknown : T extends object ? FromJSONSchema<T, Record<string, never>, [], Mode> | (unknown extends FromJSONSchema<T> ? unknown : never) extends infer V ? V : unknown : unknown;
|
|
61
|
+
/**
|
|
62
|
+
* Narrow an inferred value type to the sub-shape at `P`, or return
|
|
63
|
+
* the original value type when `P` is `undefined` (no path supplied).
|
|
64
|
+
*/
|
|
65
|
+
type NarrowAtPath<V, P extends string | undefined> = P extends string ? TypeAtPath<V, P> : V;
|
|
66
|
+
/**
|
|
67
|
+
* Public alias mapping a schema input to the rendered value type.
|
|
68
|
+
* Use to narrow a runtime callback inside the body of an `onChange`
|
|
69
|
+
* handler:
|
|
70
|
+
*
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <SchemaComponent
|
|
73
|
+
* schema={userSchema}
|
|
74
|
+
* onChange={(v) => {
|
|
75
|
+
* const user = v as InferredOutputValue<typeof userSchema>;
|
|
76
|
+
* // ...narrowly typed access on `user`
|
|
77
|
+
* }}
|
|
78
|
+
* />
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* The `onChange` argument is typed `unknown` at the props boundary
|
|
82
|
+
* because the walker propagates `unknown` values through the render
|
|
83
|
+
* pipeline. Narrowing on the consumer side is therefore an explicit
|
|
84
|
+
* step and never a silent contract gap.
|
|
85
|
+
*/
|
|
86
|
+
type InferredOutputValue<T, Ref extends string | undefined = undefined, P extends string | undefined = undefined> = NarrowAtPath<InferSchemaValue<T, Ref, "output">, P>;
|
|
87
|
+
/** Companion to {@link InferredOutputValue} for `"input"`-mode shapes. */
|
|
88
|
+
type InferredInputValue<T, Ref extends string | undefined = undefined, P extends string | undefined = undefined> = NarrowAtPath<InferSchemaValue<T, Ref, "input">, P>;
|
|
89
|
+
interface SchemaComponentProps<T = unknown, Ref extends string | undefined = undefined, P extends string | undefined = undefined> {
|
|
43
90
|
/**
|
|
44
91
|
* Zod schema, JSON Schema object, or OpenAPI document.
|
|
45
92
|
*
|
|
@@ -53,9 +100,51 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
|
|
|
53
100
|
schema: RejectUnrepresentableZod<T>;
|
|
54
101
|
/** For OpenAPI: a ref string like "#/components/schemas/User" or "/users/post". */
|
|
55
102
|
ref?: Ref;
|
|
56
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Optional dot-separated path used purely for type narrowing.
|
|
105
|
+
* When the schema is typed, the path is restricted to the valid
|
|
106
|
+
* dot-paths reachable through the schema's inferred value.
|
|
107
|
+
*
|
|
108
|
+
* RUNTIME CAVEAT: this prop is currently type-level only. The
|
|
109
|
+
* render pipeline still operates on the root schema; sub-path
|
|
110
|
+
* value resolution is provided by the dedicated `<SchemaField>`
|
|
111
|
+
* component, which already implements `resolvePath` /
|
|
112
|
+
* `setNestedValue`. The `path` prop here documents intent at the
|
|
113
|
+
* type level (and prepares the API surface) so consumers can
|
|
114
|
+
* declare narrow typed wrappers without runtime regressions. Use
|
|
115
|
+
* `<SchemaField>` when a sub-path render is required at runtime.
|
|
116
|
+
*/
|
|
117
|
+
path?: P;
|
|
118
|
+
/**
|
|
119
|
+
* Current value to render.
|
|
120
|
+
*
|
|
121
|
+
* TYPE BOUNDARY NOTE: kept as `unknown` at the props boundary so
|
|
122
|
+
* existing call sites — including legitimate edge-case fixtures
|
|
123
|
+
* that pass deliberately invalid values to exercise fallback code
|
|
124
|
+
* paths — continue to typecheck. Use {@link InferredOutputValue}
|
|
125
|
+
* to narrow on the consumer side:
|
|
126
|
+
*
|
|
127
|
+
* ```tsx
|
|
128
|
+
* const user: InferredOutputValue<typeof userSchema> = { ... };
|
|
129
|
+
* <SchemaComponent schema={userSchema} value={user} readOnly />
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* The narrowing is fully expressible through the helper alias
|
|
133
|
+
* without forcing every existing caller to update their value
|
|
134
|
+
* shapes for `exactOptionalPropertyTypes` / enum literal widening.
|
|
135
|
+
*/
|
|
57
136
|
value?: unknown;
|
|
58
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Called when the value changes (editable fields).
|
|
139
|
+
*
|
|
140
|
+
* TYPE BOUNDARY NOTE: the parameter is typed `unknown` rather
|
|
141
|
+
* than the inferred input shape because the walker pipeline only
|
|
142
|
+
* propagates `unknown` values and a narrow contravariant callback
|
|
143
|
+
* signature is not assignable from an `unknown`-emitting source
|
|
144
|
+
* without an unsafe boundary cast. The {@link InferredInputValue}
|
|
145
|
+
* alias is the recommended way for callers to narrow on the
|
|
146
|
+
* consumer side — `onChange={(v) => { const u = v as InferredInputValue<typeof schema>; ... }}`.
|
|
147
|
+
*/
|
|
59
148
|
onChange?: (value: unknown) => void;
|
|
60
149
|
/** Run schema.safeParse() on change and surface errors via onValidationError. */
|
|
61
150
|
validate?: boolean;
|
|
@@ -87,31 +176,7 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
|
|
|
87
176
|
*/
|
|
88
177
|
idPrefix?: string;
|
|
89
178
|
}
|
|
90
|
-
declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined>(
|
|
91
|
-
schema: schemaInput,
|
|
92
|
-
ref: refInput,
|
|
93
|
-
value,
|
|
94
|
-
onChange,
|
|
95
|
-
validate,
|
|
96
|
-
onValidationError,
|
|
97
|
-
onError,
|
|
98
|
-
onDiagnostic,
|
|
99
|
-
strict,
|
|
100
|
-
fields,
|
|
101
|
-
meta: componentMeta,
|
|
102
|
-
readOnly,
|
|
103
|
-
writeOnly,
|
|
104
|
-
description,
|
|
105
|
-
widgets: instanceWidgets,
|
|
106
|
-
idPrefix
|
|
107
|
-
}: SchemaComponentProps<T, Ref>): ReactNode;
|
|
108
|
-
/**
|
|
109
|
-
* Default root-path sentinel used when no `idPrefix` is supplied AND the
|
|
110
|
-
* component is rendered outside a React tree (e.g. server-side bundling
|
|
111
|
-
* test harnesses). Production callers receive a `useId()`-derived prefix
|
|
112
|
-
* that is unique per instance.
|
|
113
|
-
*/
|
|
114
|
-
declare const ROOT_PATH = "root";
|
|
179
|
+
declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined, P extends string | undefined = undefined>(props: SchemaComponentProps<T, Ref, P>): ReactNode;
|
|
115
180
|
/**
|
|
116
181
|
* Append a child path suffix to a parent path. When the suffix is omitted
|
|
117
182
|
* (e.g. transparent wrappers like union options), the parent path is
|
|
@@ -154,7 +219,7 @@ interface SchemaFieldProps<T = unknown, Ref extends string | undefined = undefin
|
|
|
154
219
|
validate?: boolean;
|
|
155
220
|
onValidationError?: (error: unknown) => void;
|
|
156
221
|
}
|
|
157
|
-
declare function SchemaField<T = unknown, Ref extends string | undefined = undefined, P extends string = string>({
|
|
222
|
+
declare function SchemaField<T = unknown, Ref extends string | undefined = undefined, P extends string = PathOfType<InferSchemaType<T>> | (string extends PathOfType<InferSchemaType<T>> ? string : never)>({
|
|
158
223
|
path,
|
|
159
224
|
schema: schemaInput,
|
|
160
225
|
ref: refInput,
|
|
@@ -165,4 +230,4 @@ declare function SchemaField<T = unknown, Ref extends string | undefined = undef
|
|
|
165
230
|
onValidationError
|
|
166
231
|
}: SchemaFieldProps<T, Ref, P>): ReactNode;
|
|
167
232
|
//#endregion
|
|
168
|
-
export {
|
|
233
|
+
export { InferredInputValue, InferredOutputValue, SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, joinPath, registerWidget, renderField, sanitisePrefix };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
|
|
3
|
+
import "../core/limits.mjs";
|
|
3
4
|
import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
4
5
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
5
6
|
import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
@@ -46,7 +47,8 @@ const globalWidgets = /* @__PURE__ */ new Map();
|
|
|
46
47
|
function registerWidget(name, render) {
|
|
47
48
|
globalWidgets.set(name, render);
|
|
48
49
|
}
|
|
49
|
-
function SchemaComponent(
|
|
50
|
+
function SchemaComponent(props) {
|
|
51
|
+
const { schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, onDiagnostic, strict, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets, idPrefix } = props;
|
|
50
52
|
const userResolver = useContext(UserResolverContext);
|
|
51
53
|
const contextWidgets = useContext(WidgetsContext);
|
|
52
54
|
const generatedId = useId();
|
|
@@ -87,7 +89,17 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
|
|
|
87
89
|
}
|
|
88
90
|
const handleChange = useCallback((nextValue) => {
|
|
89
91
|
if (validate) {
|
|
90
|
-
|
|
92
|
+
let error;
|
|
93
|
+
try {
|
|
94
|
+
error = runValidation(zodSchema, jsonSchema, nextValue, onDiagnostic);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const normalised = err instanceof SchemaNormalisationError ? err : new SchemaNormalisationError(err instanceof Error ? err.message : "Fallback validation failed", schemaInput, "zod-conversion-failed", void 0, err);
|
|
97
|
+
if (onError !== void 0) {
|
|
98
|
+
onError(normalised);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
throw normalised;
|
|
102
|
+
}
|
|
91
103
|
if (error !== void 0) {
|
|
92
104
|
onValidationError?.(error);
|
|
93
105
|
dispatchFieldErrors(fields, error);
|
|
@@ -101,7 +113,9 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
|
|
|
101
113
|
onChange,
|
|
102
114
|
onValidationError,
|
|
103
115
|
fields,
|
|
104
|
-
onDiagnostic
|
|
116
|
+
onDiagnostic,
|
|
117
|
+
onError,
|
|
118
|
+
schemaInput
|
|
105
119
|
]);
|
|
106
120
|
const walkOptions = {
|
|
107
121
|
componentMeta: mergedMeta,
|
|
@@ -119,13 +133,6 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
|
|
|
119
133
|
return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, rootPath, instanceWidgets, contextWidgets, 0);
|
|
120
134
|
}
|
|
121
135
|
/**
|
|
122
|
-
* Default root-path sentinel used when no `idPrefix` is supplied AND the
|
|
123
|
-
* component is rendered outside a React tree (e.g. server-side bundling
|
|
124
|
-
* test harnesses). Production callers receive a `useId()`-derived prefix
|
|
125
|
-
* that is unique per instance.
|
|
126
|
-
*/
|
|
127
|
-
const ROOT_PATH = "root";
|
|
128
|
-
/**
|
|
129
136
|
* Append a child path suffix to a parent path. When the suffix is omitted
|
|
130
137
|
* (e.g. transparent wrappers like union options), the parent path is
|
|
131
138
|
* returned unchanged so the child inherits the parent's id.
|
|
@@ -146,6 +153,21 @@ function sanitisePrefix(value) {
|
|
|
146
153
|
if (sanitised.length === 0) throw new Error(`Cannot derive a DOM-safe id prefix from "${value}". Pass an explicit idPrefix prop.`);
|
|
147
154
|
return sanitised;
|
|
148
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Run validation against the supplied value.
|
|
158
|
+
*
|
|
159
|
+
* Returns the validation error (Zod error or equivalent) on failure, or
|
|
160
|
+
* `undefined` when the value is valid OR when the fallback validation
|
|
161
|
+
* path was skipped because a diagnostic sink absorbed the conversion
|
|
162
|
+
* failure.
|
|
163
|
+
*
|
|
164
|
+
* Throws `SchemaNormalisationError` (kind `zod-conversion-failed`) when
|
|
165
|
+
* the JSON-Schema → Zod fallback is taken AND no diagnostic sink is
|
|
166
|
+
* wired up. The project's no-silent-fallback rule requires the failure
|
|
167
|
+
* to surface somewhere — diagnostics if the consumer opted in, an error
|
|
168
|
+
* otherwise — so the caller can route it through `onError` / an error
|
|
169
|
+
* boundary rather than have validation quietly disappear.
|
|
170
|
+
*/
|
|
149
171
|
function runValidation(zodSchema, jsonSchema, value, onDiagnostic) {
|
|
150
172
|
if (zodSchema !== void 0 && isObject(zodSchema)) {
|
|
151
173
|
const safeParseFn = zodSchema.safeParse;
|
|
@@ -159,8 +181,16 @@ function runValidation(zodSchema, jsonSchema, value, onDiagnostic) {
|
|
|
159
181
|
try {
|
|
160
182
|
parsed = z.fromJSONSchema(jsonSchema);
|
|
161
183
|
} catch (err) {
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
if (onDiagnostic !== void 0) {
|
|
185
|
+
onDiagnostic({
|
|
186
|
+
code: "unsupported-type",
|
|
187
|
+
message: `Skipping fallback validation: z.fromJSONSchema could not round-trip the normalised JSON Schema. Original message: ${err instanceof Error ? err.message : "z.fromJSONSchema threw a non-Error value"}`,
|
|
188
|
+
pointer: "",
|
|
189
|
+
detail: { source: "z.fromJSONSchema" }
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
throw new SchemaNormalisationError(`Fallback validation failed: z.fromJSONSchema could not round-trip the normalised JSON Schema. Original message: ${err instanceof Error ? err.message : "z.fromJSONSchema threw a non-Error value"}`, jsonSchema, "zod-conversion-failed", void 0, err);
|
|
164
194
|
}
|
|
165
195
|
if (isObject(parsed)) {
|
|
166
196
|
const safeParseFn = parsed.safeParse;
|
|
@@ -170,39 +200,13 @@ function runValidation(zodSchema, jsonSchema, value, onDiagnostic) {
|
|
|
170
200
|
}
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Emit a diagnostic when `z.fromJSONSchema` refuses to round-trip the
|
|
175
|
-
* already-normalised JSON Schema. The diagnostic reuses the existing
|
|
176
|
-
* `unsupported-type` code because the failure mode is the same — a
|
|
177
|
-
* keyword/structure Zod cannot represent — and the consumer should be
|
|
178
|
-
* able to opt in to noticing it via the existing diagnostics channel.
|
|
179
|
-
*
|
|
180
|
-
* Best-effort: when no `onDiagnostic` sink is configured we still swallow
|
|
181
|
-
* the throw (the alternative would crash the React render for a
|
|
182
|
-
* non-essential validation step), matching the silent-fallback contract
|
|
183
|
-
* the rest of the diagnostics system uses.
|
|
184
|
-
*/
|
|
185
|
-
function emitFromJsonSchemaDiagnostic(err, onDiagnostic) {
|
|
186
|
-
if (onDiagnostic === void 0) return;
|
|
187
|
-
onDiagnostic({
|
|
188
|
-
code: "unsupported-type",
|
|
189
|
-
message: `Skipping fallback validation: z.fromJSONSchema could not round-trip the normalised JSON Schema. Original message: ${err instanceof Error ? err.message : "z.fromJSONSchema threw a non-Error value"}`,
|
|
190
|
-
pointer: "",
|
|
191
|
-
detail: { source: "z.fromJSONSchema" }
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
/** Maximum rendering depth before treating a field as recursive. */
|
|
195
|
-
const MAX_RENDER_DEPTH = 10;
|
|
196
203
|
function renderField(tree, value, onChange, userResolver, renderChild, path, instanceWidgets, contextWidgets, depth = 0) {
|
|
197
|
-
if (path.length === 0) throw new Error("renderField requires a non-empty path. Pass
|
|
198
|
-
if (depth >=
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
" (recursive)"
|
|
204
|
-
] }) });
|
|
205
|
-
}
|
|
204
|
+
if (path.length === 0) throw new Error("renderField requires a non-empty path. Pass the root path (derived from `idPrefix` or `useId()`) for the root field, and use renderChild's pathSuffix to derive child paths.");
|
|
205
|
+
if (depth >= 10) return /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsxs("em", { children: [
|
|
206
|
+
"↻ ",
|
|
207
|
+
typeof tree.meta.description === "string" ? tree.meta.description : "schema",
|
|
208
|
+
" (recursive)"
|
|
209
|
+
] }) });
|
|
206
210
|
const componentHint = tree.meta.component;
|
|
207
211
|
if (typeof componentHint === "string") {
|
|
208
212
|
const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
|
|
@@ -283,6 +287,16 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
|
|
|
283
287
|
/**
|
|
284
288
|
* Dispatch Zod errors to per-field onValidationError callbacks.
|
|
285
289
|
* Walks the fields override tree and matches errors by path prefix.
|
|
290
|
+
*
|
|
291
|
+
* The `fields` parameter mirrors the runtime shape produced by
|
|
292
|
+
* `InferFields<T, Ref>` at the props boundary — either a typed
|
|
293
|
+
* `FieldOverrides<...>` tree (for narrowable schemas) or the loose
|
|
294
|
+
* `Record<string, FieldOverride>` fallback. Both reduce to the same
|
|
295
|
+
* runtime shape, and the runtime narrowing below
|
|
296
|
+
* (`toRecordOrUndefined`) handles `undefined` and non-object inputs
|
|
297
|
+
* defensively. Typing the parameter as the union — rather than the
|
|
298
|
+
* `unknown` that earlier revisions used — keeps the type contract
|
|
299
|
+
* visible to readers without changing runtime behaviour.
|
|
286
300
|
*/
|
|
287
301
|
function dispatchFieldErrors(fields, error) {
|
|
288
302
|
if (fields === void 0 || !isObject(error)) return;
|
|
@@ -314,4 +328,4 @@ function isCallable(value) {
|
|
|
314
328
|
return typeof value === "function";
|
|
315
329
|
}
|
|
316
330
|
//#endregion
|
|
317
|
-
export {
|
|
331
|
+
export { SchemaComponent, SchemaField, SchemaProvider, joinPath, registerWidget, renderField, sanitisePrefix };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as Diagnostic } from "../diagnostics-
|
|
3
|
-
import { r as ComponentResolver } from "../renderer-
|
|
1
|
+
import { w as SchemaMeta } from "../types-BrRMV0en.mjs";
|
|
2
|
+
import { t as Diagnostic } from "../diagnostics-D0QCYGv0.mjs";
|
|
3
|
+
import { r as ComponentResolver } from "../renderer-BaRlQIuN.mjs";
|
|
4
4
|
import { WidgetMap } from "./SchemaComponent.mjs";
|
|
5
5
|
import { ReactNode } from "react";
|
|
6
6
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "../core/limits.mjs";
|
|
1
2
|
import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
2
3
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
3
4
|
import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
@@ -74,10 +75,9 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
|
|
|
74
75
|
};
|
|
75
76
|
const tree = walk(jsonSchema, walkOptions);
|
|
76
77
|
const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
|
|
77
|
-
const MAX_SERVER_DEPTH = 10;
|
|
78
78
|
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, pathSuffix) => {
|
|
79
79
|
const childPath = joinPath(parentPath, pathSuffix);
|
|
80
|
-
if (currentDepth >=
|
|
80
|
+
if (currentDepth >= 10) return createElement("fieldset", null, createElement("em", null, `\u21bb ${typeof childTree.meta.description === "string" ? childTree.meta.description : "schema"} (recursive)`));
|
|
81
81
|
return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, widgets);
|
|
82
82
|
};
|
|
83
83
|
const renderChild = makeRenderChild(0, rootPath);
|
package/dist/react/headless.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord,
|
|
1
|
+
import { renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderString, renderTuple, renderUnion, renderUnknown } from "./headlessRenderers.mjs";
|
|
2
2
|
//#region src/react/headless.tsx
|
|
3
3
|
/**
|
|
4
4
|
* The headless resolver uses props.renderChild for recursive rendering.
|
|
@@ -27,7 +27,6 @@ const headlessResolver = {
|
|
|
27
27
|
negation: renderNegation,
|
|
28
28
|
literal: renderLiteral,
|
|
29
29
|
file: renderFile,
|
|
30
|
-
recursive: renderRecursive,
|
|
31
30
|
never: renderNever,
|
|
32
31
|
unknown: renderUnknown
|
|
33
32
|
};
|