@kubb/adapter-oas 5.0.0-beta.35 → 5.0.0-beta.37

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,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let node_fs_promises = require("node:fs/promises");
24
25
  let node_path = require("node:path");
25
26
  node_path = __toESM(node_path, 1);
26
27
  let _kubb_core = require("@kubb/core");
@@ -97,6 +98,23 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
97
98
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
98
99
  }
99
100
  //#endregion
101
+ //#region ../../internals/utils/src/fs.ts
102
+ /**
103
+ * Resolves to `true` when the file or directory at `path` exists.
104
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * if (await exists('./kubb.config.ts')) {
109
+ * const content = await read('./kubb.config.ts')
110
+ * }
111
+ * ```
112
+ */
113
+ async function exists(path) {
114
+ if (typeof Bun !== "undefined") return Bun.file(path).exists();
115
+ return (0, node_fs_promises.access)(path).then(() => true, () => false);
116
+ }
117
+ //#endregion
100
118
  //#region ../../internals/utils/src/object.ts
101
119
  /**
102
120
  * Returns `true` when `value` is a plain (non-null, non-array) object.
@@ -487,6 +505,18 @@ const structuralKeys = new Set([
487
505
  * formatMap['float'] // 'number'
488
506
  * ```
489
507
  */
508
+ /**
509
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
510
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
511
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
512
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
513
+ */
514
+ const specialCasedFormats = new Set([
515
+ "int64",
516
+ "date-time",
517
+ "date",
518
+ "time"
519
+ ]);
490
520
  const formatMap = {
491
521
  uuid: "uuid",
492
522
  email: "email",
@@ -634,7 +664,13 @@ async function mergeDocuments(pathOrApi) {
634
664
  enablePaths: false,
635
665
  canBundle: false
636
666
  })));
637
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
667
+ if (documents.length === 0) throw new _kubb_core.DiagnosticError({
668
+ code: _kubb_core.diagnosticCode.inputRequired,
669
+ severity: "error",
670
+ message: "No OAS documents were provided for merging.",
671
+ help: "Pass at least one path or document to `input.path`.",
672
+ location: { kind: "config" }
673
+ });
638
674
  const seed = {
639
675
  openapi: MERGE_OPENAPI_VERSION,
640
676
  info: {
@@ -650,9 +686,9 @@ async function mergeDocuments(pathOrApi) {
650
686
  * Creates a `Document` from an `AdapterSource`.
651
687
  *
652
688
  * 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.
689
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
690
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
691
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
656
692
  *
657
693
  * @example
658
694
  * ```ts
@@ -660,14 +696,31 @@ async function mergeDocuments(pathOrApi) {
660
696
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
661
697
  * ```
662
698
  */
663
- function parseFromConfig(source) {
699
+ async function parseFromConfig(source) {
664
700
  if (source.type === "data") {
665
701
  if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
666
702
  return parseDocument(source.data, { canBundle: false });
667
703
  }
668
704
  if (source.type === "paths") return mergeDocuments(source.paths);
669
705
  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));
706
+ const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
707
+ await assertInputExists(resolved);
708
+ return parseDocument(resolved);
709
+ }
710
+ /**
711
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
712
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
713
+ * its parse error instead.
714
+ */
715
+ async function assertInputExists(input) {
716
+ if (new URLPath(input).isURL) return;
717
+ if (!await exists(input)) throw new _kubb_core.DiagnosticError({
718
+ code: _kubb_core.diagnosticCode.inputNotFound,
719
+ severity: "error",
720
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
721
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
722
+ location: { kind: "config" }
723
+ });
671
724
  }
672
725
  /**
673
726
  * Validates an OpenAPI document using `oas-normalize` with colorized error output.
@@ -714,7 +767,21 @@ function resolveRef(document, $ref) {
714
767
  }
715
768
  if (docCache.has($ref)) return docCache.get($ref);
716
769
  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}.`);
770
+ if (!current) {
771
+ const diagnostic = {
772
+ code: _kubb_core.diagnosticCode.refNotFound,
773
+ severity: "error",
774
+ message: `Could not find a definition for ${origRef}.`,
775
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
776
+ location: {
777
+ kind: "schema",
778
+ pointer: origRef,
779
+ ref: origRef
780
+ }
781
+ };
782
+ _kubb_core.Diagnostics.report(diagnostic);
783
+ return null;
784
+ }
718
785
  docCache.set($ref, current);
719
786
  return current;
720
787
  }
@@ -741,13 +808,13 @@ function dereferenceWithRef(document, schema) {
741
808
  //#endregion
742
809
  //#region src/dialect.ts
743
810
  /**
744
- * The OpenAPI / Swagger dialect the default used by `@kubb/adapter-oas`.
811
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
745
812
  *
746
813
  * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
747
814
  * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
748
815
  * 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
816
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
817
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
751
818
  * the rest unchanged.
752
819
  *
753
820
  * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
@@ -789,7 +856,16 @@ function resolveServerUrl(server, overrides) {
789
856
  for (const [key, variable] of Object.entries(server.variables)) {
790
857
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
791
858
  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(", ")}.`);
859
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.DiagnosticError({
860
+ code: _kubb_core.diagnosticCode.invalidServerVariable,
861
+ severity: "error",
862
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
863
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
864
+ location: {
865
+ kind: "document",
866
+ pointer: "#/servers"
867
+ }
868
+ });
793
869
  url = url.replaceAll(`{${key}}`, value);
794
870
  }
795
871
  return url;
@@ -802,6 +878,15 @@ function getSchemaType(format) {
802
878
  return formatMap[format] ?? null;
803
879
  }
804
880
  /**
881
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
882
+ * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
883
+ * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
884
+ * diagnostic in step with the parser as `formatMap` grows.
885
+ */
886
+ function isHandledFormat(format) {
887
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
888
+ }
889
+ /**
805
890
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
806
891
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
807
892
  */
@@ -898,7 +983,7 @@ function getRequestSchema(document, operation, options = {}) {
898
983
  /**
899
984
  * Flattens a keyword-only `allOf` into its parent schema.
900
985
  *
901
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
986
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
902
987
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
903
988
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
904
989
  *
@@ -908,7 +993,7 @@ function getRequestSchema(document, operation, options = {}) {
908
993
  * // { type: 'object', properties: {}, description: 'A pet' }
909
994
  *
910
995
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
911
- * // returned unchanged contains a $ref
996
+ * // returned unchanged, contains a $ref
912
997
  * ```
913
998
  */
914
999
  /**
@@ -934,7 +1019,7 @@ function flattenSchema(schema) {
934
1019
  /**
935
1020
  * Extracts the inline schema from a media-type `content` map.
936
1021
  *
937
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
1022
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
938
1023
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
939
1024
  *
940
1025
  * @example
@@ -1126,9 +1211,9 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1126
1211
  /**
1127
1212
  * Returns all request body content type keys for an operation.
1128
1213
  *
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.
1214
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1215
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1216
+ * available content types even for referenced bodies.
1132
1217
  *
1133
1218
  * @example
1134
1219
  * ```ts
@@ -1145,7 +1230,7 @@ function getRequestBodyContentTypes(document, operation) {
1145
1230
  /**
1146
1231
  * Returns all response content type keys for an operation at a given status code.
1147
1232
  *
1148
- * Response `$ref`s are resolved in-place first the same mutation `getResponseSchema` performs
1233
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1149
1234
  * so the returned list reflects the available content types even for referenced responses.
1150
1235
  *
1151
1236
  * @example
@@ -1205,16 +1290,12 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1205
1290
  */
1206
1291
  const resolvingRefs = /* @__PURE__ */ new Set();
1207
1292
  /**
1208
- * Cache of already-resolved `$ref` schemas within this parser instance.
1209
- *
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.
1293
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1215
1294
  *
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.
1295
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1296
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1297
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1298
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1218
1299
  */
1219
1300
  const resolvedRefCache = /* @__PURE__ */ new Map();
1220
1301
  /**
@@ -1752,7 +1833,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1752
1833
  /**
1753
1834
  * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
1754
1835
  * `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
1836
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1756
1837
  * match/convert/fall-through contract.
1757
1838
  */
1758
1839
  const schemaRules = [
@@ -2097,6 +2178,50 @@ function patchDiscriminatorNode(node, entry) {
2097
2178
  };
2098
2179
  }
2099
2180
  //#endregion
2181
+ //#region src/schemaDiagnostics.ts
2182
+ /**
2183
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2184
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2185
+ * pointer as it descends so a nested field reports against its full path
2186
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2187
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2188
+ * no-op outside one, and repeats are deduped by the build.
2189
+ */
2190
+ function reportSchemaDiagnostics({ node, name }) {
2191
+ visit(node, `#/components/schemas/${name}`);
2192
+ }
2193
+ function visit(node, pointer) {
2194
+ if (node.deprecated) _kubb_core.Diagnostics.report({
2195
+ code: _kubb_core.diagnosticCode.deprecated,
2196
+ severity: "info",
2197
+ message: "This schema is marked as deprecated.",
2198
+ location: {
2199
+ kind: "schema",
2200
+ pointer
2201
+ }
2202
+ });
2203
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) _kubb_core.Diagnostics.report({
2204
+ code: _kubb_core.diagnosticCode.unsupportedFormat,
2205
+ severity: "warning",
2206
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2207
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2208
+ location: {
2209
+ kind: "schema",
2210
+ pointer
2211
+ }
2212
+ });
2213
+ if (node.type === "object") {
2214
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${property.name}`);
2215
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2216
+ return;
2217
+ }
2218
+ if (node.type === "array" || node.type === "tuple") {
2219
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2220
+ return;
2221
+ }
2222
+ if (node.type === "union" || node.type === "intersection") for (const member of node.members ?? []) visit(member, pointer);
2223
+ }
2224
+ //#endregion
2100
2225
  //#region src/stream.ts
2101
2226
  /**
2102
2227
  * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
@@ -2156,7 +2281,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2156
2281
  * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2157
2282
  * Both are proportional to the number of aliases or discriminator parents, not total schema count.
2158
2283
  *
2159
- * Each schema is parsed again during the streaming pass this is intentional.
2284
+ * Each schema is parsed again during the streaming pass. This is intentional.
2160
2285
  * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
2161
2286
  *
2162
2287
  * @example
@@ -2180,6 +2305,10 @@ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions,
2180
2305
  name
2181
2306
  }, parserOptions);
2182
2307
  allNodes.push(node);
2308
+ reportSchemaDiagnostics({
2309
+ node,
2310
+ name
2311
+ });
2183
2312
  if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2184
2313
  if (_kubb_core.ast.narrowSchema(node, _kubb_core.ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2185
2314
  if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
@@ -2396,6 +2525,7 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2396
2525
  return parsedDocument;
2397
2526
  },
2398
2527
  async validate(input, options) {
2528
+ await assertInputExists(input);
2399
2529
  await validateDocument(await parseDocument(input), options);
2400
2530
  },
2401
2531
  getImports(node, resolve) {