schema-components 1.17.0 → 1.18.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.
Files changed (48) hide show
  1. package/dist/core/adapter.d.mts +1 -1
  2. package/dist/core/constraints.d.mts +1 -1
  3. package/dist/core/diagnostics.d.mts +1 -1
  4. package/dist/core/merge.d.mts +14 -8
  5. package/dist/core/merge.mjs +81 -12
  6. package/dist/core/normalise.d.mts +1 -1
  7. package/dist/core/ref.d.mts +1 -1
  8. package/dist/core/renderer.d.mts +2 -2
  9. package/dist/core/renderer.mjs +50 -1
  10. package/dist/core/swagger2.d.mts +1 -1
  11. package/dist/core/typeInference.d.mts +2 -2
  12. package/dist/core/walkBuilders.d.mts +2 -2
  13. package/dist/core/walker.mjs +2 -2
  14. package/dist/{diagnostics-DzbZmcLI.d.mts → diagnostics-BYk63jsC.d.mts} +1 -1
  15. package/dist/html/a11y.d.mts +13 -2
  16. package/dist/html/a11y.mjs +26 -2
  17. package/dist/html/renderToHtml.d.mts +1 -1
  18. package/dist/html/renderToHtml.mjs +5 -3
  19. package/dist/html/renderToHtmlStream.d.mts +1 -1
  20. package/dist/html/renderers.d.mts +4 -3
  21. package/dist/html/renderers.mjs +9 -13
  22. package/dist/html/streamRenderers.d.mts +1 -1
  23. package/dist/openapi/bundle.d.mts +9 -4
  24. package/dist/openapi/bundle.mjs +73 -15
  25. package/dist/openapi/components.d.mts +1 -1
  26. package/dist/openapi/components.mjs +22 -17
  27. package/dist/openapi/parser.mjs +8 -8
  28. package/dist/openapi/resolve.d.mts +13 -2
  29. package/dist/openapi/resolve.mjs +19 -3
  30. package/dist/react/SchemaComponent.d.mts +4 -4
  31. package/dist/react/SchemaComponent.mjs +2 -31
  32. package/dist/react/SchemaView.d.mts +4 -3
  33. package/dist/react/SchemaView.mjs +13 -44
  34. package/dist/react/headless.d.mts +1 -1
  35. package/dist/react/headlessRenderers.d.mts +1 -1
  36. package/dist/react/headlessRenderers.mjs +3 -2
  37. package/dist/{ref-DvWoULcy.d.mts → ref-Ckt5liZs.d.mts} +1 -1
  38. package/dist/{renderer-B3s8o2B8.d.mts → renderer-DXo-rXHJ.d.mts} +18 -1
  39. package/dist/themes/mantine.d.mts +1 -1
  40. package/dist/themes/mantine.mjs +4 -3
  41. package/dist/themes/mui.d.mts +1 -1
  42. package/dist/themes/mui.mjs +6 -5
  43. package/dist/themes/radix.d.mts +1 -1
  44. package/dist/themes/radix.mjs +3 -3
  45. package/dist/themes/shadcn.d.mts +1 -1
  46. package/dist/themes/shadcn.mjs +6 -5
  47. package/dist/{typeInference-k7FXfTVO.d.mts → typeInference-5JiqIZ8t.d.mts} +57 -4
  48. package/package.json +1 -1
@@ -29,28 +29,38 @@ import { isObject } from "../core/guards.mjs";
29
29
  *
30
30
  * Walks every $ref in the document. For external refs (not starting with `#`),
31
31
  * calls the resolver to fetch the external document, extracts the referenced
32
- * schema, inlines it into `components.schemas` with a synthesised name, and
33
- * rewrites the $ref to point to the inlined copy.
32
+ * schema, inlines it into `components.schemas` under a synthesised name, and
33
+ * rewrites the original $ref to point at the new internal location
34
+ * (`#/components/schemas/<name>`).
35
+ *
36
+ * Identical external refs share a single entry — the second occurrence of
37
+ * the same `(uri, fragment)` pair reuses the name produced for the first.
38
+ * Name collisions between different refs are resolved by suffixing a counter.
34
39
  *
35
40
  * The resolver is called once per unique URI and the result is cached.
36
41
  *
37
- * Returns a deep-cloned document with all external refs resolved.
38
- * The original document is never mutated.
42
+ * Returns a deep-cloned document with all external refs replaced by internal
43
+ * refs. The original document is never mutated.
39
44
  */
40
45
  async function bundleOpenApiDoc(doc, resolver) {
41
46
  const result = structuredClone(doc);
42
47
  const uriCache = /* @__PURE__ */ new Map();
48
+ const inlineCache = /* @__PURE__ */ new Map();
43
49
  if (!isObject(result.components)) result.components = {};
44
- if (!isObject(result.components)) result.components = {};
45
- if (isObject(result.components) && !isObject(result.components.schemas)) result.components.schemas = {};
46
- await walkAndInline(result, uriCache, resolver);
50
+ const components = result.components;
51
+ if (!isObject(components)) throw new Error("bundleOpenApiDoc: components is not an object");
52
+ if (!isObject(components.schemas)) components.schemas = {};
53
+ const schemasNode = components.schemas;
54
+ if (!isObject(schemasNode)) throw new Error("bundleOpenApiDoc: components.schemas is not an object");
55
+ await walkAndInline(result, schemasNode, uriCache, inlineCache, resolver);
47
56
  return result;
48
57
  }
49
58
  /**
50
59
  * Walk a document tree, find external $ref strings, resolve them,
51
- * inline the targets, and rewrite the refs.
60
+ * inline the targets into `components.schemas`, and rewrite each $ref
61
+ * to point at the new internal location.
52
62
  */
53
- async function walkAndInline(node, uriCache, resolver) {
63
+ async function walkAndInline(node, schemasNode, uriCache, inlineCache, resolver) {
54
64
  if (!isObject(node)) return;
55
65
  if (typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
56
66
  const ref = node.$ref;
@@ -66,15 +76,21 @@ async function walkAndInline(node, uriCache, resolver) {
66
76
  }
67
77
  }
68
78
  if (externalDoc !== void 0) {
69
- const target = resolveFragment(externalDoc, fragment);
70
- if (isObject(target)) {
71
- delete node.$ref;
72
- for (const [key, value] of Object.entries(target)) node[key] = value;
79
+ const cacheKey = `${uri}${fragment}`;
80
+ let inlinedName = inlineCache.get(cacheKey);
81
+ if (inlinedName === void 0) {
82
+ const target = resolveFragment(externalDoc, fragment);
83
+ if (isObject(target)) {
84
+ inlinedName = registerInline(schemasNode, uri, fragment, target);
85
+ inlineCache.set(cacheKey, inlinedName);
86
+ await walkAndInline(schemasNode[inlinedName], schemasNode, uriCache, inlineCache, resolver);
87
+ }
73
88
  }
89
+ if (inlinedName !== void 0) node.$ref = `#/components/schemas/${inlinedName}`;
74
90
  }
75
91
  }
76
- for (const value of Object.values(node)) if (isObject(value)) await walkAndInline(value, uriCache, resolver);
77
- else if (Array.isArray(value)) for (const item of value) await walkAndInline(item, uriCache, resolver);
92
+ for (const value of Object.values(node)) if (isObject(value)) await walkAndInline(value, schemasNode, uriCache, inlineCache, resolver);
93
+ else if (Array.isArray(value)) for (const item of value) await walkAndInline(item, schemasNode, uriCache, inlineCache, resolver);
78
94
  }
79
95
  /**
80
96
  * Resolve a JSON Pointer fragment within a document.
@@ -91,5 +107,47 @@ function resolveFragment(doc, fragment) {
91
107
  }
92
108
  return isObject(current) ? current : void 0;
93
109
  }
110
+ /**
111
+ * Derive a candidate identifier for an inlined external schema. Prefers
112
+ * the last meaningful segment of the JSON Pointer fragment; falls back
113
+ * to the URI's filename (sans extension), then to a generic prefix.
114
+ */
115
+ function deriveCandidateName(uri, fragment) {
116
+ if (fragment.startsWith("#/")) {
117
+ const last = fragment.slice(2).split("/").at(-1);
118
+ if (last !== void 0 && last.length > 0) return sanitiseName(last);
119
+ }
120
+ const pathOnly = uri.split(/[?#]/)[0] ?? uri;
121
+ const lastSlash = pathOnly.lastIndexOf("/");
122
+ const filename = lastSlash >= 0 ? pathOnly.slice(lastSlash + 1) : pathOnly;
123
+ const dot = filename.lastIndexOf(".");
124
+ const stem = dot > 0 ? filename.slice(0, dot) : filename;
125
+ if (stem.length > 0) return sanitiseName(stem);
126
+ return "ExternalSchema";
127
+ }
128
+ /**
129
+ * Sanitise a string into a JSON Pointer-safe identifier: alphanumerics
130
+ * and underscores only. An empty result falls back to "Schema".
131
+ */
132
+ function sanitiseName(raw) {
133
+ const cleaned = raw.replace(/[^A-Za-z0-9_]/g, "_");
134
+ return cleaned.length > 0 ? cleaned : "Schema";
135
+ }
136
+ /**
137
+ * Place the resolved target into `components.schemas` under a unique
138
+ * name derived from the ref, and return the chosen name. Collisions
139
+ * with existing entries are resolved by suffixing a counter.
140
+ */
141
+ function registerInline(schemasNode, uri, fragment, target) {
142
+ const base = deriveCandidateName(uri, fragment);
143
+ let name = base;
144
+ let counter = 2;
145
+ while (name in schemasNode) {
146
+ name = `${base}_${String(counter)}`;
147
+ counter++;
148
+ }
149
+ schemasNode[name] = structuredClone(target);
150
+ return name;
151
+ }
94
152
  //#endregion
95
153
  export { bundleOpenApiDoc };
@@ -1,5 +1,5 @@
1
1
  import { T as SchemaMeta, u as FieldOverride } from "../types-D_5ST7SS.mjs";
2
- import { i as InferResponseFields, n as InferParameterOverrides, r as InferRequestBodyFields, u as UnsafeFields } from "../typeInference-k7FXfTVO.mjs";
2
+ import { a as InferResponseFields, d as UnsafeFields, i as InferRequestBodyFields, r as InferParameterOverrides } from "../typeInference-5JiqIZ8t.mjs";
3
3
  import { WidgetMap } from "../react/SchemaComponent.mjs";
4
4
  import { ReactNode } from "react";
5
5
 
@@ -1,16 +1,14 @@
1
- import { toRecordOrUndefined } from "../core/guards.mjs";
2
- import { SchemaNormalisationError } from "../core/errors.mjs";
3
- import { normaliseSchema } from "../core/adapter.mjs";
1
+ import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
4
2
  import { walk } from "../core/walker.mjs";
3
+ import { joinPath, renderField, sanitisePrefix } from "../react/SchemaComponent.mjs";
5
4
  import { ApiCallbacks } from "./ApiCallbacks.mjs";
6
5
  import { ApiLinks } from "./ApiLinks.mjs";
7
6
  import { ApiResponseHeaders } from "./ApiResponseHeaders.mjs";
8
7
  import { ApiSecurity } from "./ApiSecurity.mjs";
9
8
  import { getLinks, getSecurityRequirements, getSecuritySchemes, listCallbacks } from "./parser.mjs";
10
- import { joinPath, renderField, sanitisePrefix } from "../react/SchemaComponent.mjs";
11
9
  import { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
12
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
10
  import { useId } from "react";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
12
  //#region src/openapi/components.tsx
15
13
  /**
16
14
  * OpenAPI React components with type-safe generics.
@@ -27,25 +25,17 @@ import { useId } from "react";
27
25
  */
28
26
  function noop() {}
29
27
  function renderSchema(schema, rootDocument, options) {
30
- let jsonSchema;
31
- let rootMeta;
32
- try {
33
- const normalised = normaliseSchema(schema);
34
- jsonSchema = normalised.jsonSchema;
35
- rootMeta = normalised.rootMeta;
36
- } catch (err) {
37
- throw new SchemaNormalisationError(err instanceof Error ? err.message : "Failed to normalise schema", schema, "unknown");
38
- }
28
+ if (!isObject(schema)) throw new Error("renderSchema received a non-object schema from the resolver.");
29
+ const rootMeta = extractRootMetaFromSchema(schema);
39
30
  const componentMeta = {};
40
31
  if (options.readOnly === true) componentMeta.readOnly = true;
41
32
  if (options.meta !== void 0) for (const [k, v] of Object.entries(options.meta)) componentMeta[k] = v;
42
- const walkOpts = {
33
+ const tree = walk(schema, {
43
34
  componentMeta,
44
35
  rootMeta,
45
36
  fieldOverrides: toRecordOrUndefined(options.fields),
46
37
  rootDocument
47
- };
48
- const tree = walk(jsonSchema, walkOpts);
38
+ });
49
39
  const makeRenderChild = (parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
50
40
  const childPath = joinPath(parentPath, pathSuffix);
51
41
  return renderField(childTree, childValue, childOnChange, void 0, makeRenderChild(childPath), childPath, options.widgets);
@@ -262,5 +252,20 @@ function buildParamMeta(param, overrides, meta) {
262
252
  if (meta !== void 0) for (const [k, v] of Object.entries(meta)) result[k] = v;
263
253
  return Object.keys(result).length > 0 ? result : void 0;
264
254
  }
255
+ /**
256
+ * Extract root-level meta (title, description, readOnly, etc.) from a
257
+ * JSON Schema node. Mirrors `extractRootMetaFromJson` in the adapter so
258
+ * pre-normalised schemas (extracted from `getParsed`) still surface root
259
+ * meta to the walker without an extra adapter round-trip.
260
+ */
261
+ function extractRootMetaFromSchema(jsonSchema) {
262
+ const meta = {};
263
+ if (jsonSchema.readOnly === true) meta.readOnly = true;
264
+ if (jsonSchema.writeOnly === true) meta.writeOnly = true;
265
+ if (typeof jsonSchema.description === "string") meta.description = jsonSchema.description;
266
+ if (typeof jsonSchema.title === "string") meta.title = jsonSchema.title;
267
+ if (typeof jsonSchema.deprecated === "boolean") meta.deprecated = jsonSchema.deprecated;
268
+ return Object.keys(meta).length > 0 ? meta : void 0;
269
+ }
265
270
  //#endregion
266
271
  export { ApiOperation, ApiParameters, ApiRequestBody, ApiResponse };
@@ -79,22 +79,22 @@ function getParameters(parsed, path, method) {
79
79
  if (pathItem === void 0) return [];
80
80
  const operation = getProperty(pathItem, method);
81
81
  if (!isObject(operation)) return [];
82
- const pathParams = extractParameterList(getProperty(pathItem, "parameters"));
83
- const opParams = extractParameterList(getProperty(operation, "parameters"));
82
+ const pathParams = extractParameterList(parsed.doc, getProperty(pathItem, "parameters"));
83
+ const opParams = extractParameterList(parsed.doc, getProperty(operation, "parameters"));
84
84
  const map = /* @__PURE__ */ new Map();
85
85
  for (const param of pathParams) map.set(`${param.name}:${param.location}`, param);
86
86
  for (const param of opParams) map.set(`${param.name}:${param.location}`, param);
87
87
  return [...map.values()];
88
88
  }
89
- function extractParameterList(parameters) {
89
+ function extractParameterList(doc, parameters) {
90
90
  if (!Array.isArray(parameters)) return [];
91
91
  const result = [];
92
92
  for (const param of parameters) {
93
93
  if (!isObject(param)) continue;
94
- const name = getProperty(param, "name");
95
- const location = getProperty(param, "in");
94
+ const resolved = resolveParam(doc, param);
95
+ const name = getProperty(resolved, "name");
96
+ const location = getProperty(resolved, "in");
96
97
  if (typeof name !== "string" || typeof location !== "string") continue;
97
- const resolved = resolveParam(param);
98
98
  const schema = getProperty(resolved, "schema");
99
99
  result.push({
100
100
  name,
@@ -107,10 +107,10 @@ function extractParameterList(parameters) {
107
107
  }
108
108
  return result;
109
109
  }
110
- function resolveParam(param) {
110
+ function resolveParam(doc, param) {
111
111
  const ref = getProperty(param, "$ref");
112
112
  if (typeof ref === "string" && ref.startsWith("#/")) {
113
- const resolved = resolveRefInDoc(param, ref);
113
+ const resolved = resolveRefInDoc(doc, ref);
114
114
  if (resolved !== void 0) return resolved;
115
115
  }
116
116
  return param;
@@ -2,8 +2,19 @@ import { OpenApiDocument, OperationInfo, ParameterInfo, ResponseInfo, getRequest
2
2
 
3
3
  //#region src/openapi/resolve.d.ts
4
4
  /**
5
- * Parse and cache an OpenAPI document. Returns cached version if
6
- * the same object identity has been seen before.
5
+ * Parse and cache an OpenAPI document. Returns the cached parse for the
6
+ * same object identity.
7
+ *
8
+ * Before parsing, the document is run through the version-aware
9
+ * normalisation pipeline (`normaliseOpenApiSchemas`) so OpenAPI 3.0.x
10
+ * keywords (`nullable`, `discriminator`, `example`) and Swagger 2.0
11
+ * documents are converted to canonical Draft 2020-12 form. The parser
12
+ * and downstream extractors (`getRequestBody`, `getResponses`, etc.) then
13
+ * observe schemas in the same form `<SchemaComponent>` does, keeping the
14
+ * OpenAPI components on the same pipeline as the top-level adapter.
15
+ *
16
+ * The cache is keyed by the caller-supplied document so subsequent calls
17
+ * with the same input bypass both normalisation and parsing.
7
18
  */
8
19
  declare function getParsed(doc: Record<string, unknown>): OpenApiDocument;
9
20
  /**
@@ -1,4 +1,6 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
+ import { detectOpenApiVersion } from "../core/version.mjs";
3
+ import { i as normaliseOpenApiSchemas } from "../normalise-tL9FckAk.mjs";
2
4
  import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
3
5
  //#region src/openapi/resolve.ts
4
6
  /**
@@ -10,14 +12,28 @@ import { getParameters, getRequestBody, getResponses, listOperations, parseOpenA
10
12
  */
11
13
  const docCache = /* @__PURE__ */ new WeakMap();
12
14
  /**
13
- * Parse and cache an OpenAPI document. Returns cached version if
14
- * the same object identity has been seen before.
15
+ * Parse and cache an OpenAPI document. Returns the cached parse for the
16
+ * same object identity.
17
+ *
18
+ * Before parsing, the document is run through the version-aware
19
+ * normalisation pipeline (`normaliseOpenApiSchemas`) so OpenAPI 3.0.x
20
+ * keywords (`nullable`, `discriminator`, `example`) and Swagger 2.0
21
+ * documents are converted to canonical Draft 2020-12 form. The parser
22
+ * and downstream extractors (`getRequestBody`, `getResponses`, etc.) then
23
+ * observe schemas in the same form `<SchemaComponent>` does, keeping the
24
+ * OpenAPI components on the same pipeline as the top-level adapter.
25
+ *
26
+ * The cache is keyed by the caller-supplied document so subsequent calls
27
+ * with the same input bypass both normalisation and parsing.
15
28
  */
16
29
  function getParsed(doc) {
17
30
  const cached = docCache.get(doc);
18
31
  if (cached !== void 0) return cached;
19
- const parsed = parseOpenApiDocument(doc);
32
+ const version = detectOpenApiVersion(doc);
33
+ const normalisedDoc = version !== void 0 ? normaliseOpenApiSchemas(doc, version) : doc;
34
+ const parsed = parseOpenApiDocument(normalisedDoc);
20
35
  docCache.set(doc, parsed);
36
+ if (normalisedDoc !== doc) docCache.set(normalisedDoc, parsed);
21
37
  return parsed;
22
38
  }
23
39
  /**
@@ -1,11 +1,11 @@
1
1
  import { M as WalkedField, T as SchemaMeta, d as FieldOverrides, u as FieldOverride } from "../types-D_5ST7SS.mjs";
2
- import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
2
+ import { t as Diagnostic } from "../diagnostics-BYk63jsC.mjs";
3
3
  import { t as SchemaError } from "../errors-C5zRC2PU.mjs";
4
- import { l as RenderProps, r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
5
- import { c as ResolveOpenAPIRef, s as PathOfType, t as FromJSONSchema } from "../typeInference-k7FXfTVO.mjs";
4
+ import { l as RenderProps, r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
5
+ import { c as PathOfType, l as ResolveOpenAPIRef, n as FromJSONSchema } from "../typeInference-5JiqIZ8t.mjs";
6
6
  import { z } from "zod";
7
- import * as _$react_jsx_runtime0 from "react/jsx-runtime";
8
7
  import { ReactNode } from "react";
8
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
9
9
 
10
10
  //#region src/react/SchemaComponent.d.ts
11
11
  /**
@@ -2,13 +2,13 @@
2
2
  import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
4
4
  import { normaliseSchema } from "../core/adapter.mjs";
5
- import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
5
+ import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
6
6
  import { walk } from "../core/walker.mjs";
7
7
  import { headlessResolver } from "./headless.mjs";
8
8
  import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
9
9
  import { z } from "zod";
10
- import { jsx, jsxs } from "react/jsx-runtime";
11
10
  import { createContext, isValidElement, useCallback, useContext, useId, useMemo } from "react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
12
  //#region src/react/SchemaComponent.tsx
13
13
  /**
14
14
  * <SchemaComponent> — renders UI from Zod, JSON Schema, or OpenAPI schemas.
@@ -203,35 +203,6 @@ function renderField(tree, value, onChange, userResolver, renderChild, path, ins
203
203
  if (value === void 0 || value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
204
204
  return /* @__PURE__ */ jsx("span", { children: typeof value === "string" ? value : JSON.stringify(value) });
205
205
  }
206
- function buildRenderProps(tree, value, onChange, renderChild, path) {
207
- const props = {
208
- value,
209
- onChange,
210
- readOnly: tree.editability === "presentation",
211
- writeOnly: tree.editability === "input",
212
- meta: tree.meta,
213
- constraints: tree.constraints,
214
- path,
215
- tree,
216
- renderChild
217
- };
218
- if (tree.type === "enum") props.enumValues = tree.enumValues;
219
- if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
220
- if (tree.type === "object") props.fields = tree.fields;
221
- if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
222
- if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
223
- if (tree.type === "record") props.keyType = tree.keyType;
224
- if (tree.type === "record") props.valueType = tree.valueType;
225
- if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
226
- if (tree.type === "conditional") props.ifClause = tree.ifClause;
227
- if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
228
- if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
229
- if (tree.type === "negation") props.negated = tree.negated;
230
- if (tree.type === "recursive") props.refTarget = tree.refTarget;
231
- if (tree.type === "literal") props.literalValues = tree.literalValues;
232
- if (tree.examples !== void 0) props.examples = tree.examples;
233
- return props;
234
- }
235
206
  function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
236
207
  const userResolver = useContext(UserResolverContext);
237
208
  const contextWidgets = useContext(WidgetsContext);
@@ -1,6 +1,6 @@
1
1
  import { T as SchemaMeta } from "../types-D_5ST7SS.mjs";
2
- import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
3
- import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
2
+ import { t as Diagnostic } from "../diagnostics-BYk63jsC.mjs";
3
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
4
4
  import { WidgetMap } from "./SchemaComponent.mjs";
5
5
  import { ReactNode } from "react";
6
6
 
@@ -38,7 +38,8 @@ interface SchemaViewProps {
38
38
  idPrefix?: string;
39
39
  }
40
40
  /**
41
- * Server-safe schema renderer — no hooks, no context, no state.
41
+ * Server-safe schema renderer — no context and no state. The only hook
42
+ * called is `useId()`, which is RSC-safe.
42
43
  *
43
44
  * Always renders in read-only mode. For editable forms, use
44
45
  * `<SchemaComponent>` with `"use client"`.
@@ -1,18 +1,21 @@
1
1
  import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
2
2
  import { normaliseSchema } from "../core/adapter.mjs";
3
- import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
3
+ import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
4
4
  import { walk } from "../core/walker.mjs";
5
5
  import { headlessResolver } from "./headless.mjs";
6
6
  import { joinPath, sanitisePrefix } from "./SchemaComponent.mjs";
7
- import { jsx } from "react/jsx-runtime";
8
7
  import { createElement, isValidElement, useId } from "react";
8
+ import { jsx } from "react/jsx-runtime";
9
9
  //#region src/react/SchemaView.tsx
10
10
  /**
11
11
  * React Server Component for read-only schema rendering.
12
12
  *
13
- * This component has zero hooks — no `useContext`, no `useMemo`,
14
- * no `useCallback`. It can run in a React Server Component environment
15
- * without the `"use client"` directive.
13
+ * Uses no React state, context, or effects — no `useContext`, `useMemo`,
14
+ * `useCallback`, `useState`, or `useEffect`. The single hook called is
15
+ * `useId()`, which is one of the few hooks permitted inside a React
16
+ * Server Component (it is RSC-safe by design) and is used solely to
17
+ * derive a stable per-instance `idPrefix`. The component therefore runs
18
+ * in an RSC environment without the `"use client"` directive.
16
19
  *
17
20
  * **Read-only only.** For interactive forms with `onChange`, use
18
21
  * `<SchemaComponent>` (which requires `"use client"`).
@@ -31,9 +34,9 @@ import { createElement, isValidElement, useId } from "react";
31
34
  * Server Components cannot use React context, so the resolver
32
35
  * is passed explicitly.
33
36
  */
34
- function noop() {}
35
37
  /**
36
- * Server-safe schema renderer — no hooks, no context, no state.
38
+ * Server-safe schema renderer — no context and no state. The only hook
39
+ * called is `useId()`, which is RSC-safe.
37
40
  *
38
41
  * Always renders in read-only mode. For editable forms, use
39
42
  * `<SchemaComponent>` with `"use client"`.
@@ -81,22 +84,12 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
81
84
  }
82
85
  function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
83
86
  if (path.length === 0) throw new Error("renderFieldServer requires a non-empty path. Pass ROOT_PATH at the root and join children via joinPath().");
87
+ const adaptedRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
84
88
  const componentHint = tree.meta.component;
85
89
  if (typeof componentHint === "string") {
86
90
  const widget = widgets?.get(componentHint);
87
91
  if (widget !== void 0) {
88
- const wrapRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
89
- const result = widget({
90
- value,
91
- onChange: noop,
92
- readOnly: true,
93
- writeOnly: false,
94
- meta: tree.meta,
95
- constraints: tree.constraints,
96
- path,
97
- tree,
98
- renderChild: wrapRenderChild
99
- });
92
+ const result = widget(buildRenderProps(tree, value, void 0, adaptedRenderChild, path));
100
93
  if (result !== void 0 && result !== null) {
101
94
  if (isValidElement(result)) return result;
102
95
  if (typeof result === "string" || typeof result === "number") return result;
@@ -105,31 +98,7 @@ function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
105
98
  }
106
99
  const renderFn = getRenderFunction(tree.type, resolver);
107
100
  if (renderFn !== void 0) {
108
- const props = {
109
- value,
110
- onChange: noop,
111
- readOnly: true,
112
- writeOnly: false,
113
- meta: tree.meta,
114
- constraints: tree.constraints,
115
- path,
116
- tree,
117
- renderChild: (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix)
118
- };
119
- if (tree.type === "enum") props.enumValues = tree.enumValues;
120
- if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
121
- if (tree.type === "object") props.fields = tree.fields;
122
- if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
123
- if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
124
- if (tree.type === "record") props.keyType = tree.keyType;
125
- if (tree.type === "record") props.valueType = tree.valueType;
126
- if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
127
- if (tree.type === "conditional") props.ifClause = tree.ifClause;
128
- if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
129
- if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
130
- if (tree.type === "negation") props.negated = tree.negated;
131
- if (tree.type === "recursive") props.refTarget = tree.refTarget;
132
- if (tree.type === "literal") props.literalValues = tree.literalValues;
101
+ const props = buildRenderProps(tree, value, void 0, adaptedRenderChild, path);
133
102
  try {
134
103
  const result = renderFn(props);
135
104
  if (result !== void 0 && result !== null) {
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
1
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
2
2
 
3
3
  //#region src/react/headless.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { l as RenderProps } from "../renderer-B3s8o2B8.mjs";
2
+ import { l as RenderProps } from "../renderer-DXo-rXHJ.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/react/headlessRenderers.d.ts
@@ -1,6 +1,6 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
2
  import { isValidElement, useCallback, useEffect, useRef } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/react/headlessRenderers.tsx
5
5
  /**
6
6
  * Headless renderer functions — one per schema type.
@@ -496,7 +496,7 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
496
496
  }, [
497
497
  optionLabels,
498
498
  discKey,
499
- props
499
+ props.onChange
500
500
  ]);
501
501
  const wrapIndex = useCallback((index) => (index % options.length + options.length) % options.length, [options.length]);
502
502
  const handleKeyDown = useCallback((e) => {
@@ -537,6 +537,7 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
537
537
  },
538
538
  type: "button",
539
539
  role: "tab",
540
+ id: `${panelId}-tab-${String(i)}`,
540
541
  "aria-selected": i === activeIndex ? "true" : void 0,
541
542
  "aria-controls": `${panelId}-panel`,
542
543
  tabIndex: i === activeIndex ? 0 : -1,
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "./diagnostics-DzbZmcLI.mjs";
1
+ import { i as DiagnosticsOptions } from "./diagnostics-BYk63jsC.mjs";
2
2
 
3
3
  //#region src/core/ref.d.ts
4
4
  /**
@@ -99,6 +99,23 @@ interface HtmlRenderProps extends BaseFieldProps {
99
99
  */
100
100
  renderChild: (tree: WalkedField, value: unknown, pathSuffix?: string) => string;
101
101
  }
102
+ /**
103
+ * Build the `RenderProps` object handed to a resolver render function or a
104
+ * widget. Used by both the server-side `<SchemaView>` (which has no
105
+ * `onChange`) and the client-side `<SchemaComponent>` (which threads an
106
+ * `onChange` callback).
107
+ *
108
+ * When `onChange` is `undefined` the caller is rendering in read-only mode:
109
+ * a noop `onChange` is wired up, `readOnly` is forced to `true`, and
110
+ * `writeOnly` is forced to `false`. Otherwise the editability is taken
111
+ * from `tree.editability`.
112
+ *
113
+ * The duplicate sibling fields (`enumValues`, `element`, `fields`, etc.)
114
+ * are populated for backwards compatibility with renderers that have not
115
+ * yet migrated to reading from `tree` directly. New renderers should
116
+ * narrow on `tree.type` and read from `tree`.
117
+ */
118
+ declare function buildRenderProps(tree: WalkedField, value: unknown, onChange: ((next: unknown) => void) | undefined, renderChild: RenderProps["renderChild"], path: string): RenderProps;
102
119
  type RenderFunction = (props: RenderProps) => unknown;
103
120
  interface ComponentResolver {
104
121
  string?: RenderFunction;
@@ -166,4 +183,4 @@ declare function mergeResolvers(user: ComponentResolver, fallback: ComponentReso
166
183
  */
167
184
  declare function mergeHtmlResolvers(user: HtmlResolver, fallback: HtmlResolver): HtmlResolver;
168
185
  //#endregion
169
- export { HtmlRenderProps as a, RenderFunction as c, getRenderFunction as d, mergeHtmlResolvers as f, HtmlRenderFunction as i, RenderProps as l, typeToKey as m, BaseFieldProps as n, HtmlResolver as o, mergeResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, getHtmlRenderFn as u };
186
+ export { HtmlRenderProps as a, RenderFunction as c, getHtmlRenderFn as d, getRenderFunction as f, typeToKey as h, HtmlRenderFunction as i, RenderProps as l, mergeResolvers as m, BaseFieldProps as n, HtmlResolver as o, mergeHtmlResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, buildRenderProps as u };
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
1
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
2
2
 
3
3
  //#region src/themes/mantine.d.ts
4
4
  /**
@@ -103,6 +103,7 @@ function renderEnumInput(props) {
103
103
  id,
104
104
  children: enumValue || "—"
105
105
  });
106
+ const enumValues = props.tree.type === "enum" ? props.tree.enumValues : [];
106
107
  return /* @__PURE__ */ jsx(MantineSelect, {
107
108
  id,
108
109
  label,
@@ -110,15 +111,15 @@ function renderEnumInput(props) {
110
111
  onChange: (v) => {
111
112
  if (typeof v === "string") props.onChange(v);
112
113
  },
113
- data: (props.enumValues ?? []).map((v) => ({
114
+ data: enumValues.map((v) => ({
114
115
  value: v,
115
116
  label: v
116
117
  }))
117
118
  });
118
119
  }
119
120
  function renderObjectContainer(props) {
120
- const fields = props.fields;
121
- if (fields === void 0) return null;
121
+ if (props.tree.type !== "object") return null;
122
+ const fields = props.tree.fields;
122
123
  const obj = isObject(props.value) ? props.value : {};
123
124
  return /* @__PURE__ */ jsx(MantineFieldset, {
124
125
  legend: getLabel(props),
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
1
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
2
2
 
3
3
  //#region src/themes/mui.d.ts
4
4
  /**