@kubb/adapter-oas 5.0.0-beta.36 → 5.0.0-beta.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,11 +21,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let node_path = require("node:path");
25
- node_path = __toESM(node_path, 1);
26
24
  let _kubb_core = require("@kubb/core");
27
25
  let oas = require("oas");
28
26
  oas = __toESM(oas, 1);
27
+ let node_path = require("node:path");
28
+ node_path = __toESM(node_path, 1);
29
+ let node_fs_promises = require("node:fs/promises");
29
30
  let _redocly_openapi_core = require("@redocly/openapi-core");
30
31
  let oas_normalize = require("oas-normalize");
31
32
  oas_normalize = __toESM(oas_normalize, 1);
@@ -33,6 +34,138 @@ let swagger2openapi = require("swagger2openapi");
33
34
  swagger2openapi = __toESM(swagger2openapi, 1);
34
35
  let oas_types = require("oas/types");
35
36
  let oas_utils = require("oas/utils");
37
+ //#region src/constants.ts
38
+ /**
39
+ * Default parser options applied when no explicit options are provided.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
44
+ *
45
+ * const parser = createOasParser(oas)
46
+ * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
47
+ * ```
48
+ */
49
+ const DEFAULT_PARSER_OPTIONS = {
50
+ dateType: "string",
51
+ integerType: "bigint",
52
+ unknownType: "any",
53
+ emptySchemaType: "any",
54
+ enumSuffix: "enum"
55
+ };
56
+ /**
57
+ * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
58
+ *
59
+ * Used when building or parsing `$ref` strings.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
64
+ * ```
65
+ */
66
+ const SCHEMA_REF_PREFIX = "#/components/schemas/";
67
+ /**
68
+ * OpenAPI version string written into the stub document created during multi-spec merges.
69
+ */
70
+ const MERGE_OPENAPI_VERSION = "3.0.0";
71
+ /**
72
+ * Fallback `info.title` placed in the stub document when merging multiple API files.
73
+ */
74
+ const MERGE_DEFAULT_TITLE = "Merged API";
75
+ /**
76
+ * Fallback `info.version` placed in the stub document when merging multiple API files.
77
+ */
78
+ const MERGE_DEFAULT_VERSION = "1.0.0";
79
+ /**
80
+ * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
81
+ *
82
+ * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
83
+ * intersection member rather than being merged into the parent.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { structuralKeys } from '@kubb/adapter-oas'
88
+ *
89
+ * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
90
+ * // true when fragment has e.g. 'properties' or 'oneOf'
91
+ * ```
92
+ */
93
+ const structuralKeys = new Set([
94
+ "properties",
95
+ "items",
96
+ "additionalProperties",
97
+ "oneOf",
98
+ "anyOf",
99
+ "allOf",
100
+ "not"
101
+ ]);
102
+ /**
103
+ * Static map from OAS `format` strings to Kubb `SchemaType` values.
104
+ *
105
+ * Only formats whose AST type differs from the OAS `type` field appear here.
106
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
107
+ * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
108
+ * `idn-hostname` map to `'url'` as the closest generic string-format type.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { formatMap } from '@kubb/adapter-oas'
113
+ *
114
+ * formatMap['uuid'] // 'uuid'
115
+ * formatMap['binary'] // 'blob'
116
+ * formatMap['float'] // 'number'
117
+ * ```
118
+ */
119
+ /**
120
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
121
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
122
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
123
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
124
+ */
125
+ const specialCasedFormats = new Set([
126
+ "int64",
127
+ "date-time",
128
+ "date",
129
+ "time"
130
+ ]);
131
+ const formatMap = {
132
+ uuid: "uuid",
133
+ email: "email",
134
+ "idn-email": "email",
135
+ uri: "url",
136
+ "uri-reference": "url",
137
+ url: "url",
138
+ ipv4: "ipv4",
139
+ ipv6: "ipv6",
140
+ hostname: "url",
141
+ "idn-hostname": "url",
142
+ binary: "blob",
143
+ byte: "blob",
144
+ int32: "integer",
145
+ float: "number",
146
+ double: "number"
147
+ };
148
+ /**
149
+ * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * import { enumExtensionKeys } from '@kubb/adapter-oas'
154
+ *
155
+ * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
156
+ * ```
157
+ */
158
+ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
159
+ /**
160
+ * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
161
+ * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
162
+ */
163
+ const typeOptionMap = new Map([
164
+ ["any", _kubb_core.ast.schemaTypes.any],
165
+ ["unknown", _kubb_core.ast.schemaTypes.unknown],
166
+ ["void", _kubb_core.ast.schemaTypes.void]
167
+ ]);
168
+ //#endregion
36
169
  //#region ../../internals/utils/src/casing.ts
37
170
  /**
38
171
  * Shared implementation for camelCase and PascalCase conversion.
@@ -97,6 +230,23 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
97
230
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
98
231
  }
99
232
  //#endregion
233
+ //#region ../../internals/utils/src/fs.ts
234
+ /**
235
+ * Resolves to `true` when the file or directory at `path` exists.
236
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * if (await exists('./kubb.config.ts')) {
241
+ * const content = await read('./kubb.config.ts')
242
+ * }
243
+ * ```
244
+ */
245
+ async function exists(path) {
246
+ if (typeof Bun !== "undefined") return Bun.file(path).exists();
247
+ return (0, node_fs_promises.access)(path).then(() => true, () => false);
248
+ }
249
+ //#endregion
100
250
  //#region ../../internals/utils/src/object.ts
101
251
  /**
102
252
  * Returns `true` when `value` is a plain (non-null, non-array) object.
@@ -131,30 +281,6 @@ function mergeDeep(target, source) {
131
281
  return result;
132
282
  }
133
283
  //#endregion
134
- //#region ../../internals/utils/src/promise.ts
135
- /**
136
- * Returns a wrapper that caches the result of the first invocation and replays
137
- * it for every subsequent call, ignoring later arguments.
138
- *
139
- * Works for sync and async factories — for async, the cached value is the
140
- * promise itself, so concurrent callers share one in-flight execution and
141
- * cannot race each other.
142
- *
143
- * @example
144
- * ```ts
145
- * const loadDocument = once(async (path: string) => parse(await readFile(path)))
146
- * const a = loadDocument('./a.yaml') // parses
147
- * const b = loadDocument('./b.yaml') // returns the cached promise from the first call
148
- * ```
149
- */
150
- function once(factory) {
151
- let cache;
152
- return (...args) => {
153
- if (!cache) cache = { value: factory(...args) };
154
- return cache.value;
155
- };
156
- }
157
- //#endregion
158
284
  //#region ../../internals/utils/src/reserved.ts
159
285
  /**
160
286
  * JavaScript and Java reserved words.
@@ -405,126 +531,6 @@ var URLPath = class {
405
531
  }
406
532
  };
407
533
  //#endregion
408
- //#region src/constants.ts
409
- /**
410
- * Default parser options applied when no explicit options are provided.
411
- *
412
- * @example
413
- * ```ts
414
- * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
415
- *
416
- * const parser = createOasParser(oas)
417
- * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
418
- * ```
419
- */
420
- const DEFAULT_PARSER_OPTIONS = {
421
- dateType: "string",
422
- integerType: "bigint",
423
- unknownType: "any",
424
- emptySchemaType: "any",
425
- enumSuffix: "enum"
426
- };
427
- /**
428
- * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
429
- *
430
- * Used when building or parsing `$ref` strings.
431
- *
432
- * @example
433
- * ```ts
434
- * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
435
- * ```
436
- */
437
- const SCHEMA_REF_PREFIX = "#/components/schemas/";
438
- /**
439
- * OpenAPI version string written into the stub document created during multi-spec merges.
440
- */
441
- const MERGE_OPENAPI_VERSION = "3.0.0";
442
- /**
443
- * Fallback `info.title` placed in the stub document when merging multiple API files.
444
- */
445
- const MERGE_DEFAULT_TITLE = "Merged API";
446
- /**
447
- * Fallback `info.version` placed in the stub document when merging multiple API files.
448
- */
449
- const MERGE_DEFAULT_VERSION = "1.0.0";
450
- /**
451
- * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
452
- *
453
- * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
454
- * intersection member rather than being merged into the parent.
455
- *
456
- * @example
457
- * ```ts
458
- * import { structuralKeys } from '@kubb/adapter-oas'
459
- *
460
- * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
461
- * // true when fragment has e.g. 'properties' or 'oneOf'
462
- * ```
463
- */
464
- const structuralKeys = new Set([
465
- "properties",
466
- "items",
467
- "additionalProperties",
468
- "oneOf",
469
- "anyOf",
470
- "allOf",
471
- "not"
472
- ]);
473
- /**
474
- * Static map from OAS `format` strings to Kubb `SchemaType` values.
475
- *
476
- * Only formats whose AST type differs from the OAS `type` field appear here.
477
- * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
478
- * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
479
- * `idn-hostname` map to `'url'` as the closest generic string-format type.
480
- *
481
- * @example
482
- * ```ts
483
- * import { formatMap } from '@kubb/adapter-oas'
484
- *
485
- * formatMap['uuid'] // 'uuid'
486
- * formatMap['binary'] // 'blob'
487
- * formatMap['float'] // 'number'
488
- * ```
489
- */
490
- const formatMap = {
491
- uuid: "uuid",
492
- email: "email",
493
- "idn-email": "email",
494
- uri: "url",
495
- "uri-reference": "url",
496
- url: "url",
497
- ipv4: "ipv4",
498
- ipv6: "ipv6",
499
- hostname: "url",
500
- "idn-hostname": "url",
501
- binary: "blob",
502
- byte: "blob",
503
- int32: "integer",
504
- float: "number",
505
- double: "number"
506
- };
507
- /**
508
- * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
509
- *
510
- * @example
511
- * ```ts
512
- * import { enumExtensionKeys } from '@kubb/adapter-oas'
513
- *
514
- * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
515
- * ```
516
- */
517
- const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
518
- /**
519
- * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
520
- * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
521
- */
522
- const typeOptionMap = new Map([
523
- ["any", _kubb_core.ast.schemaTypes.any],
524
- ["unknown", _kubb_core.ast.schemaTypes.unknown],
525
- ["void", _kubb_core.ast.schemaTypes.void]
526
- ]);
527
- //#endregion
528
534
  //#region src/guards.ts
529
535
  /**
530
536
  * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
@@ -634,7 +640,13 @@ async function mergeDocuments(pathOrApi) {
634
640
  enablePaths: false,
635
641
  canBundle: false
636
642
  })));
637
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
643
+ if (documents.length === 0) throw new _kubb_core.DiagnosticError({
644
+ code: _kubb_core.diagnosticCode.inputRequired,
645
+ severity: "error",
646
+ message: "No OAS documents were provided for merging.",
647
+ help: "Pass at least one path or document to `input.path`.",
648
+ location: { kind: "config" }
649
+ });
638
650
  const seed = {
639
651
  openapi: MERGE_OPENAPI_VERSION,
640
652
  info: {
@@ -650,9 +662,9 @@ async function mergeDocuments(pathOrApi) {
650
662
  * Creates a `Document` from an `AdapterSource`.
651
663
  *
652
664
  * Handles all three source types:
653
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
654
- * - `{ type: 'paths' }` merges multiple file paths into a single document.
655
- * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
665
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
666
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
667
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
656
668
  *
657
669
  * @example
658
670
  * ```ts
@@ -660,14 +672,31 @@ async function mergeDocuments(pathOrApi) {
660
672
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
661
673
  * ```
662
674
  */
663
- function parseFromConfig(source) {
675
+ async function parseFromConfig(source) {
664
676
  if (source.type === "data") {
665
677
  if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
666
678
  return parseDocument(source.data, { canBundle: false });
667
679
  }
668
680
  if (source.type === "paths") return mergeDocuments(source.paths);
669
681
  if (new URLPath(source.path).isURL) return parseDocument(source.path);
670
- return parseDocument(node_path.default.resolve(node_path.default.dirname(source.path), source.path));
682
+ const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
683
+ await assertInputExists(resolved);
684
+ return parseDocument(resolved);
685
+ }
686
+ /**
687
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
688
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
689
+ * its parse error instead.
690
+ */
691
+ async function assertInputExists(input) {
692
+ if (new URLPath(input).isURL) return;
693
+ if (!await exists(input)) throw new _kubb_core.DiagnosticError({
694
+ code: _kubb_core.diagnosticCode.inputNotFound,
695
+ severity: "error",
696
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
697
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
698
+ location: { kind: "config" }
699
+ });
671
700
  }
672
701
  /**
673
702
  * Validates an OpenAPI document using `oas-normalize` with colorized error output.
@@ -714,7 +743,21 @@ function resolveRef(document, $ref) {
714
743
  }
715
744
  if (docCache.has($ref)) return docCache.get($ref);
716
745
  const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
717
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
746
+ if (!current) {
747
+ const diagnostic = {
748
+ code: _kubb_core.diagnosticCode.refNotFound,
749
+ severity: "error",
750
+ message: `Could not find a definition for ${origRef}.`,
751
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
752
+ location: {
753
+ kind: "schema",
754
+ pointer: origRef,
755
+ ref: origRef
756
+ }
757
+ };
758
+ if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.DiagnosticError(diagnostic);
759
+ return null;
760
+ }
718
761
  docCache.set($ref, current);
719
762
  return current;
720
763
  }
@@ -741,13 +784,13 @@ function dereferenceWithRef(document, schema) {
741
784
  //#endregion
742
785
  //#region src/dialect.ts
743
786
  /**
744
- * The OpenAPI / Swagger dialect the default used by `@kubb/adapter-oas`.
787
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
745
788
  *
746
789
  * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
747
790
  * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
748
791
  * ref resolution) so the converter pipeline and dispatch rules stay shared. A
749
- * future adapter (e.g. AsyncAPI) ships its own dialect `type: ['null', …]`
750
- * nullability, no discriminator object, binary via `contentEncoding` and reuses
792
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
793
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
751
794
  * the rest unchanged.
752
795
  *
753
796
  * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
@@ -789,7 +832,16 @@ function resolveServerUrl(server, overrides) {
789
832
  for (const [key, variable] of Object.entries(server.variables)) {
790
833
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
791
834
  if (value === void 0) continue;
792
- if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
835
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.DiagnosticError({
836
+ code: _kubb_core.diagnosticCode.invalidServerVariable,
837
+ severity: "error",
838
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
839
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
840
+ location: {
841
+ kind: "document",
842
+ pointer: "#/servers"
843
+ }
844
+ });
793
845
  url = url.replaceAll(`{${key}}`, value);
794
846
  }
795
847
  return url;
@@ -802,6 +854,15 @@ function getSchemaType(format) {
802
854
  return formatMap[format] ?? null;
803
855
  }
804
856
  /**
857
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
858
+ * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
859
+ * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
860
+ * diagnostic in step with the parser as `formatMap` grows.
861
+ */
862
+ function isHandledFormat(format) {
863
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
864
+ }
865
+ /**
805
866
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
806
867
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
807
868
  */
@@ -898,7 +959,7 @@ function getRequestSchema(document, operation, options = {}) {
898
959
  /**
899
960
  * Flattens a keyword-only `allOf` into its parent schema.
900
961
  *
901
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
962
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
902
963
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
903
964
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
904
965
  *
@@ -908,7 +969,7 @@ function getRequestSchema(document, operation, options = {}) {
908
969
  * // { type: 'object', properties: {}, description: 'A pet' }
909
970
  *
910
971
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
911
- * // returned unchanged contains a $ref
972
+ * // returned unchanged, contains a $ref
912
973
  * ```
913
974
  */
914
975
  /**
@@ -934,7 +995,7 @@ function flattenSchema(schema) {
934
995
  /**
935
996
  * Extracts the inline schema from a media-type `content` map.
936
997
  *
937
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
998
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
938
999
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
939
1000
  *
940
1001
  * @example
@@ -1126,9 +1187,9 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1126
1187
  /**
1127
1188
  * Returns all request body content type keys for an operation.
1128
1189
  *
1129
- * The requestBody is dereferenced **in-place** when it is a `$ref` the same mutation
1130
- * that `getRequestSchema` already performs so that the returned list accurately reflects
1131
- * the available content types even for referenced bodies.
1190
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1191
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1192
+ * available content types even for referenced bodies.
1132
1193
  *
1133
1194
  * @example
1134
1195
  * ```ts
@@ -1145,7 +1206,7 @@ function getRequestBodyContentTypes(document, operation) {
1145
1206
  /**
1146
1207
  * Returns all response content type keys for an operation at a given status code.
1147
1208
  *
1148
- * Response `$ref`s are resolved in-place first the same mutation `getResponseSchema` performs
1209
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1149
1210
  * so the returned list reflects the available content types even for referenced responses.
1150
1211
  *
1151
1212
  * @example
@@ -1205,16 +1266,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1205
1266
  */
1206
1267
  const resolvingRefs = /* @__PURE__ */ new Set();
1207
1268
  /**
1208
- * Cache of already-resolved `$ref` schemas within this parser instance.
1269
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1209
1270
  *
1210
- * Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
1211
- * every time it appears as a `$ref` in a different parent schema. In heavily
1212
- * cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
1213
- * blowup `customer` alone may be referenced from dozens of top-level schemas,
1214
- * each triggering a fresh recursive expansion of its entire sub-tree.
1215
- *
1216
- * Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
1217
- * where N is the number of unique schema names.
1271
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1272
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1273
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1274
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1218
1275
  */
1219
1276
  const resolvedRefCache = /* @__PURE__ */ new Map();
1220
1277
  /**
@@ -1752,7 +1809,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1752
1809
  /**
1753
1810
  * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
1754
1811
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1755
- * `type`. The first matching rule that produces a node wins; see {@link SchemaRule} for the
1812
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1756
1813
  * match/convert/fall-through contract.
1757
1814
  */
1758
1815
  const schemaRules = [
@@ -2097,6 +2154,61 @@ function patchDiscriminatorNode(node, entry) {
2097
2154
  };
2098
2155
  }
2099
2156
  //#endregion
2157
+ //#region src/schemaDiagnostics.ts
2158
+ /**
2159
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2160
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2161
+ * pointer as it descends so a nested field reports against its full path
2162
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2163
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2164
+ * no-op outside one, and repeats are deduped by the build.
2165
+ */
2166
+ function reportSchemaDiagnostics({ node, name }) {
2167
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2168
+ }
2169
+ /**
2170
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2171
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2172
+ */
2173
+ function escapePointerToken(token) {
2174
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2175
+ }
2176
+ function visit(node, pointer) {
2177
+ if (node.deprecated) _kubb_core.Diagnostics.report({
2178
+ code: _kubb_core.diagnosticCode.deprecated,
2179
+ severity: "info",
2180
+ message: "This schema is marked as deprecated.",
2181
+ location: {
2182
+ kind: "schema",
2183
+ pointer
2184
+ }
2185
+ });
2186
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) _kubb_core.Diagnostics.report({
2187
+ code: _kubb_core.diagnosticCode.unsupportedFormat,
2188
+ severity: "warning",
2189
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2190
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2191
+ location: {
2192
+ kind: "schema",
2193
+ pointer
2194
+ }
2195
+ });
2196
+ if (node.type === "object") {
2197
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2198
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2199
+ return;
2200
+ }
2201
+ if (node.type === "array") {
2202
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2203
+ return;
2204
+ }
2205
+ if (node.type === "tuple") {
2206
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2207
+ return;
2208
+ }
2209
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2210
+ }
2211
+ //#endregion
2100
2212
  //#region src/stream.ts
2101
2213
  /**
2102
2214
  * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
@@ -2156,7 +2268,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2156
2268
  * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2157
2269
  * Both are proportional to the number of aliases or discriminator parents, not total schema count.
2158
2270
  *
2159
- * Each schema is parsed again during the streaming pass this is intentional.
2271
+ * Each schema is parsed again during the streaming pass. This is intentional.
2160
2272
  * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
2161
2273
  *
2162
2274
  * @example
@@ -2180,6 +2292,10 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2180
2292
  name
2181
2293
  }, parserOptions);
2182
2294
  allNodes.push(node);
2295
+ reportSchemaDiagnostics({
2296
+ node,
2297
+ name
2298
+ });
2183
2299
  if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2184
2300
  if (_kubb_core.ast.narrowSchema(node, _kubb_core.ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2185
2301
  if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
@@ -2320,37 +2436,72 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2320
2436
  };
2321
2437
  let nameMapping = /* @__PURE__ */ new Map();
2322
2438
  let parsedDocument = null;
2323
- const ensureDocument = once(async (source) => {
2324
- const fresh = await parseFromConfig(source);
2325
- if (validate) await validateDocument(fresh);
2326
- parsedDocument = fresh;
2327
- return fresh;
2328
- });
2329
- const ensureSchemas = once(async (document) => {
2330
- const result = getSchemas(document, { contentType });
2331
- nameMapping = result.nameMapping;
2332
- return result.schemas;
2333
- });
2334
- const ensureBaseOas = once((document) => new oas.default(document));
2335
- const ensureSchemaParser = once((document) => createSchemaParser({
2336
- document,
2337
- contentType
2338
- }));
2339
- const ensurePreScan = once((schemas, parseSchema, parseOperation, baseOas) => preScan({
2340
- schemas,
2341
- parseSchema,
2342
- parseOperation,
2343
- baseOas,
2344
- parserOptions,
2345
- discriminator,
2346
- dedupe
2347
- }));
2439
+ const documentCache = /* @__PURE__ */ new WeakMap();
2440
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2441
+ const baseOasCache = /* @__PURE__ */ new WeakMap();
2442
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2443
+ const preScanCache = /* @__PURE__ */ new WeakMap();
2444
+ function ensureDocument(source) {
2445
+ const cached = documentCache.get(source);
2446
+ if (cached) return cached;
2447
+ const promise = (async () => {
2448
+ const fresh = await parseFromConfig(source);
2449
+ if (validate) await validateDocument(fresh);
2450
+ parsedDocument = fresh;
2451
+ return fresh;
2452
+ })();
2453
+ documentCache.set(source, promise);
2454
+ return promise;
2455
+ }
2456
+ function ensureSchemas(document) {
2457
+ const cached = schemasCache.get(document);
2458
+ if (cached) return cached;
2459
+ const promise = Promise.resolve().then(() => {
2460
+ const result = getSchemas(document, { contentType });
2461
+ nameMapping = result.nameMapping;
2462
+ return result.schemas;
2463
+ });
2464
+ schemasCache.set(document, promise);
2465
+ return promise;
2466
+ }
2467
+ function ensureBaseOas(document) {
2468
+ const cached = baseOasCache.get(document);
2469
+ if (cached) return cached;
2470
+ const baseOas = new oas.default(document);
2471
+ baseOasCache.set(document, baseOas);
2472
+ return baseOas;
2473
+ }
2474
+ function ensureSchemaParser(document) {
2475
+ const cached = schemaParserCache.get(document);
2476
+ if (cached) return cached;
2477
+ const parser = createSchemaParser({
2478
+ document,
2479
+ contentType
2480
+ });
2481
+ schemaParserCache.set(document, parser);
2482
+ return parser;
2483
+ }
2484
+ function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
2485
+ const cached = preScanCache.get(document);
2486
+ if (cached) return cached;
2487
+ const result = preScan({
2488
+ schemas,
2489
+ parseSchema,
2490
+ parseOperation,
2491
+ baseOas,
2492
+ parserOptions,
2493
+ discriminator,
2494
+ dedupe
2495
+ });
2496
+ preScanCache.set(document, result);
2497
+ return result;
2498
+ }
2348
2499
  async function createStream(source) {
2349
2500
  const document = await ensureDocument(source);
2350
2501
  const schemas = await ensureSchemas(document);
2351
2502
  const { parseSchema, parseOperation } = ensureSchemaParser(document);
2352
2503
  const baseOas = ensureBaseOas(document);
2353
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas);
2504
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
2354
2505
  return createInputStream({
2355
2506
  schemas,
2356
2507
  parseSchema,
@@ -2396,6 +2547,7 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2396
2547
  return parsedDocument;
2397
2548
  },
2398
2549
  async validate(input, options) {
2550
+ await assertInputExists(input);
2399
2551
  await validateDocument(await parseDocument(input), options);
2400
2552
  },
2401
2553
  getImports(node, resolve) {