@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.js CHANGED
@@ -1,12 +1,145 @@
1
1
  import "./chunk-C0LytTxp.js";
2
- import path from "node:path";
3
- import { ast, createAdapter } from "@kubb/core";
2
+ import { DiagnosticError, Diagnostics, ast, createAdapter, diagnosticCode } from "@kubb/core";
4
3
  import BaseOas from "oas";
4
+ import path from "node:path";
5
+ import { access } from "node:fs/promises";
5
6
  import { bundle, loadConfig } from "@redocly/openapi-core";
6
7
  import OASNormalize from "oas-normalize";
7
8
  import swagger2openapi from "swagger2openapi";
8
9
  import { isRef } from "oas/types";
9
10
  import { matchesMimeType } from "oas/utils";
11
+ //#region src/constants.ts
12
+ /**
13
+ * Default parser options applied when no explicit options are provided.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
18
+ *
19
+ * const parser = createOasParser(oas)
20
+ * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
21
+ * ```
22
+ */
23
+ const DEFAULT_PARSER_OPTIONS = {
24
+ dateType: "string",
25
+ integerType: "bigint",
26
+ unknownType: "any",
27
+ emptySchemaType: "any",
28
+ enumSuffix: "enum"
29
+ };
30
+ /**
31
+ * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
32
+ *
33
+ * Used when building or parsing `$ref` strings.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
38
+ * ```
39
+ */
40
+ const SCHEMA_REF_PREFIX = "#/components/schemas/";
41
+ /**
42
+ * OpenAPI version string written into the stub document created during multi-spec merges.
43
+ */
44
+ const MERGE_OPENAPI_VERSION = "3.0.0";
45
+ /**
46
+ * Fallback `info.title` placed in the stub document when merging multiple API files.
47
+ */
48
+ const MERGE_DEFAULT_TITLE = "Merged API";
49
+ /**
50
+ * Fallback `info.version` placed in the stub document when merging multiple API files.
51
+ */
52
+ const MERGE_DEFAULT_VERSION = "1.0.0";
53
+ /**
54
+ * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
55
+ *
56
+ * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
57
+ * intersection member rather than being merged into the parent.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { structuralKeys } from '@kubb/adapter-oas'
62
+ *
63
+ * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
64
+ * // true when fragment has e.g. 'properties' or 'oneOf'
65
+ * ```
66
+ */
67
+ const structuralKeys = new Set([
68
+ "properties",
69
+ "items",
70
+ "additionalProperties",
71
+ "oneOf",
72
+ "anyOf",
73
+ "allOf",
74
+ "not"
75
+ ]);
76
+ /**
77
+ * Static map from OAS `format` strings to Kubb `SchemaType` values.
78
+ *
79
+ * Only formats whose AST type differs from the OAS `type` field appear here.
80
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
81
+ * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
82
+ * `idn-hostname` map to `'url'` as the closest generic string-format type.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { formatMap } from '@kubb/adapter-oas'
87
+ *
88
+ * formatMap['uuid'] // 'uuid'
89
+ * formatMap['binary'] // 'blob'
90
+ * formatMap['float'] // 'number'
91
+ * ```
92
+ */
93
+ /**
94
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
95
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
96
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
97
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
98
+ */
99
+ const specialCasedFormats = new Set([
100
+ "int64",
101
+ "date-time",
102
+ "date",
103
+ "time"
104
+ ]);
105
+ const formatMap = {
106
+ uuid: "uuid",
107
+ email: "email",
108
+ "idn-email": "email",
109
+ uri: "url",
110
+ "uri-reference": "url",
111
+ url: "url",
112
+ ipv4: "ipv4",
113
+ ipv6: "ipv6",
114
+ hostname: "url",
115
+ "idn-hostname": "url",
116
+ binary: "blob",
117
+ byte: "blob",
118
+ int32: "integer",
119
+ float: "number",
120
+ double: "number"
121
+ };
122
+ /**
123
+ * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * import { enumExtensionKeys } from '@kubb/adapter-oas'
128
+ *
129
+ * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
130
+ * ```
131
+ */
132
+ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
133
+ /**
134
+ * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
135
+ * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
136
+ */
137
+ const typeOptionMap = new Map([
138
+ ["any", ast.schemaTypes.any],
139
+ ["unknown", ast.schemaTypes.unknown],
140
+ ["void", ast.schemaTypes.void]
141
+ ]);
142
+ //#endregion
10
143
  //#region ../../internals/utils/src/casing.ts
11
144
  /**
12
145
  * Shared implementation for camelCase and PascalCase conversion.
@@ -71,6 +204,23 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
71
204
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
72
205
  }
73
206
  //#endregion
207
+ //#region ../../internals/utils/src/fs.ts
208
+ /**
209
+ * Resolves to `true` when the file or directory at `path` exists.
210
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * if (await exists('./kubb.config.ts')) {
215
+ * const content = await read('./kubb.config.ts')
216
+ * }
217
+ * ```
218
+ */
219
+ async function exists(path) {
220
+ if (typeof Bun !== "undefined") return Bun.file(path).exists();
221
+ return access(path).then(() => true, () => false);
222
+ }
223
+ //#endregion
74
224
  //#region ../../internals/utils/src/object.ts
75
225
  /**
76
226
  * Returns `true` when `value` is a plain (non-null, non-array) object.
@@ -105,30 +255,6 @@ function mergeDeep(target, source) {
105
255
  return result;
106
256
  }
107
257
  //#endregion
108
- //#region ../../internals/utils/src/promise.ts
109
- /**
110
- * Returns a wrapper that caches the result of the first invocation and replays
111
- * it for every subsequent call, ignoring later arguments.
112
- *
113
- * Works for sync and async factories — for async, the cached value is the
114
- * promise itself, so concurrent callers share one in-flight execution and
115
- * cannot race each other.
116
- *
117
- * @example
118
- * ```ts
119
- * const loadDocument = once(async (path: string) => parse(await readFile(path)))
120
- * const a = loadDocument('./a.yaml') // parses
121
- * const b = loadDocument('./b.yaml') // returns the cached promise from the first call
122
- * ```
123
- */
124
- function once(factory) {
125
- let cache;
126
- return (...args) => {
127
- if (!cache) cache = { value: factory(...args) };
128
- return cache.value;
129
- };
130
- }
131
- //#endregion
132
258
  //#region ../../internals/utils/src/reserved.ts
133
259
  /**
134
260
  * JavaScript and Java reserved words.
@@ -379,126 +505,6 @@ var URLPath = class {
379
505
  }
380
506
  };
381
507
  //#endregion
382
- //#region src/constants.ts
383
- /**
384
- * Default parser options applied when no explicit options are provided.
385
- *
386
- * @example
387
- * ```ts
388
- * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
389
- *
390
- * const parser = createOasParser(oas)
391
- * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
392
- * ```
393
- */
394
- const DEFAULT_PARSER_OPTIONS = {
395
- dateType: "string",
396
- integerType: "bigint",
397
- unknownType: "any",
398
- emptySchemaType: "any",
399
- enumSuffix: "enum"
400
- };
401
- /**
402
- * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
403
- *
404
- * Used when building or parsing `$ref` strings.
405
- *
406
- * @example
407
- * ```ts
408
- * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
409
- * ```
410
- */
411
- const SCHEMA_REF_PREFIX = "#/components/schemas/";
412
- /**
413
- * OpenAPI version string written into the stub document created during multi-spec merges.
414
- */
415
- const MERGE_OPENAPI_VERSION = "3.0.0";
416
- /**
417
- * Fallback `info.title` placed in the stub document when merging multiple API files.
418
- */
419
- const MERGE_DEFAULT_TITLE = "Merged API";
420
- /**
421
- * Fallback `info.version` placed in the stub document when merging multiple API files.
422
- */
423
- const MERGE_DEFAULT_VERSION = "1.0.0";
424
- /**
425
- * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
426
- *
427
- * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
428
- * intersection member rather than being merged into the parent.
429
- *
430
- * @example
431
- * ```ts
432
- * import { structuralKeys } from '@kubb/adapter-oas'
433
- *
434
- * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
435
- * // true when fragment has e.g. 'properties' or 'oneOf'
436
- * ```
437
- */
438
- const structuralKeys = new Set([
439
- "properties",
440
- "items",
441
- "additionalProperties",
442
- "oneOf",
443
- "anyOf",
444
- "allOf",
445
- "not"
446
- ]);
447
- /**
448
- * Static map from OAS `format` strings to Kubb `SchemaType` values.
449
- *
450
- * Only formats whose AST type differs from the OAS `type` field appear here.
451
- * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
452
- * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
453
- * `idn-hostname` map to `'url'` as the closest generic string-format type.
454
- *
455
- * @example
456
- * ```ts
457
- * import { formatMap } from '@kubb/adapter-oas'
458
- *
459
- * formatMap['uuid'] // 'uuid'
460
- * formatMap['binary'] // 'blob'
461
- * formatMap['float'] // 'number'
462
- * ```
463
- */
464
- const formatMap = {
465
- uuid: "uuid",
466
- email: "email",
467
- "idn-email": "email",
468
- uri: "url",
469
- "uri-reference": "url",
470
- url: "url",
471
- ipv4: "ipv4",
472
- ipv6: "ipv6",
473
- hostname: "url",
474
- "idn-hostname": "url",
475
- binary: "blob",
476
- byte: "blob",
477
- int32: "integer",
478
- float: "number",
479
- double: "number"
480
- };
481
- /**
482
- * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
483
- *
484
- * @example
485
- * ```ts
486
- * import { enumExtensionKeys } from '@kubb/adapter-oas'
487
- *
488
- * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
489
- * ```
490
- */
491
- const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
492
- /**
493
- * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
494
- * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
495
- */
496
- const typeOptionMap = new Map([
497
- ["any", ast.schemaTypes.any],
498
- ["unknown", ast.schemaTypes.unknown],
499
- ["void", ast.schemaTypes.void]
500
- ]);
501
- //#endregion
502
508
  //#region src/guards.ts
503
509
  /**
504
510
  * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
@@ -608,7 +614,13 @@ async function mergeDocuments(pathOrApi) {
608
614
  enablePaths: false,
609
615
  canBundle: false
610
616
  })));
611
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
617
+ if (documents.length === 0) throw new DiagnosticError({
618
+ code: diagnosticCode.inputRequired,
619
+ severity: "error",
620
+ message: "No OAS documents were provided for merging.",
621
+ help: "Pass at least one path or document to `input.path`.",
622
+ location: { kind: "config" }
623
+ });
612
624
  const seed = {
613
625
  openapi: MERGE_OPENAPI_VERSION,
614
626
  info: {
@@ -624,9 +636,9 @@ async function mergeDocuments(pathOrApi) {
624
636
  * Creates a `Document` from an `AdapterSource`.
625
637
  *
626
638
  * Handles all three source types:
627
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
628
- * - `{ type: 'paths' }` merges multiple file paths into a single document.
629
- * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
639
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
640
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
641
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
630
642
  *
631
643
  * @example
632
644
  * ```ts
@@ -634,14 +646,31 @@ async function mergeDocuments(pathOrApi) {
634
646
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
635
647
  * ```
636
648
  */
637
- function parseFromConfig(source) {
649
+ async function parseFromConfig(source) {
638
650
  if (source.type === "data") {
639
651
  if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
640
652
  return parseDocument(source.data, { canBundle: false });
641
653
  }
642
654
  if (source.type === "paths") return mergeDocuments(source.paths);
643
655
  if (new URLPath(source.path).isURL) return parseDocument(source.path);
644
- return parseDocument(path.resolve(path.dirname(source.path), source.path));
656
+ const resolved = path.resolve(path.dirname(source.path), source.path);
657
+ await assertInputExists(resolved);
658
+ return parseDocument(resolved);
659
+ }
660
+ /**
661
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
662
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
663
+ * its parse error instead.
664
+ */
665
+ async function assertInputExists(input) {
666
+ if (new URLPath(input).isURL) return;
667
+ if (!await exists(input)) throw new DiagnosticError({
668
+ code: diagnosticCode.inputNotFound,
669
+ severity: "error",
670
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
671
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
672
+ location: { kind: "config" }
673
+ });
645
674
  }
646
675
  /**
647
676
  * Validates an OpenAPI document using `oas-normalize` with colorized error output.
@@ -688,7 +717,21 @@ function resolveRef(document, $ref) {
688
717
  }
689
718
  if (docCache.has($ref)) return docCache.get($ref);
690
719
  const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
691
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
720
+ if (!current) {
721
+ const diagnostic = {
722
+ code: diagnosticCode.refNotFound,
723
+ severity: "error",
724
+ message: `Could not find a definition for ${origRef}.`,
725
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
726
+ location: {
727
+ kind: "schema",
728
+ pointer: origRef,
729
+ ref: origRef
730
+ }
731
+ };
732
+ if (!Diagnostics.report(diagnostic)) throw new DiagnosticError(diagnostic);
733
+ return null;
734
+ }
692
735
  docCache.set($ref, current);
693
736
  return current;
694
737
  }
@@ -715,13 +758,13 @@ function dereferenceWithRef(document, schema) {
715
758
  //#endregion
716
759
  //#region src/dialect.ts
717
760
  /**
718
- * The OpenAPI / Swagger dialect the default used by `@kubb/adapter-oas`.
761
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
719
762
  *
720
763
  * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
721
764
  * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
722
765
  * ref resolution) so the converter pipeline and dispatch rules stay shared. A
723
- * future adapter (e.g. AsyncAPI) ships its own dialect `type: ['null', …]`
724
- * nullability, no discriminator object, binary via `contentEncoding` and reuses
766
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
767
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
725
768
  * the rest unchanged.
726
769
  *
727
770
  * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
@@ -763,7 +806,16 @@ function resolveServerUrl(server, overrides) {
763
806
  for (const [key, variable] of Object.entries(server.variables)) {
764
807
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
765
808
  if (value === void 0) continue;
766
- 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(", ")}.`);
809
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new DiagnosticError({
810
+ code: diagnosticCode.invalidServerVariable,
811
+ severity: "error",
812
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
813
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
814
+ location: {
815
+ kind: "document",
816
+ pointer: "#/servers"
817
+ }
818
+ });
767
819
  url = url.replaceAll(`{${key}}`, value);
768
820
  }
769
821
  return url;
@@ -776,6 +828,15 @@ function getSchemaType(format) {
776
828
  return formatMap[format] ?? null;
777
829
  }
778
830
  /**
831
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
832
+ * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
833
+ * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
834
+ * diagnostic in step with the parser as `formatMap` grows.
835
+ */
836
+ function isHandledFormat(format) {
837
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
838
+ }
839
+ /**
779
840
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
780
841
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
781
842
  */
@@ -872,7 +933,7 @@ function getRequestSchema(document, operation, options = {}) {
872
933
  /**
873
934
  * Flattens a keyword-only `allOf` into its parent schema.
874
935
  *
875
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
936
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
876
937
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
877
938
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
878
939
  *
@@ -882,7 +943,7 @@ function getRequestSchema(document, operation, options = {}) {
882
943
  * // { type: 'object', properties: {}, description: 'A pet' }
883
944
  *
884
945
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
885
- * // returned unchanged contains a $ref
946
+ * // returned unchanged, contains a $ref
886
947
  * ```
887
948
  */
888
949
  /**
@@ -908,7 +969,7 @@ function flattenSchema(schema) {
908
969
  /**
909
970
  * Extracts the inline schema from a media-type `content` map.
910
971
  *
911
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
972
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
912
973
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
913
974
  *
914
975
  * @example
@@ -1100,9 +1161,9 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1100
1161
  /**
1101
1162
  * Returns all request body content type keys for an operation.
1102
1163
  *
1103
- * The requestBody is dereferenced **in-place** when it is a `$ref` the same mutation
1104
- * that `getRequestSchema` already performs so that the returned list accurately reflects
1105
- * the available content types even for referenced bodies.
1164
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1165
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1166
+ * available content types even for referenced bodies.
1106
1167
  *
1107
1168
  * @example
1108
1169
  * ```ts
@@ -1119,7 +1180,7 @@ function getRequestBodyContentTypes(document, operation) {
1119
1180
  /**
1120
1181
  * Returns all response content type keys for an operation at a given status code.
1121
1182
  *
1122
- * Response `$ref`s are resolved in-place first the same mutation `getResponseSchema` performs
1183
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1123
1184
  * so the returned list reflects the available content types even for referenced responses.
1124
1185
  *
1125
1186
  * @example
@@ -1179,16 +1240,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1179
1240
  */
1180
1241
  const resolvingRefs = /* @__PURE__ */ new Set();
1181
1242
  /**
1182
- * Cache of already-resolved `$ref` schemas within this parser instance.
1243
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1183
1244
  *
1184
- * Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
1185
- * every time it appears as a `$ref` in a different parent schema. In heavily
1186
- * cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
1187
- * blowup `customer` alone may be referenced from dozens of top-level schemas,
1188
- * each triggering a fresh recursive expansion of its entire sub-tree.
1189
- *
1190
- * Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
1191
- * where N is the number of unique schema names.
1245
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1246
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1247
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1248
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1192
1249
  */
1193
1250
  const resolvedRefCache = /* @__PURE__ */ new Map();
1194
1251
  /**
@@ -1726,7 +1783,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1726
1783
  /**
1727
1784
  * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
1728
1785
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1729
- * `type`. The first matching rule that produces a node wins; see {@link SchemaRule} for the
1786
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1730
1787
  * match/convert/fall-through contract.
1731
1788
  */
1732
1789
  const schemaRules = [
@@ -2071,6 +2128,61 @@ function patchDiscriminatorNode(node, entry) {
2071
2128
  };
2072
2129
  }
2073
2130
  //#endregion
2131
+ //#region src/schemaDiagnostics.ts
2132
+ /**
2133
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2134
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2135
+ * pointer as it descends so a nested field reports against its full path
2136
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2137
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2138
+ * no-op outside one, and repeats are deduped by the build.
2139
+ */
2140
+ function reportSchemaDiagnostics({ node, name }) {
2141
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2142
+ }
2143
+ /**
2144
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2145
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2146
+ */
2147
+ function escapePointerToken(token) {
2148
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2149
+ }
2150
+ function visit(node, pointer) {
2151
+ if (node.deprecated) Diagnostics.report({
2152
+ code: diagnosticCode.deprecated,
2153
+ severity: "info",
2154
+ message: "This schema is marked as deprecated.",
2155
+ location: {
2156
+ kind: "schema",
2157
+ pointer
2158
+ }
2159
+ });
2160
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
2161
+ code: diagnosticCode.unsupportedFormat,
2162
+ severity: "warning",
2163
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2164
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2165
+ location: {
2166
+ kind: "schema",
2167
+ pointer
2168
+ }
2169
+ });
2170
+ if (node.type === "object") {
2171
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2172
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2173
+ return;
2174
+ }
2175
+ if (node.type === "array") {
2176
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2177
+ return;
2178
+ }
2179
+ if (node.type === "tuple") {
2180
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2181
+ return;
2182
+ }
2183
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2184
+ }
2185
+ //#endregion
2074
2186
  //#region src/stream.ts
2075
2187
  /**
2076
2188
  * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
@@ -2130,7 +2242,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2130
2242
  * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2131
2243
  * Both are proportional to the number of aliases or discriminator parents, not total schema count.
2132
2244
  *
2133
- * Each schema is parsed again during the streaming pass this is intentional.
2245
+ * Each schema is parsed again during the streaming pass. This is intentional.
2134
2246
  * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
2135
2247
  *
2136
2248
  * @example
@@ -2154,6 +2266,10 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2154
2266
  name
2155
2267
  }, parserOptions);
2156
2268
  allNodes.push(node);
2269
+ reportSchemaDiagnostics({
2270
+ node,
2271
+ name
2272
+ });
2157
2273
  if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2158
2274
  if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2159
2275
  if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
@@ -2294,37 +2410,72 @@ const adapterOas = createAdapter((options) => {
2294
2410
  };
2295
2411
  let nameMapping = /* @__PURE__ */ new Map();
2296
2412
  let parsedDocument = null;
2297
- const ensureDocument = once(async (source) => {
2298
- const fresh = await parseFromConfig(source);
2299
- if (validate) await validateDocument(fresh);
2300
- parsedDocument = fresh;
2301
- return fresh;
2302
- });
2303
- const ensureSchemas = once(async (document) => {
2304
- const result = getSchemas(document, { contentType });
2305
- nameMapping = result.nameMapping;
2306
- return result.schemas;
2307
- });
2308
- const ensureBaseOas = once((document) => new BaseOas(document));
2309
- const ensureSchemaParser = once((document) => createSchemaParser({
2310
- document,
2311
- contentType
2312
- }));
2313
- const ensurePreScan = once((schemas, parseSchema, parseOperation, baseOas) => preScan({
2314
- schemas,
2315
- parseSchema,
2316
- parseOperation,
2317
- baseOas,
2318
- parserOptions,
2319
- discriminator,
2320
- dedupe
2321
- }));
2413
+ const documentCache = /* @__PURE__ */ new WeakMap();
2414
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2415
+ const baseOasCache = /* @__PURE__ */ new WeakMap();
2416
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2417
+ const preScanCache = /* @__PURE__ */ new WeakMap();
2418
+ function ensureDocument(source) {
2419
+ const cached = documentCache.get(source);
2420
+ if (cached) return cached;
2421
+ const promise = (async () => {
2422
+ const fresh = await parseFromConfig(source);
2423
+ if (validate) await validateDocument(fresh);
2424
+ parsedDocument = fresh;
2425
+ return fresh;
2426
+ })();
2427
+ documentCache.set(source, promise);
2428
+ return promise;
2429
+ }
2430
+ function ensureSchemas(document) {
2431
+ const cached = schemasCache.get(document);
2432
+ if (cached) return cached;
2433
+ const promise = Promise.resolve().then(() => {
2434
+ const result = getSchemas(document, { contentType });
2435
+ nameMapping = result.nameMapping;
2436
+ return result.schemas;
2437
+ });
2438
+ schemasCache.set(document, promise);
2439
+ return promise;
2440
+ }
2441
+ function ensureBaseOas(document) {
2442
+ const cached = baseOasCache.get(document);
2443
+ if (cached) return cached;
2444
+ const baseOas = new BaseOas(document);
2445
+ baseOasCache.set(document, baseOas);
2446
+ return baseOas;
2447
+ }
2448
+ function ensureSchemaParser(document) {
2449
+ const cached = schemaParserCache.get(document);
2450
+ if (cached) return cached;
2451
+ const parser = createSchemaParser({
2452
+ document,
2453
+ contentType
2454
+ });
2455
+ schemaParserCache.set(document, parser);
2456
+ return parser;
2457
+ }
2458
+ function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
2459
+ const cached = preScanCache.get(document);
2460
+ if (cached) return cached;
2461
+ const result = preScan({
2462
+ schemas,
2463
+ parseSchema,
2464
+ parseOperation,
2465
+ baseOas,
2466
+ parserOptions,
2467
+ discriminator,
2468
+ dedupe
2469
+ });
2470
+ preScanCache.set(document, result);
2471
+ return result;
2472
+ }
2322
2473
  async function createStream(source) {
2323
2474
  const document = await ensureDocument(source);
2324
2475
  const schemas = await ensureSchemas(document);
2325
2476
  const { parseSchema, parseOperation } = ensureSchemaParser(document);
2326
2477
  const baseOas = ensureBaseOas(document);
2327
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas);
2478
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
2328
2479
  return createInputStream({
2329
2480
  schemas,
2330
2481
  parseSchema,
@@ -2370,6 +2521,7 @@ const adapterOas = createAdapter((options) => {
2370
2521
  return parsedDocument;
2371
2522
  },
2372
2523
  async validate(input, options) {
2524
+ await assertInputExists(input);
2373
2525
  await validateDocument(await parseDocument(input), options);
2374
2526
  },
2375
2527
  getImports(node, resolve) {