schema-components 1.22.0 → 1.24.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 (82) hide show
  1. package/README.md +3 -1
  2. package/dist/core/adapter.d.mts +97 -3
  3. package/dist/core/adapter.mjs +260 -111
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -7
  6. package/dist/core/cssClasses.d.mts +52 -0
  7. package/dist/core/cssClasses.mjs +51 -0
  8. package/dist/core/diagnostics.d.mts +1 -1
  9. package/dist/core/errors.d.mts +1 -1
  10. package/dist/core/errors.mjs +5 -13
  11. package/dist/core/fieldOrder.d.mts +1 -1
  12. package/dist/core/formats.d.mts +9 -2
  13. package/dist/core/formats.mjs +12 -1
  14. package/dist/core/idPath.d.mts +54 -0
  15. package/dist/core/idPath.mjs +66 -0
  16. package/dist/core/merge.d.mts +10 -1
  17. package/dist/core/merge.mjs +49 -10
  18. package/dist/core/normalise.d.mts +14 -3
  19. package/dist/core/normalise.mjs +2 -2
  20. package/dist/core/openapi30.d.mts +15 -1
  21. package/dist/core/openapi30.mjs +2 -2
  22. package/dist/core/openapiConstants.d.mts +67 -0
  23. package/dist/core/openapiConstants.mjs +90 -0
  24. package/dist/core/ref.d.mts +2 -2
  25. package/dist/core/ref.mjs +83 -6
  26. package/dist/core/refChain.d.mts +70 -0
  27. package/dist/core/refChain.mjs +44 -0
  28. package/dist/core/renderer.d.mts +1 -1
  29. package/dist/core/swagger2.d.mts +1 -1
  30. package/dist/core/swagger2.mjs +1 -1
  31. package/dist/core/typeInference.d.mts +982 -2
  32. package/dist/core/types.d.mts +1 -1
  33. package/dist/core/unionMatch.d.mts +36 -0
  34. package/dist/core/unionMatch.mjs +53 -0
  35. package/dist/core/version.d.mts +1 -1
  36. package/dist/core/version.mjs +29 -17
  37. package/dist/core/walkBuilders.d.mts +23 -4
  38. package/dist/core/walkBuilders.mjs +27 -7
  39. package/dist/core/walker.d.mts +1 -1
  40. package/dist/core/walker.mjs +44 -45
  41. package/dist/{diagnostics-D0QCYGv0.d.mts → diagnostics-Cbwak-ZX.d.mts} +1 -1
  42. package/dist/{errors-DpFwqs5C.d.mts → errors-g_MCTQel.d.mts} +9 -15
  43. package/dist/html/a11y.d.mts +9 -4
  44. package/dist/html/a11y.mjs +10 -19
  45. package/dist/html/renderToHtml.d.mts +2 -2
  46. package/dist/html/renderToHtmlStream.d.mts +2 -2
  47. package/dist/html/renderToHtmlStream.mjs +12 -1
  48. package/dist/html/renderers.d.mts +32 -8
  49. package/dist/html/renderers.mjs +125 -111
  50. package/dist/html/streamRenderers.d.mts +4 -5
  51. package/dist/html/streamRenderers.mjs +40 -61
  52. package/dist/{normalise-DVEJQmF7.mjs → normalise-DCYp06Sr.mjs} +352 -162
  53. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  54. package/dist/openapi/ApiLinks.d.mts +1 -1
  55. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  56. package/dist/openapi/ApiSecurity.d.mts +1 -1
  57. package/dist/openapi/components.d.mts +116 -37
  58. package/dist/openapi/components.mjs +54 -37
  59. package/dist/openapi/parser.d.mts +19 -9
  60. package/dist/openapi/parser.mjs +262 -86
  61. package/dist/openapi/resolve.d.mts +20 -11
  62. package/dist/openapi/resolve.mjs +135 -75
  63. package/dist/react/SchemaComponent.d.mts +32 -7
  64. package/dist/react/SchemaComponent.mjs +45 -21
  65. package/dist/react/SchemaView.d.mts +30 -10
  66. package/dist/react/a11y.d.mts +21 -0
  67. package/dist/react/a11y.mjs +24 -0
  68. package/dist/react/fieldPath.d.mts +1 -1
  69. package/dist/react/headless.d.mts +1 -1
  70. package/dist/react/headlessRenderers.d.mts +8 -9
  71. package/dist/react/headlessRenderers.mjs +41 -72
  72. package/dist/{ref-D-_JBZkF.d.mts → ref-DCDuswPe.d.mts} +38 -3
  73. package/dist/{renderer-BaRlQIuN.d.mts → renderer-CXJ8y0qw.d.mts} +1 -1
  74. package/dist/themes/mantine.d.mts +1 -1
  75. package/dist/themes/mui.d.mts +1 -1
  76. package/dist/themes/radix.d.mts +1 -1
  77. package/dist/themes/shadcn.d.mts +1 -1
  78. package/dist/themes/shadcn.mjs +2 -1
  79. package/dist/{types-BrRMV0en.d.mts → types-BTB73MB8.d.mts} +32 -4
  80. package/dist/{version-D2jfdX6E.d.mts → version-BFTVLsdb.d.mts} +7 -1
  81. package/package.json +1 -1
  82. package/dist/typeInference-DkcUHfaM.d.mts +0 -982
@@ -3,8 +3,9 @@ import "../core/limits.mjs";
3
3
  import { emitDiagnostic } from "../core/diagnostics.mjs";
4
4
  import { isPrototypePollutingKey } from "../core/uri.mjs";
5
5
  import { detectOpenApiVersion } from "../core/version.mjs";
6
- import { a as normaliseOpenApiSchemas } from "../normalise-DVEJQmF7.mjs";
7
- import { getParameters, getRequestBody, getResponses, listOperations, listWebhooks, parseOpenApiDocument } from "./parser.mjs";
6
+ import { o as normaliseOpenApiSchemas, r as documentContainsKeyword } from "../normalise-DCYp06Sr.mjs";
7
+ import { resolveRefChain } from "../core/refChain.mjs";
8
+ import { getParameters, getRequestBody, getResponses, listAllOperations, parseOpenApiDocument } from "./parser.mjs";
8
9
  //#region src/openapi/resolve.ts
9
10
  /**
10
11
  * OpenAPI document resolution and caching.
@@ -27,25 +28,55 @@ const docCache = /* @__PURE__ */ new WeakMap();
27
28
  * same form `<SchemaComponent>` does, keeping the OpenAPI components on
28
29
  * the same pipeline as the top-level adapter.
29
30
  *
30
- * When `diagnostics` is supplied, normalisation events
31
- * (`duplicate-body-parameter`, `dropped-swagger-feature`,
32
- * `unknown-json-schema-dialect`, `divisible-by-conflict`,
33
- * `relative-ref-resolved`, etc.) are forwarded to the sink. Passing
34
- * diagnostics also bypasses the cache so each call observes the
35
- * normalisation pipeline running against the supplied sink — caching
36
- * would silently swallow every emission after the first.
31
+ * ### Caching and diagnostics
37
32
  *
38
- * The cache is keyed by the caller-supplied document so subsequent
39
- * cache-eligible calls with the same input bypass both normalisation
40
- * and parsing.
33
+ * Normalisation runs at most once per document identity. The full set
34
+ * of doc-level diagnostics emitted during that single run is captured
35
+ * into the cache alongside the parsed result. Each caller-supplied
36
+ * sink receives the captured diagnostics exactly once per cached
37
+ * entry, no matter how many times `getParsed` is called with that
38
+ * `(doc, sink)` pair.
39
+ *
40
+ * The previous implementation bypassed the cache whenever
41
+ * `diagnostics` was supplied and re-ran the entire normalisation
42
+ * pipeline against the new sink. That fired every doc-level
43
+ * diagnostic once per call, so a parent like `ApiWebhooks` that
44
+ * renders `ApiWebhook` per webhook entry caused N-fold emission of a
45
+ * single real cause. With the new strategy, cardinality stays at one
46
+ * per real cause regardless of how many child renders share the
47
+ * sink.
48
+ *
49
+ * Strict mode is treated as a per-call invariant — see
50
+ * {@link replayCapturedDiagnostics} for the rationale.
41
51
  */
42
52
  function getParsed(doc, diagnostics) {
43
- if (diagnostics === void 0) {
44
- const cached = docCache.get(doc);
45
- if (cached !== void 0) return cached;
53
+ let cached = docCache.get(doc);
54
+ if (cached === void 0) {
55
+ cached = buildCachedParse(doc);
56
+ docCache.set(doc, cached);
57
+ if (cached.parsed.doc !== doc) docCache.set(cached.parsed.doc, cached);
46
58
  }
59
+ if (diagnostics?.diagnostics !== void 0 || diagnostics?.strict === true) replayCapturedDiagnostics(cached, diagnostics);
60
+ return cached.parsed;
61
+ }
62
+ /**
63
+ * Run the normalisation, validation, and parse pipeline against a doc
64
+ * once, capturing every emitted diagnostic into a private array. The
65
+ * private array becomes the source of truth for later replay through
66
+ * caller-supplied sinks.
67
+ *
68
+ * The internal capturing sink does NOT set `strict`. Letting strict
69
+ * mode throw mid-walk would leave the cache empty and force every
70
+ * subsequent caller to re-run the pipeline from scratch — and the
71
+ * defect this caching strategy fixes was precisely that kind of
72
+ * re-running. Strict is enforced instead during replay
73
+ * (see {@link replayCapturedDiagnostics}).
74
+ */
75
+ function buildCachedParse(doc) {
76
+ const captured = [];
77
+ const captureOpts = { diagnostics: (d) => captured.push(d) };
47
78
  const version = detectOpenApiVersion(doc);
48
- if (diagnostics !== void 0 && version?.major === 3 && docHasXmlAnywhere(doc)) emitDiagnostic(diagnostics, {
79
+ if (version?.major === 3 && documentContainsKeyword(doc, "xml")) emitDiagnostic(captureOpts, {
49
80
  code: "dropped-swagger-feature",
50
81
  message: `OpenAPI ${String(version.major)}.${String(version.minor)} xml Schema Object metadata is not rendered and will be ignored`,
51
82
  pointer: "",
@@ -54,17 +85,38 @@ function getParsed(doc, diagnostics) {
54
85
  source: "openapi-3.x"
55
86
  }
56
87
  });
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
- }
62
- const parsed = parseOpenApiDocument(normalisedDoc);
63
- if (diagnostics === void 0) {
64
- docCache.set(doc, parsed);
65
- if (normalisedDoc !== doc) docCache.set(normalisedDoc, parsed);
66
- }
67
- return parsed;
88
+ const normalisedDoc = version !== void 0 ? normaliseOpenApiSchemas(doc, version, captureOpts) : doc;
89
+ validateSecuritySchemeTypes(normalisedDoc, captureOpts);
90
+ detectUnsupportedCrossSchemaRefs(normalisedDoc, captureOpts);
91
+ return {
92
+ parsed: parseOpenApiDocument(normalisedDoc),
93
+ diagnostics: captured,
94
+ notifiedSinks: /* @__PURE__ */ new WeakSet()
95
+ };
96
+ }
97
+ /**
98
+ * Replay each captured diagnostic through the caller-supplied options.
99
+ *
100
+ * Strict mode is treated as a per-call invariant: when `strict` is set
101
+ * we always run the replay so the first captured diagnostic throws,
102
+ * matching the historical fail-fast contract regardless of how many
103
+ * times the cache has previously notified the sink.
104
+ *
105
+ * Non-strict, sink-bearing callers de-duplicate at the function-
106
+ * identity boundary: a second call with the same `(doc, sink)` pair
107
+ * short-circuits because the sink has already seen every captured
108
+ * diagnostic. This is the cardinality-1 guarantee that fixes the
109
+ * N-fold emission caused by parent-fans-out-into-children renders.
110
+ *
111
+ * The sink is only marked notified after a successful replay so a
112
+ * strict throw mid-replay does not silence a follow-up non-strict
113
+ * call that still wants the full captured set.
114
+ */
115
+ function replayCapturedDiagnostics(cached, opts) {
116
+ const sink = opts.diagnostics;
117
+ if (!(opts.strict === true) && sink !== void 0 && cached.notifiedSinks.has(sink)) return;
118
+ for (const diagnostic of cached.diagnostics) emitDiagnostic(opts, diagnostic);
119
+ if (sink !== void 0) cached.notifiedSinks.add(sink);
68
120
  }
69
121
  /**
70
122
  * Coerce an unknown value to a record, returning `undefined` when the
@@ -177,25 +229,6 @@ function detectUnsupportedCrossSchemaRefs(doc, diagnostics) {
177
229
  walk(doc, "");
178
230
  }
179
231
  /**
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;
197
- }
198
- /**
199
232
  * Look up a Path Item Object on the (already-normalised) parsed document,
200
233
  * following a single `$ref` hop into `components/pathItems` (OpenAPI 3.1)
201
234
  * if present. Returns `undefined` when the path is not present or the
@@ -205,44 +238,71 @@ function docHasXmlAnywhere(node) {
205
238
  * still surfacing path-item-level metadata to the React layer.
206
239
  */
207
240
  function lookupPathItemNode(parsed, path, diagnostics) {
208
- const fromPaths = resolvePathItemNode(parsed, getProperty(getProperty(parsed.doc, "paths"), path), diagnostics);
241
+ const paths = getProperty(parsed.doc, "paths");
242
+ const webhooks = getProperty(parsed.doc, "webhooks");
243
+ const pathsEntry = getProperty(paths, path);
244
+ const webhooksEntry = getProperty(webhooks, path);
245
+ if (isObject(pathsEntry) && isObject(webhooksEntry)) emitDiagnostic(diagnostics, {
246
+ code: "path-webhook-name-collision",
247
+ message: `Identifier "${path}" appears in both \`paths\` and \`webhooks\`; \`paths\` takes precedence`,
248
+ pointer: `/paths/${path.replace(/~/g, "~0").replace(/\//g, "~1")}`,
249
+ detail: { name: path }
250
+ });
251
+ const fromPaths = resolvePathItemNode(parsed, pathsEntry, diagnostics);
209
252
  if (fromPaths !== void 0) return fromPaths;
210
- return resolvePathItemNode(parsed, getProperty(getProperty(parsed.doc, "webhooks"), path), diagnostics);
253
+ return resolvePathItemNode(parsed, webhooksEntry, diagnostics);
254
+ }
255
+ /**
256
+ * Resolve a fragment `$ref` (must start with `#/`) against the parsed
257
+ * document by walking the JSON Pointer one segment at a time. Returns
258
+ * the resolved node only when every segment lands on a plain object;
259
+ * returns `undefined` when any intermediate segment is missing or
260
+ * non-object, or when the final node is not an object.
261
+ *
262
+ * Rejects `__proto__`, `constructor`, `prototype` segments — walking
263
+ * into any of these reads `Object.prototype` and would let a crafted
264
+ * pathItems `$ref` smuggle properties from the runtime prototype
265
+ * chain into the resolved Path Item Object.
266
+ */
267
+ function lookupFragmentRef(parsed, ref) {
268
+ if (!ref.startsWith("#/")) return void 0;
269
+ const parts = ref.slice(2).split("/");
270
+ let node = parsed.doc;
271
+ for (const part of parts) {
272
+ if (!isObject(node)) return void 0;
273
+ const decoded = part.replace(/~1/g, "/").replace(/~0/g, "~");
274
+ if (isPrototypePollutingKey(decoded)) return void 0;
275
+ node = node[decoded];
276
+ }
277
+ return isObject(node) ? node : void 0;
211
278
  }
212
279
  function resolvePathItemNode(parsed, pathItem, diagnostics) {
213
280
  if (!isObject(pathItem)) return void 0;
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)) {
281
+ return resolveRefChain(pathItem, {
282
+ lookup: (ref) => lookupFragmentRef(parsed, ref),
283
+ extractRef: (node) => {
284
+ const ref = getProperty(node, "$ref");
285
+ if (typeof ref !== "string") return void 0;
286
+ if (!ref.startsWith("#/")) return void 0;
287
+ return ref;
288
+ },
289
+ onCycle: (ref) => {
221
290
  emitDiagnostic(diagnostics, {
222
291
  code: "cyclic-path-item-ref",
223
292
  message: `Cyclic Path Item Object $ref "${ref}"`,
224
293
  pointer: ref,
225
294
  detail: { ref }
226
295
  });
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;
240
- }
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 }
296
+ },
297
+ onDepthExceeded: () => {
298
+ emitDiagnostic(diagnostics, {
299
+ code: "path-item-ref-too-deep",
300
+ message: `Path Item Object $ref chain exceeded ${String(8)} hops`,
301
+ pointer: "",
302
+ detail: { maxHops: 8 }
303
+ });
304
+ },
305
+ maxHops: 8
246
306
  });
247
307
  }
248
308
  function extractPathItemInfo(pathItem) {
@@ -265,7 +325,7 @@ function extractPathItemInfo(pathItem) {
265
325
  */
266
326
  function resolveOperationFromParsed(parsed, path, method, diagnostics) {
267
327
  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);
328
+ const operation = listAllOperations(parsed).find((op) => op.path === path && op.method === method);
269
329
  if (operation === void 0) throw new Error(`Operation not found: ${method.toUpperCase()} ${path}`);
270
330
  if (pathItemNode === void 0) throw new Error(`Path item missing for ${method.toUpperCase()} ${path}`);
271
331
  return {
@@ -1,8 +1,8 @@
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";
1
+ import { d as FieldOverrides, j as WalkedField, u as FieldOverride, w as SchemaMeta } from "../types-BTB73MB8.mjs";
2
+ import { t as Diagnostic } from "../diagnostics-Cbwak-ZX.mjs";
3
+ import { t as SchemaError } from "../errors-g_MCTQel.mjs";
4
+ import { l as RenderProps, r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
5
+ import { FromJSONSchema, FromJSONSchemaMode, IsSwagger2Doc, PathOfType, RejectUnrepresentableZod, ResolveOpenAPIRef, TypeAtPath, __SchemaInferenceFellBack } from "../core/typeInference.mjs";
6
6
  import { z } from "zod";
7
7
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
8
8
  import { ReactNode } from "react";
@@ -36,7 +36,7 @@ declare function SchemaProvider({
36
36
  * or `<SchemaProvider>` instead.
37
37
  */
38
38
  declare function registerWidget(name: string, render: (props: RenderProps) => unknown): void;
39
- type InferFields<T, Ref extends string | undefined> = T extends z.ZodType ? FieldOverrides<z.infer<T>> : T extends {
39
+ type InferFields<T, Ref extends string | undefined> = IsSwagger2Doc<T> extends true ? __SchemaInferenceFellBack : 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
  /**
@@ -55,7 +55,7 @@ type InferFields<T, Ref extends string | undefined> = T extends z.ZodType ? Fiel
55
55
  * without a ref), the result falls back to `unknown` so callers can
56
56
  * still supply arbitrary values.
57
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 {
58
+ type InferSchemaValue<T, Ref extends string | undefined, Mode extends FromJSONSchemaMode> = IsSwagger2Doc<T> extends true ? __SchemaInferenceFellBack : T extends z.ZodType ? Mode extends "input" ? z.input<T> : z.output<T> : T extends {
59
59
  openapi: unknown;
60
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
61
  /**
@@ -132,6 +132,17 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
132
132
  * The narrowing is fully expressible through the helper alias
133
133
  * without forcing every existing caller to update their value
134
134
  * shapes for `exactOptionalPropertyTypes` / enum literal widening.
135
+ *
136
+ * TODO(round7-integration): promote to
137
+ * `NarrowAtPath<InferSchemaValue<T, Ref, "output">, P>` once the
138
+ * test fixtures (headless union, discriminated union,
139
+ * schemaview equivalence, type-inference issue fixes) are
140
+ * migrated to either narrow their fixtures or accept the loose
141
+ * boundary. The retype cascades through call sites that
142
+ * intentionally pass invalid values to exercise fallback paths
143
+ * (`value={undefined}`, off-discriminator values, etc.) and
144
+ * through fixtures whose enum/literal types widen at the call
145
+ * site. Coordinated migration is required.
135
146
  */
136
147
  value?: unknown;
137
148
  /**
@@ -144,6 +155,15 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
144
155
  * without an unsafe boundary cast. The {@link InferredInputValue}
145
156
  * alias is the recommended way for callers to narrow on the
146
157
  * consumer side — `onChange={(v) => { const u = v as InferredInputValue<typeof schema>; ... }}`.
158
+ *
159
+ * TODO(round7-integration): promote to
160
+ * `(value: NarrowAtPath<InferSchemaValue<T, Ref, "input">, P>) => void`
161
+ * alongside the `value` retype. The contravariant boundary needs
162
+ * an internal cast at the only call site (`onChange?.(nextValue)`
163
+ * in `handleChange`) that the project's no-`as` rule disallows
164
+ * without explicit justification. The right place to introduce
165
+ * the cast is a tiny typed boundary helper accompanied by the
166
+ * fixture migration noted above.
147
167
  */
148
168
  onChange?: (value: unknown) => void;
149
169
  /** Run schema.safeParse() on change and surface errors via onValidationError. */
@@ -181,6 +201,11 @@ declare function SchemaComponent<T = unknown, Ref extends string | undefined = u
181
201
  * Append a child path suffix to a parent path. When the suffix is omitted
182
202
  * (e.g. transparent wrappers like union options), the parent path is
183
203
  * returned unchanged so the child inherits the parent's id.
204
+ *
205
+ * Bracketed array indices like `[0]` append directly so `tags` + `[0]`
206
+ * becomes `tags[0]` rather than `tags.[0]` — matching the canonical form
207
+ * used by `html/a11y.ts` `joinPath` and `react/fieldPath.ts` `resolvePath`,
208
+ * which already parses bracket notation when navigating WalkedField trees.
184
209
  */
185
210
  declare function joinPath(parent: string, suffix: string | undefined): string;
186
211
  /**
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
2
+ import { getProperty, isObject, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import "../core/limits.mjs";
4
4
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
5
5
  import { normaliseSchema } from "../core/adapter.mjs";
@@ -87,6 +87,7 @@ function SchemaComponent(props) {
87
87
  }
88
88
  throw error;
89
89
  }
90
+ const fieldsRecord = toRecordOrUndefined(fields);
90
91
  const handleChange = useCallback((nextValue) => {
91
92
  if (validate) {
92
93
  let error;
@@ -102,7 +103,7 @@ function SchemaComponent(props) {
102
103
  }
103
104
  if (error !== void 0) {
104
105
  onValidationError?.(error);
105
- dispatchFieldErrors(fields, error);
106
+ dispatchFieldErrors(fieldsRecord, error);
106
107
  }
107
108
  }
108
109
  onChange?.(nextValue);
@@ -112,7 +113,7 @@ function SchemaComponent(props) {
112
113
  jsonSchema,
113
114
  onChange,
114
115
  onValidationError,
115
- fields,
116
+ fieldsRecord,
116
117
  onDiagnostic,
117
118
  onError,
118
119
  schemaInput
@@ -120,7 +121,7 @@ function SchemaComponent(props) {
120
121
  const walkOptions = {
121
122
  componentMeta: mergedMeta,
122
123
  rootMeta,
123
- fieldOverrides: fields,
124
+ fieldOverrides: fieldsRecord,
124
125
  rootDocument,
125
126
  ...diagnostics !== void 0 ? { diagnostics } : {}
126
127
  };
@@ -136,10 +137,16 @@ function SchemaComponent(props) {
136
137
  * Append a child path suffix to a parent path. When the suffix is omitted
137
138
  * (e.g. transparent wrappers like union options), the parent path is
138
139
  * returned unchanged so the child inherits the parent's id.
140
+ *
141
+ * Bracketed array indices like `[0]` append directly so `tags` + `[0]`
142
+ * becomes `tags[0]` rather than `tags.[0]` — matching the canonical form
143
+ * used by `html/a11y.ts` `joinPath` and `react/fieldPath.ts` `resolvePath`,
144
+ * which already parses bracket notation when navigating WalkedField trees.
139
145
  */
140
146
  function joinPath(parent, suffix) {
141
147
  if (suffix === void 0 || suffix.length === 0) return parent;
142
148
  if (parent.length === 0) return suffix;
149
+ if (suffix.startsWith("[")) return `${parent}${suffix}`;
143
150
  return `${parent}.${suffix}`;
144
151
  }
145
152
  /**
@@ -170,9 +177,9 @@ function sanitisePrefix(value) {
170
177
  */
171
178
  function runValidation(zodSchema, jsonSchema, value, onDiagnostic) {
172
179
  if (zodSchema !== void 0 && isObject(zodSchema)) {
173
- const safeParseFn = zodSchema.safeParse;
174
- if (isCallable(safeParseFn)) {
175
- const result = safeParseFn(value);
180
+ const validateFn = isCodecSchema(zodSchema) ? zodSchema.safeEncode : zodSchema.safeParse;
181
+ if (isCallable(validateFn)) {
182
+ const result = validateFn(value);
176
183
  if (isObject(result) && "success" in result && result.success !== true) return result.error;
177
184
  return;
178
185
  }
@@ -261,12 +268,11 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
261
268
  if (fieldTree === void 0) throw new SchemaFieldError(`Field not found: ${path}`, schemaInput, path);
262
269
  const fieldValue = resolveValue(value, path);
263
270
  const handleChange = useCallback((nextFieldValue) => {
271
+ const newRootValue = setNestedValue(value, path, nextFieldValue);
264
272
  if (validate) {
265
- const newRootValue = setNestedValue(value, path, nextFieldValue);
266
273
  const error = runValidation(zodSchema, jsonSchema, newRootValue);
267
274
  if (error !== void 0) onValidationError?.(error);
268
275
  }
269
- const newRootValue = setNestedValue(value, path, nextFieldValue);
270
276
  onChange?.(newRootValue);
271
277
  }, [
272
278
  validate,
@@ -288,24 +294,21 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
288
294
  * Dispatch Zod errors to per-field onValidationError callbacks.
289
295
  * Walks the fields override tree and matches errors by path prefix.
290
296
  *
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.
297
+ * The runtime shape of `fields` is always `Record<string, FieldOverride>`
298
+ * after `InferFields<T, Ref>` is erasedthe typed variants
299
+ * (`FieldOverrides<U>`) and the loose `Record<string, FieldOverride>`
300
+ * fallback share the same structural shape, so the dispatch logic only
301
+ * needs the loose record. The previous parameter union
302
+ * (`Record<string, FieldOverride> | FieldOverrides<unknown> |
303
+ * undefined`) collapsed because `FieldOverrides<unknown>` reduces to
304
+ * `{}`, contributing no extra precision while adding noise to readers.
300
305
  */
301
306
  function dispatchFieldErrors(fields, error) {
302
307
  if (fields === void 0 || !isObject(error)) return;
303
308
  if (!("issues" in error)) return;
304
309
  const issues = error.issues;
305
310
  if (!Array.isArray(issues)) return;
306
- const overrides = toRecordOrUndefined(fields);
307
- if (overrides === void 0) return;
308
- for (const [key, override] of Object.entries(overrides)) {
311
+ for (const [key, override] of Object.entries(fields)) {
309
312
  if (override === void 0 || typeof override !== "object") continue;
310
313
  if (override === null) continue;
311
314
  if (!("onValidationError" in override)) continue;
@@ -327,5 +330,26 @@ function isFieldErrorCallback(value) {
327
330
  function isCallable(value) {
328
331
  return typeof value === "function";
329
332
  }
333
+ /**
334
+ * True when `value` is a Zod schema implemented as a codec.
335
+ *
336
+ * Detection looks for `"$ZodCodec"` in the schema's `_zod.traits`
337
+ * Set, mirroring the runtime detection used inside
338
+ * `core/adapter.ts` (`isCodecSchema` there) and Zod's own
339
+ * `isTransforming` helper. Duplicated here rather than imported
340
+ * because adapter.ts does not export the helper and is outside this
341
+ * fix-cycle's owned files.
342
+ *
343
+ * TODO(round7-integration): replace with an `import` once
344
+ * `isCodecSchema` is exported from `core/adapter.ts` (or promoted to
345
+ * `core/guards.ts`) by a coordinating commit.
346
+ */
347
+ function isCodecSchema(value) {
348
+ const zod = getProperty(value, "_zod");
349
+ if (!isObject(zod)) return false;
350
+ const traits = zod.traits;
351
+ if (traits instanceof Set) return traits.has("$ZodCodec");
352
+ return false;
353
+ }
330
354
  //#endregion
331
355
  export { SchemaComponent, SchemaField, SchemaProvider, joinPath, registerWidget, renderField, sanitisePrefix };
@@ -1,16 +1,36 @@
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";
1
+ import { w as SchemaMeta } from "../types-BTB73MB8.mjs";
2
+ import { t as Diagnostic } from "../diagnostics-Cbwak-ZX.mjs";
3
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
4
+ import { RejectUnrepresentableZod } from "../core/typeInference.mjs";
4
5
  import { WidgetMap } from "./SchemaComponent.mjs";
5
6
  import { ReactNode } from "react";
6
7
 
7
8
  //#region src/react/SchemaView.d.ts
8
- interface SchemaViewProps {
9
- /** Zod schema, JSON Schema object, or OpenAPI document. */
10
- schema: unknown;
9
+ interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefined> {
10
+ /**
11
+ * Zod schema, JSON Schema object, or OpenAPI document.
12
+ *
13
+ * Subject to the same compile-time rejection of unrepresentable
14
+ * Zod 4 types as {@link SchemaComponentProps.schema} — see
15
+ * {@link RejectUnrepresentableZod}.
16
+ */
17
+ schema: RejectUnrepresentableZod<T>;
11
18
  /** For OpenAPI: a ref string like "#/components/schemas/User". */
12
- ref?: string;
13
- /** Current value to render. */
19
+ ref?: Ref;
20
+ /**
21
+ * Current value to render.
22
+ *
23
+ * TYPE BOUNDARY NOTE: mirrors `SchemaComponentProps.value` — kept
24
+ * as `unknown` so the same boundary holds for both the editable
25
+ * (`SchemaComponent`) and read-only (`SchemaView`) entry points.
26
+ * The {@link InferredOutputValue} alias is the recommended way
27
+ * for callers to narrow on the consumer side.
28
+ *
29
+ * TODO(round7-integration): promote to
30
+ * `InferSchemaValue<T, Ref, "output">` alongside the matching
31
+ * `SchemaComponent` change. See the type-boundary note on
32
+ * `SchemaComponentProps.value` for the migration coordination.
33
+ */
14
34
  value?: unknown;
15
35
  /** Per-field meta overrides. */
16
36
  fields?: Record<string, unknown>;
@@ -44,7 +64,7 @@ interface SchemaViewProps {
44
64
  * Always renders in read-only mode. For editable forms, use
45
65
  * `<SchemaComponent>` with `"use client"`.
46
66
  */
47
- declare function SchemaView({
67
+ declare function SchemaView<T = unknown, Ref extends string | undefined = undefined>({
48
68
  schema: schemaInput,
49
69
  ref: refInput,
50
70
  value,
@@ -56,6 +76,6 @@ declare function SchemaView({
56
76
  onDiagnostic,
57
77
  strict,
58
78
  idPrefix
59
- }: SchemaViewProps): ReactNode;
79
+ }: SchemaViewProps<T, Ref>): ReactNode;
60
80
  //#endregion
61
81
  export { SchemaView, SchemaViewProps };
@@ -0,0 +1,21 @@
1
+ import { j as WalkedField } from "../types-BTB73MB8.mjs";
2
+
3
+ //#region src/react/a11y.d.ts
4
+ /**
5
+ * Build the ARIA attribute bundle for a renderer.
6
+ *
7
+ * - `aria-required="true"` whenever the field is non-optional.
8
+ * - `aria-label=<description>` when a non-empty description is supplied.
9
+ *
10
+ * Returns a plain `Record<string, string>` (rather than a typed
11
+ * attribute interface) so callers can spread the result into any JSX
12
+ * element type without per-element TypeScript widening.
13
+ *
14
+ * Each helper from `html/a11y.ts` returns its corresponding fragment
15
+ * separately. The React renderers merge them into a single object
16
+ * here because the headless renderers compose one attribute bag per
17
+ * `<input>` element rather than threading multiple bags through `h()`.
18
+ */
19
+ declare function buildAriaAttrs(tree: WalkedField, description?: unknown): Record<string, string>;
20
+ //#endregion
21
+ export { buildAriaAttrs };
@@ -0,0 +1,24 @@
1
+ //#region src/react/a11y.ts
2
+ /**
3
+ * Build the ARIA attribute bundle for a renderer.
4
+ *
5
+ * - `aria-required="true"` whenever the field is non-optional.
6
+ * - `aria-label=<description>` when a non-empty description is supplied.
7
+ *
8
+ * Returns a plain `Record<string, string>` (rather than a typed
9
+ * attribute interface) so callers can spread the result into any JSX
10
+ * element type without per-element TypeScript widening.
11
+ *
12
+ * Each helper from `html/a11y.ts` returns its corresponding fragment
13
+ * separately. The React renderers merge them into a single object
14
+ * here because the headless renderers compose one attribute bag per
15
+ * `<input>` element rather than threading multiple bags through `h()`.
16
+ */
17
+ function buildAriaAttrs(tree, description) {
18
+ const attrs = {};
19
+ if (tree.isOptional === false) attrs["aria-required"] = "true";
20
+ if (typeof description === "string" && description.length > 0) attrs["aria-label"] = description;
21
+ return attrs;
22
+ }
23
+ //#endregion
24
+ export { buildAriaAttrs };
@@ -1,4 +1,4 @@
1
- import { j as WalkedField } from "../types-BrRMV0en.mjs";
1
+ import { j as WalkedField } from "../types-BTB73MB8.mjs";
2
2
 
3
3
  //#region src/react/fieldPath.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BaRlQIuN.mjs";
1
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
2
2
 
3
3
  //#region src/react/headless.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { j as WalkedField } from "../types-BrRMV0en.mjs";
2
- import { l as RenderProps } from "../renderer-BaRlQIuN.mjs";
1
+ import { j as WalkedField } from "../types-BTB73MB8.mjs";
2
+ import { l as RenderProps } from "../renderer-CXJ8y0qw.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/react/headlessRenderers.d.ts
@@ -10,15 +10,14 @@ import { ReactNode } from "react";
10
10
  declare function toReactNode(value: unknown): ReactNode;
11
11
  /**
12
12
  * Build a stable, unique input ID from the path.
13
- * Used for `htmlFor`/`id` association between labels and inputs.
14
13
  *
15
- * Throws on an empty path: the previous "sc-field" fallback caused every
16
- * input across a form to share the same id, breaking label-input pairing
17
- * and screen reader navigation. Callers must thread a non-empty path
18
- * (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
14
+ * Re-exported alias for {@link fieldDomId} so external themes (shadcn,
15
+ * MUI, mantine, radix) keep importing `inputId` from the React entry
16
+ * point. Both the React and HTML renderers must derive the same id from
17
+ * the same path `fieldDomId` in `core/idPath.ts` is the single
18
+ * source-of-truth.
19
19
  *
20
- * Dots and bracket indices in paths are converted to hyphens to keep the
21
- * id valid as a CSS selector and predictable in test queries.
20
+ * Throws on an empty path; see `fieldDomId` for the rationale.
22
21
  */
23
22
  declare function inputId(path: string): string;
24
23
  declare function renderString(props: RenderProps): ReactNode;