schema-components 1.17.0 → 1.18.1

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 (53) 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/fieldOrder.d.mts +10 -0
  5. package/dist/core/fieldOrder.mjs +12 -0
  6. package/dist/core/merge.d.mts +14 -8
  7. package/dist/core/merge.mjs +109 -12
  8. package/dist/core/normalise.d.mts +1 -1
  9. package/dist/core/ref.d.mts +1 -1
  10. package/dist/core/renderer.d.mts +2 -2
  11. package/dist/core/renderer.mjs +31 -1
  12. package/dist/core/swagger2.d.mts +1 -1
  13. package/dist/core/typeInference.d.mts +2 -2
  14. package/dist/core/walkBuilders.d.mts +2 -2
  15. package/dist/core/walker.mjs +2 -2
  16. package/dist/{diagnostics-DzbZmcLI.d.mts → diagnostics-BYk63jsC.d.mts} +1 -1
  17. package/dist/html/a11y.d.mts +13 -2
  18. package/dist/html/a11y.mjs +26 -2
  19. package/dist/html/renderToHtml.d.mts +1 -1
  20. package/dist/html/renderToHtml.mjs +18 -33
  21. package/dist/html/renderToHtmlStream.d.mts +1 -1
  22. package/dist/html/renderers.d.mts +4 -3
  23. package/dist/html/renderers.mjs +37 -36
  24. package/dist/html/streamRenderers.d.mts +1 -1
  25. package/dist/html/streamRenderers.mjs +10 -21
  26. package/dist/openapi/ApiSecurity.mjs +1 -1
  27. package/dist/openapi/bundle.d.mts +9 -4
  28. package/dist/openapi/bundle.mjs +74 -15
  29. package/dist/openapi/components.d.mts +1 -1
  30. package/dist/openapi/components.mjs +20 -15
  31. package/dist/openapi/parser.d.mts +2 -2
  32. package/dist/openapi/parser.mjs +12 -12
  33. package/dist/openapi/resolve.d.mts +13 -2
  34. package/dist/openapi/resolve.mjs +19 -3
  35. package/dist/react/SchemaComponent.d.mts +3 -3
  36. package/dist/react/SchemaComponent.mjs +1 -30
  37. package/dist/react/SchemaView.d.mts +4 -3
  38. package/dist/react/SchemaView.mjs +12 -43
  39. package/dist/react/headless.d.mts +1 -1
  40. package/dist/react/headlessRenderers.d.mts +1 -1
  41. package/dist/react/headlessRenderers.mjs +37 -32
  42. package/dist/{ref-DvWoULcy.d.mts → ref-Ckt5liZs.d.mts} +1 -1
  43. package/dist/{renderer-B3s8o2B8.d.mts → renderer-BAGoX4AK.d.mts} +20 -26
  44. package/dist/themes/mantine.d.mts +1 -1
  45. package/dist/themes/mantine.mjs +6 -4
  46. package/dist/themes/mui.d.mts +1 -1
  47. package/dist/themes/mui.mjs +7 -5
  48. package/dist/themes/radix.d.mts +1 -1
  49. package/dist/themes/radix.mjs +5 -4
  50. package/dist/themes/shadcn.d.mts +1 -1
  51. package/dist/themes/shadcn.mjs +8 -6
  52. package/dist/{typeInference-k7FXfTVO.d.mts → typeInference-5JiqIZ8t.d.mts} +57 -4
  53. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { sortFieldsByOrder } from "../core/fieldOrder.mjs";
1
2
  import { h, raw, serialize } from "./html.mjs";
2
3
  import { ariaDescribedByAttrs, ariaLabelAttrs, ariaReadonlyAttrs, ariaRequiredAttrs, buildHintElement, buildInputId, requiredIndicator } from "./a11y.mjs";
3
4
  //#region src/html/renderers.ts
@@ -7,11 +8,12 @@ function dateInputType(format) {
7
8
  if (format === "date-time" || format === "datetime") return "datetime-local";
8
9
  }
9
10
  /**
10
- * Normalise a dot-separated path into a valid, `sc-` prefixed HTML ID.
11
- * Dots in paths (from nested objects) become hyphens.
11
+ * Normalise a structural path into a valid, `sc-` prefixed HTML ID.
12
+ * Dots (object nesting) and brackets (array indices) become hyphens so
13
+ * the id remains a valid CSS selector and predictable in test queries.
12
14
  */
13
15
  function fieldId(path) {
14
- return `sc-${path.replace(/\./g, "-")}`;
16
+ return `sc-${path.replace(/[.[\]]+/g, "-").replace(/-+$/g, "")}`;
15
17
  }
16
18
  function renderStringHtml(props) {
17
19
  if (props.readOnly) return serialize(renderStringReadOnly(props));
@@ -134,7 +136,8 @@ function renderEnumEditable(props) {
134
136
  const enumValue = typeof props.value === "string" ? props.value : "";
135
137
  const id = fieldId(props.path);
136
138
  const selectedValue = props.writeOnly ? "" : enumValue;
137
- const optionNodes = [h("option", { value: "" }, "Select…"), ...(props.enumValues ?? []).map((v) => {
139
+ const enumValues = props.tree.type === "enum" ? props.tree.enumValues : [];
140
+ const optionNodes = [h("option", { value: "" }, "Select…"), ...enumValues.map((v) => {
138
141
  const display = v === null ? "null" : typeof v === "string" ? v : String(v);
139
142
  const attrs = { value: display };
140
143
  if (display === selectedValue) attrs.selected = true;
@@ -152,22 +155,20 @@ function renderObjectHtml(props) {
152
155
  return serialize(renderObjectNode(props));
153
156
  }
154
157
  function renderObjectNode(props) {
155
- const fields = props.fields;
156
- if (fields === void 0) return "";
158
+ if (props.tree.type !== "object") return "";
159
+ const fields = props.tree.fields;
157
160
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
158
161
  const obj = isRecord(props.value) ? props.value : {};
159
162
  const descriptionText = typeof props.meta.description === "string" ? props.meta.description : void 0;
160
163
  const legend = descriptionText !== void 0 ? h("legend", {}, descriptionText) : void 0;
161
- const sortedEntries = Object.entries(fields).sort((a, b) => {
162
- return (typeof a[1].meta.order === "number" ? a[1].meta.order : Infinity) - (typeof b[1].meta.order === "number" ? b[1].meta.order : Infinity);
163
- }).filter(([, field]) => field.meta.visible !== false);
164
+ const sortedEntries = sortFieldsByOrder(fields).filter(([, field]) => field.meta.visible !== false);
164
165
  if (props.readOnly) {
165
166
  const children = [];
166
167
  if (legend !== void 0) children.push(legend);
167
168
  for (const [key, field] of sortedEntries) {
168
169
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
169
170
  const childValue = obj[key];
170
- const childHtml = props.renderChild(field, childValue, props.path ? `${props.path}.${key}` : key);
171
+ const childHtml = props.renderChild(field, childValue, key);
171
172
  children.push(h("dt", { class: "sc-label" }, label));
172
173
  children.push(h("dd", { class: "sc-value" }, raw(childHtml)));
173
174
  }
@@ -180,9 +181,8 @@ function renderObjectNode(props) {
180
181
  for (const [key, field] of sortedEntries) {
181
182
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
182
183
  const fieldId = buildInputId(props.path, key);
183
- const childPath = props.path ? `${props.path}.${key}` : key;
184
184
  const childValue = obj[key];
185
- const childHtml = props.renderChild(field, childValue, childPath);
185
+ const childHtml = props.renderChild(field, childValue, key);
186
186
  const required = requiredIndicator(field);
187
187
  const labelContent = [label];
188
188
  if (required !== void 0) labelContent.push(required);
@@ -203,24 +203,20 @@ function renderArrayHtml(props) {
203
203
  }
204
204
  function renderArrayNode(props) {
205
205
  const arr = Array.isArray(props.value) ? props.value : [];
206
- const element = props.element;
206
+ const element = props.tree.type === "array" ? props.tree.element : void 0;
207
207
  if (element === void 0) return "";
208
- const items = arr.map((item) => {
209
- return h("li", { class: "sc-item" }, raw(props.renderChild(element, item)));
210
- });
211
- if (props.readOnly) return h("ul", { class: "sc-array" }, ...items);
212
- return h("div", { class: "sc-array" }, ...arr.map((item) => {
213
- return h("div", {}, raw(props.renderChild(element, item)));
214
- }));
208
+ const childHtmls = arr.map((item, i) => props.renderChild(element, item, `[${String(i)}]`));
209
+ if (props.readOnly) return h("ul", { class: "sc-array" }, ...childHtmls.map((childHtml) => h("li", { class: "sc-item" }, raw(childHtml))));
210
+ return h("div", { class: "sc-array" }, ...childHtmls.map((childHtml) => h("div", {}, raw(childHtml))));
215
211
  }
216
212
  function renderRecordHtml(props) {
217
213
  return serialize(renderRecordNode(props));
218
214
  }
219
215
  function renderRecordNode(props) {
216
+ if (props.tree.type !== "record") return "";
220
217
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
221
218
  const obj = isRecord(props.value) ? props.value : {};
222
- const valueType = props.valueType;
223
- if (valueType === void 0) return "";
219
+ const valueType = props.tree.valueType;
224
220
  const attrs = {
225
221
  class: "sc-record",
226
222
  role: "group"
@@ -242,12 +238,13 @@ function renderRecordNode(props) {
242
238
  return h("div", attrs, ...children);
243
239
  }
244
240
  function renderLiteralHtml(props) {
245
- const values = props.literalValues;
246
- if (values === void 0 || values.length === 0) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
241
+ if (props.tree.type !== "literal") return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
242
+ const values = props.tree.literalValues;
243
+ if (values.length === 0) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
247
244
  return serialize(h("span", { class: "sc-value" }, values.map((v) => v === null ? "null" : String(v)).join(", ")));
248
245
  }
249
246
  function renderUnionHtml(props) {
250
- const options = props.options;
247
+ const options = props.tree.type === "union" || props.tree.type === "discriminatedUnion" ? props.tree.options : void 0;
251
248
  if (options === void 0 || options.length === 0) {
252
249
  if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
253
250
  return serialize(h("span", { class: "sc-value" }, JSON.stringify(props.value)));
@@ -259,8 +256,8 @@ function renderUnionHtml(props) {
259
256
  return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
260
257
  }
261
258
  function renderDiscriminatedUnionHtml(props) {
262
- const options = props.options;
263
- const discriminator = props.discriminator;
259
+ const options = props.tree.type === "discriminatedUnion" ? props.tree.options : void 0;
260
+ const discriminator = props.tree.type === "discriminatedUnion" ? props.tree.discriminator : void 0;
264
261
  if (options === void 0 || options.length === 0) {
265
262
  if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
266
263
  return serialize(h("span", { class: "sc-value" }, JSON.stringify(props.value)));
@@ -289,7 +286,9 @@ function renderDiscriminatedUnionHtml(props) {
289
286
  if (activeOption !== void 0) return props.renderChild(activeOption, props.value);
290
287
  return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
291
288
  }
292
- const panelId = `sc-${props.path}-panel`;
289
+ const baseId = fieldId(props.path);
290
+ const panelId = `${baseId}-panel`;
291
+ const tabId = (i) => `${baseId}-tab-${String(i)}`;
293
292
  const children = [h("div", {
294
293
  role: "tablist",
295
294
  class: "sc-tabs",
@@ -299,7 +298,7 @@ function renderDiscriminatedUnionHtml(props) {
299
298
  type: "button",
300
299
  role: "tab",
301
300
  class: i === activeIndex ? "sc-tab sc-tab--active" : "sc-tab",
302
- id: `sc-${props.path}-tab-${String(i)}`,
301
+ id: tabId(i),
303
302
  "aria-selected": i === activeIndex ? "true" : void 0,
304
303
  "aria-controls": panelId,
305
304
  tabindex: i === activeIndex ? "0" : "-1"
@@ -310,7 +309,7 @@ function renderDiscriminatedUnionHtml(props) {
310
309
  children.push(h("div", {
311
310
  role: "tabpanel",
312
311
  id: panelId,
313
- "aria-labelledby": `sc-${props.path}-tab-${String(activeIndex)}`
312
+ "aria-labelledby": tabId(activeIndex)
314
313
  }, raw(childHtml)));
315
314
  }
316
315
  return serialize(h("div", { class: "sc-discriminated-union" }, ...children));
@@ -335,7 +334,7 @@ function renderFileHtml(props) {
335
334
  return serialize(h("input", attrs));
336
335
  }
337
336
  function renderRecursiveHtml(props) {
338
- const refTarget = props.refTarget ?? "";
337
+ const refTarget = props.tree.type === "recursive" ? props.tree.refTarget : "";
339
338
  return serialize(h("fieldset", { class: "sc-recursive" }, `↻ ${typeof props.meta.description === "string" ? props.meta.description : refTarget} (recursive)`));
340
339
  }
341
340
  function renderUnknownHtml(props) {
@@ -354,9 +353,9 @@ function renderUnknownHtml(props) {
354
353
  return serialize(h("input", attrs));
355
354
  }
356
355
  function renderTupleHtml(props) {
356
+ if (props.tree.type !== "tuple") return renderUnknownHtml(props);
357
357
  const arr = Array.isArray(props.value) ? props.value : [];
358
- const prefixItems = props.prefixItems;
359
- if (prefixItems === void 0) return renderUnknownHtml(props);
358
+ const prefixItems = props.tree.prefixItems;
360
359
  const children = [];
361
360
  for (let i = 0; i < prefixItems.length; i++) {
362
361
  const itemValue = arr[i];
@@ -369,9 +368,11 @@ function renderTupleHtml(props) {
369
368
  }
370
369
  function renderConditionalHtml(props) {
371
370
  const children = [];
372
- if (props.ifClause !== void 0) children.push(h("div", { class: "sc-conditional-if" }, raw("if: ...")));
373
- if (props.thenClause !== void 0) children.push(h("div", { class: "sc-conditional-then" }, raw("then: ...")));
374
- if (props.elseClause !== void 0) children.push(h("div", { class: "sc-conditional-else" }, raw("else: ...")));
371
+ if (props.tree.type === "conditional") {
372
+ children.push(h("div", { class: "sc-conditional-if" }, raw("if: ...")));
373
+ if (props.tree.thenClause !== void 0) children.push(h("div", { class: "sc-conditional-then" }, raw("then: ...")));
374
+ if (props.tree.elseClause !== void 0) children.push(h("div", { class: "sc-conditional-else" }, raw("else: ...")));
375
+ }
375
376
  return serialize(h("div", { class: "sc-conditional" }, ...children));
376
377
  }
377
378
  function renderNegationHtml(props) {
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
2
+ import { o as HtmlResolver } from "../renderer-BAGoX4AK.mjs";
3
3
  import { HtmlElement } from "./html.mjs";
4
4
 
5
5
  //#region src/html/streamRenderers.d.ts
@@ -14,27 +14,16 @@ function yieldClose(el) {
14
14
  }
15
15
  function renderLeaf(tree, value, mergedResolver, path) {
16
16
  const renderFn = getHtmlRenderFn(tree.type, mergedResolver);
17
- if (renderFn !== void 0) {
18
- const props = {
19
- value,
20
- readOnly: tree.editability === "presentation",
21
- writeOnly: tree.editability === "input",
22
- meta: tree.meta,
23
- constraints: tree.constraints,
24
- path,
25
- tree,
26
- renderChild: () => ""
27
- };
28
- if (tree.type === "enum") props.enumValues = tree.enumValues;
29
- if (tree.type === "literal") props.literalValues = tree.literalValues;
30
- if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
31
- if (tree.type === "object") props.fields = tree.fields;
32
- if (tree.type === "union") props.options = tree.options;
33
- if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
34
- if (tree.type === "record") props.keyType = tree.keyType;
35
- if (tree.type === "record") props.valueType = tree.valueType;
36
- return renderFn(props);
37
- }
17
+ if (renderFn !== void 0) return renderFn({
18
+ value,
19
+ readOnly: tree.editability === "presentation",
20
+ writeOnly: tree.editability === "input",
21
+ meta: tree.meta,
22
+ constraints: tree.constraints,
23
+ path,
24
+ tree,
25
+ renderChild: () => ""
26
+ });
38
27
  if (value === void 0 || value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
39
28
  return serialize(h("span", { class: "sc-value" }, typeof value === "string" ? value : JSON.stringify(value)));
40
29
  }
@@ -13,7 +13,7 @@ function ApiSecurity({ requirements, schemes }) {
13
13
  "data-security-name": true,
14
14
  children: req.name
15
15
  }),
16
- scheme !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
16
+ scheme !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [scheme.type !== void 0 && /* @__PURE__ */ jsx("span", {
17
17
  "data-security-type": true,
18
18
  children: scheme.type
19
19
  }), scheme.description && /* @__PURE__ */ jsx("span", {
@@ -34,13 +34,18 @@ type BundleResolver = (uri: string) => unknown;
34
34
  *
35
35
  * Walks every $ref in the document. For external refs (not starting with `#`),
36
36
  * calls the resolver to fetch the external document, extracts the referenced
37
- * schema, inlines it into `components.schemas` with a synthesised name, and
38
- * rewrites the $ref to point to the inlined copy.
37
+ * schema, inlines it into `components.schemas` under a synthesised name, and
38
+ * rewrites the original $ref to point at the new internal location
39
+ * (`#/components/schemas/<name>`).
40
+ *
41
+ * Identical external refs share a single entry — the second occurrence of
42
+ * the same `(uri, fragment)` pair reuses the name produced for the first.
43
+ * Name collisions between different refs are resolved by suffixing a counter.
39
44
  *
40
45
  * The resolver is called once per unique URI and the result is cached.
41
46
  *
42
- * Returns a deep-cloned document with all external refs resolved.
43
- * The original document is never mutated.
47
+ * Returns a deep-cloned document with all external refs replaced by internal
48
+ * refs. The original document is never mutated.
44
49
  */
45
50
  declare function bundleOpenApiDoc(doc: Record<string, unknown>, resolver: BundleResolver): Promise<Record<string, unknown>>;
46
51
  //#endregion
@@ -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,22 @@ 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
  }
91
+ return;
75
92
  }
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);
93
+ for (const value of Object.values(node)) if (isObject(value)) await walkAndInline(value, schemasNode, uriCache, inlineCache, resolver);
94
+ else if (Array.isArray(value)) for (const item of value) await walkAndInline(item, schemasNode, uriCache, inlineCache, resolver);
78
95
  }
79
96
  /**
80
97
  * Resolve a JSON Pointer fragment within a document.
@@ -91,5 +108,47 @@ function resolveFragment(doc, fragment) {
91
108
  }
92
109
  return isObject(current) ? current : void 0;
93
110
  }
111
+ /**
112
+ * Derive a candidate identifier for an inlined external schema. Prefers
113
+ * the last meaningful segment of the JSON Pointer fragment; falls back
114
+ * to the URI's filename (sans extension), then to a generic prefix.
115
+ */
116
+ function deriveCandidateName(uri, fragment) {
117
+ if (fragment.startsWith("#/")) {
118
+ const last = fragment.slice(2).split("/").at(-1);
119
+ if (last !== void 0 && last.length > 0) return sanitiseName(last);
120
+ }
121
+ const pathOnly = uri.split(/[?#]/)[0] ?? uri;
122
+ const lastSlash = pathOnly.lastIndexOf("/");
123
+ const filename = lastSlash >= 0 ? pathOnly.slice(lastSlash + 1) : pathOnly;
124
+ const dot = filename.lastIndexOf(".");
125
+ const stem = dot > 0 ? filename.slice(0, dot) : filename;
126
+ if (stem.length > 0) return sanitiseName(stem);
127
+ return "ExternalSchema";
128
+ }
129
+ /**
130
+ * Sanitise a string into a JSON Pointer-safe identifier: alphanumerics
131
+ * and underscores only. An empty result falls back to "Schema".
132
+ */
133
+ function sanitiseName(raw) {
134
+ const cleaned = raw.replace(/[^A-Za-z0-9_]/g, "_");
135
+ return cleaned.length > 0 ? cleaned : "Schema";
136
+ }
137
+ /**
138
+ * Place the resolved target into `components.schemas` under a unique
139
+ * name derived from the ref, and return the chosen name. Collisions
140
+ * with existing entries are resolved by suffixing a counter.
141
+ */
142
+ function registerInline(schemasNode, uri, fragment, target) {
143
+ const base = deriveCandidateName(uri, fragment);
144
+ let name = base;
145
+ let counter = 2;
146
+ while (name in schemasNode) {
147
+ name = `${base}_${String(counter)}`;
148
+ counter++;
149
+ }
150
+ schemasNode[name] = structuredClone(target);
151
+ return name;
152
+ }
94
153
  //#endregion
95
154
  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,6 +1,4 @@
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";
5
3
  import { ApiCallbacks } from "./ApiCallbacks.mjs";
6
4
  import { ApiLinks } from "./ApiLinks.mjs";
@@ -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 };
@@ -41,7 +41,7 @@ interface SecurityRequirement {
41
41
  scopes: string[];
42
42
  }
43
43
  interface SecurityScheme {
44
- type: string;
44
+ type: string | undefined;
45
45
  description: string | undefined;
46
46
  name: string | undefined;
47
47
  location: string | undefined;
@@ -92,7 +92,7 @@ declare function getRequestBody(parsed: OpenApiDocument, path: string, method: s
92
92
  declare function getResponses(parsed: OpenApiDocument, path: string, method: string): ResponseInfo[];
93
93
  declare function getSecurityRequirements(parsed: OpenApiDocument, path: string, method: string): SecurityRequirement[];
94
94
  declare function getSecuritySchemes(parsed: OpenApiDocument): Map<string, SecurityScheme>;
95
- declare function getResponseHeaders(response: JsonObject): Map<string, HeaderInfo>;
95
+ declare function getResponseHeaders(response: JsonObject, doc?: JsonObject): Map<string, HeaderInfo>;
96
96
  declare function listWebhooks(parsed: OpenApiDocument): WebhookInfo[];
97
97
  declare function getExternalDocs(obj: JsonObject): ExternalDocs | undefined;
98
98
  declare function getXmlInfo(schema: JsonObject): XmlInfo | undefined;
@@ -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;
@@ -143,7 +143,7 @@ function getResponses(parsed, path, method) {
143
143
  const content = getProperty(response, "content");
144
144
  const contentTypes = isObject(content) ? Object.keys(content) : [];
145
145
  const schema = isObject(content) ? extractSchemaFromContent(content) : void 0;
146
- const headers = getResponseHeaders(response);
146
+ const headers = getResponseHeaders(response, parsed.doc);
147
147
  result.push({
148
148
  statusCode,
149
149
  description: getString(response, "description"),
@@ -196,7 +196,7 @@ function getSecuritySchemes(parsed) {
196
196
  if (!isObject(scheme)) continue;
197
197
  const flowsProp = getProperty(scheme, "flows");
198
198
  result.set(name, {
199
- type: getString(scheme, "type") ?? "",
199
+ type: getString(scheme, "type"),
200
200
  description: getString(scheme, "description"),
201
201
  name: getString(scheme, "name"),
202
202
  location: getString(scheme, "in"),
@@ -208,14 +208,14 @@ function getSecuritySchemes(parsed) {
208
208
  }
209
209
  return result;
210
210
  }
211
- function getResponseHeaders(response) {
211
+ function getResponseHeaders(response, doc) {
212
212
  const result = /* @__PURE__ */ new Map();
213
213
  const headers = getProperty(response, "headers");
214
214
  if (!isObject(headers)) return result;
215
215
  for (const [name, headerObj] of Object.entries(headers)) {
216
216
  if (!isObject(headerObj)) continue;
217
217
  const ref = getString(headerObj, "$ref");
218
- const header = (ref !== void 0 ? resolveRefInDoc(headerObj, ref) : void 0) ?? headerObj;
218
+ const header = (ref !== void 0 && doc !== void 0 ? resolveRefInDoc(doc, ref) : void 0) ?? headerObj;
219
219
  const schemaProp = getProperty(header, "schema");
220
220
  result.set(name, {
221
221
  name,
@@ -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,8 +1,8 @@
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-BAGoX4AK.mjs";
5
+ import { c as PathOfType, l as ResolveOpenAPIRef, n as FromJSONSchema } from "../typeInference-5JiqIZ8t.mjs";
6
6
  import { z } from "zod";
7
7
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
8
8
  import { ReactNode } from "react";