@hey-api/json-schema-ref-parser 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bundle.ts CHANGED
@@ -92,7 +92,7 @@ const getContainerTypeFromPath = (
92
92
  };
93
93
 
94
94
  /**
95
- * Inventories the given JSON Reference (i.e. records detailed information about it so we can
95
+ * Inventories the given JSON Reference (i.e., records detailed information about it so we can
96
96
  * optimize all $refs in the schema), and then crawls the resolved value.
97
97
  */
98
98
  const inventory$Ref = <S extends object = JSONSchema>({
@@ -157,11 +157,31 @@ const inventory$Ref = <S extends object = JSONSchema>({
157
157
  pointer = $refs._resolve($refPath, pathFromRoot, options);
158
158
  } catch (error) {
159
159
  if (error instanceof MissingPointerError) {
160
- // Log warning but continue - common in complex schema ecosystems
161
- console.warn(`Skipping unresolvable $ref: ${$refPath}`);
162
- return;
160
+ // The ref couldn't be resolved in the target file. This commonly
161
+ // happens when a wrapper file redirects via $ref to a versioned
162
+ // file, and the bundler's crawl path retains the wrapper URL.
163
+ // Try resolving the hash fragment against other files in $refs
164
+ // that might contain the target schema.
165
+ const hash = url.getHash($refPath);
166
+ if (hash) {
167
+ const baseFile = url.stripHash($refPath);
168
+ for (const filePath of Object.keys($refs._$refs)) {
169
+ if (filePath === baseFile) continue;
170
+ try {
171
+ pointer = $refs._resolve(filePath + hash, pathFromRoot, options);
172
+ if (pointer) break;
173
+ } catch {
174
+ // try next file
175
+ }
176
+ }
177
+ }
178
+ if (!pointer) {
179
+ console.warn(`Skipping unresolvable $ref: ${$refPath}`);
180
+ return;
181
+ }
182
+ } else {
183
+ throw error;
163
184
  }
164
- throw error; // Re-throw unexpected errors
165
185
  }
166
186
 
167
187
  if (pointer) {
@@ -193,10 +213,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
193
213
  }
194
214
 
195
215
  const newEntry: InventoryEntry = {
196
- $ref, // The JSON Reference (e.g. {$ref: string})
197
- circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
216
+ $ref, // The JSON Reference (e.g., {$ref: string})
217
+ circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e., it references itself)
198
218
  depth, // How far from the JSON Schema root is this $ref pointer?
199
- extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
219
+ extended, // Does this $ref extend its resolved value? (i.e., it has extra properties, in addition to "$ref")
200
220
  external, // Does this $ref pointer point to a file other than the main JSON Schema file?
201
221
  file, // The file that the $ref pointer resolves to
202
222
  hash, // The hash within `file` that the $ref pointer resolves to
@@ -217,8 +237,19 @@ const inventory$Ref = <S extends object = JSONSchema>({
217
237
  inventory.push(newEntry);
218
238
  inventoryLookup.add(newEntry);
219
239
 
220
- // Recursively crawl the resolved value
240
+ // Recursively crawl the resolved value.
241
+ // When the resolution followed a $ref chain to a different file,
242
+ // use the resolved file as the base path so that local $ref values
243
+ // (e.g., #/components/schemas/SiblingSchema) inside the resolved
244
+ // value resolve against the correct file.
221
245
  if (!existingEntry || external) {
246
+ let crawlPath = pointer.path;
247
+
248
+ const originalFile = url.stripHash($refPath);
249
+ if (file !== originalFile) {
250
+ crawlPath = file + url.getHash(pointer.path);
251
+ }
252
+
222
253
  crawl({
223
254
  $refs,
224
255
  indirections: indirections + 1,
@@ -227,7 +258,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
227
258
  key: null,
228
259
  options,
229
260
  parent: pointer.value,
230
- path: pointer.path,
261
+ path: crawlPath,
231
262
  pathFromRoot,
232
263
  resolvedRefs,
233
264
  visitedObjects,
@@ -414,8 +445,8 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
414
445
  const ensureContainer = (
415
446
  type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',
416
447
  ) => {
417
- const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string');
418
- const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string');
448
+ const isOas3 = Boolean(root && typeof root === 'object' && typeof root.openapi === 'string');
449
+ const isOas2 = Boolean(root && typeof root === 'object' && typeof root.swagger === 'string');
419
450
 
420
451
  if (isOas3) {
421
452
  if (!root.components || typeof root.components !== 'object') {
@@ -539,7 +570,7 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
539
570
  }
540
571
 
541
572
  // Keep internal refs internal. However, if the $ref extends the resolved value
542
- // (i.e. it has additional properties in addition to "$ref"), then we must
573
+ // (i.e., it has additional properties in addition to "$ref"), then we must
543
574
  // preserve the original $ref rather than rewriting it to the resolved hash.
544
575
  if (!entry.external) {
545
576
  if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
@@ -586,7 +617,22 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
586
617
  } catch {
587
618
  // Ignore errors
588
619
  }
589
- const proposed = `${proposedBase}_${lastToken(entry.hash)}`;
620
+
621
+ // Try without prefix first (cleaner names)
622
+ const schemaName = lastToken(entry.hash);
623
+ let proposed = schemaName;
624
+
625
+ // Check if this name would conflict with existing schemas from other files
626
+ if (!usedNamesByObj.has(container)) {
627
+ usedNamesByObj.set(container, new Set<string>(Object.keys(container || {})));
628
+ }
629
+ const used = usedNamesByObj.get(container)!;
630
+
631
+ // If the name is already used, add the file prefix
632
+ if (used.has(proposed)) {
633
+ proposed = `${proposedBase}_${schemaName}`;
634
+ }
635
+
590
636
  defName = uniqueName(container, proposed);
591
637
  namesForPrefix.set(targetKey, defName);
592
638
  // Store the resolved value under the container
@@ -226,7 +226,7 @@ function dereference$Ref<S extends object = JSONSchema>(
226
226
  }
227
227
 
228
228
  if (directCircular) {
229
- // The pointer is a DIRECT circular reference (i.e. it references itself).
229
+ // The pointer is a DIRECT circular reference (i.e., it references itself).
230
230
  // So replace the $ref path with the absolute path from the JSON Schema root
231
231
  dereferencedValue.$ref = pathFromRoot;
232
232
  }
package/src/index.ts CHANGED
@@ -117,12 +117,12 @@ export class $RefParser {
117
117
 
118
118
  await resolveExternal(this, this.options);
119
119
  const errors = JSONParserErrorGroup.getParserErrors(this);
120
- if (errors.length > 0) {
120
+ if (errors.length) {
121
121
  throw new JSONParserErrorGroup(this);
122
122
  }
123
123
  _bundle(this, this.options);
124
124
  const errors2 = JSONParserErrorGroup.getParserErrors(this);
125
- if (errors2.length > 0) {
125
+ if (errors2.length) {
126
126
  throw new JSONParserErrorGroup(this);
127
127
  }
128
128
  return this.schema!;
@@ -148,14 +148,14 @@ export class $RefParser {
148
148
 
149
149
  await resolveExternal(this, this.options);
150
150
  const errors = JSONParserErrorGroup.getParserErrors(this);
151
- if (errors.length > 0) {
151
+ if (errors.length) {
152
152
  throw new JSONParserErrorGroup(this);
153
153
  }
154
154
  _bundle(this, this.options);
155
155
  // Merged root is ready for bundling
156
156
 
157
157
  const errors2 = JSONParserErrorGroup.getParserErrors(this);
158
- if (errors2.length > 0) {
158
+ if (errors2.length) {
159
159
  throw new JSONParserErrorGroup(this);
160
160
  }
161
161
  return this.schema!;
@@ -297,7 +297,7 @@ export class $RefParser {
297
297
 
298
298
  public mergeMany(): JSONSchema {
299
299
  const schemas = this.schemaMany || [];
300
- if (schemas.length === 0) {
300
+ if (!schemas.length) {
301
301
  throw ono('mergeMany called with no schemas. Did you run parseMany?');
302
302
  }
303
303
 
@@ -335,7 +335,7 @@ export class $RefParser {
335
335
  }
336
336
  }
337
337
  }
338
- if (Object.keys(infoAccumulator).length > 0) {
338
+ if (Object.keys(infoAccumulator).length) {
339
339
  merged.info = infoAccumulator;
340
340
  }
341
341
 
@@ -356,7 +356,7 @@ export class $RefParser {
356
356
  }
357
357
  }
358
358
  }
359
- if (servers.length > 0) {
359
+ if (servers.length) {
360
360
  merged.servers = servers;
361
361
  }
362
362
 
@@ -527,24 +527,48 @@ export class $RefParser {
527
527
  }
528
528
  }
529
529
 
530
+ const HTTP_METHODS = new Set([
531
+ 'delete',
532
+ 'get',
533
+ 'head',
534
+ 'options',
535
+ 'patch',
536
+ 'post',
537
+ 'put',
538
+ 'trace',
539
+ ]);
540
+
530
541
  const srcPaths = (schema.paths || {}) as Record<string, any>;
531
542
  for (const [p, item] of Object.entries(srcPaths)) {
532
- let targetPath = p;
533
543
  if (merged.paths[p]) {
534
- const trimmed = p.startsWith('/') ? p.substring(1) : p;
535
- targetPath = `/${prefix}/${trimmed}`;
544
+ const newMethods = Object.keys(item as object).filter((k) => HTTP_METHODS.has(k));
545
+ const hasMethodConflict = newMethods.some((m) => merged.paths[p][m] !== undefined);
546
+ const rewritten = cloneAndRewrite(
547
+ item,
548
+ refMap,
549
+ tagMap,
550
+ prefix,
551
+ url.stripHash(sourcePath),
552
+ );
553
+ if (hasMethodConflict) {
554
+ const trimmed = p.startsWith('/') ? p.substring(1) : p;
555
+ merged.paths[`/${prefix}/${trimmed}`] = rewritten;
556
+ } else {
557
+ Object.assign(merged.paths[p], rewritten);
558
+ }
559
+ } else {
560
+ merged.paths[p] = cloneAndRewrite(
561
+ item,
562
+ refMap,
563
+ tagMap,
564
+ prefix,
565
+ url.stripHash(sourcePath),
566
+ );
536
567
  }
537
- merged.paths[targetPath] = cloneAndRewrite(
538
- item,
539
- refMap,
540
- tagMap,
541
- prefix,
542
- url.stripHash(sourcePath),
543
- );
544
568
  }
545
569
  }
546
570
 
547
- if (tags.length > 0) {
571
+ if (tags.length) {
548
572
  merged.tags = tags;
549
573
  }
550
574
 
@@ -561,3 +585,17 @@ export class $RefParser {
561
585
 
562
586
  export { sendRequest } from './resolvers/url';
563
587
  export type { JSONSchema } from './types';
588
+ export type { JSONParserErrorType } from './util/errors';
589
+ export {
590
+ InvalidPointerError,
591
+ isHandledError,
592
+ JSONParserError,
593
+ JSONParserErrorGroup,
594
+ MissingPointerError,
595
+ normalizeError,
596
+ ParserError,
597
+ ResolverError,
598
+ TimeoutError,
599
+ UnmatchedParserError,
600
+ UnmatchedResolverError,
601
+ } from './util/errors';
@@ -1,5 +1,4 @@
1
- import yaml from 'js-yaml';
2
- import { JSON_SCHEMA } from 'js-yaml';
1
+ import { parse } from 'yaml';
3
2
 
4
3
  import type { FileInfo, JSONSchema, Plugin } from '../types';
5
4
  import { ParserError } from '../util/errors';
@@ -16,8 +15,7 @@ export const yamlParser: Plugin = {
16
15
  }
17
16
 
18
17
  try {
19
- const yamlSchema = yaml.load(data, { schema: JSON_SCHEMA }) as JSONSchema;
20
- return yamlSchema;
18
+ return parse(data) as JSONSchema;
21
19
  } catch (error: any) {
22
20
  throw new ParserError(error?.message || 'Parser Error', file.url);
23
21
  }
package/src/pointer.ts CHANGED
@@ -140,7 +140,7 @@ class Pointer<S extends object = JSONSchema> {
140
140
  }
141
141
  }
142
142
 
143
- if (errors.length > 0) {
143
+ if (errors.length) {
144
144
  throw errors.length === 1
145
145
  ? errors[0]
146
146
  : new AggregateError(errors, 'Multiple missing pointer errors');
@@ -171,7 +171,7 @@ class Pointer<S extends object = JSONSchema> {
171
171
  const tokens = Pointer.parse(this.path);
172
172
  let token;
173
173
 
174
- if (tokens.length === 0) {
174
+ if (!tokens.length) {
175
175
  // There are no tokens, replace the entire object with the new value
176
176
  this.value = value;
177
177
  return value;
@@ -205,7 +205,7 @@ class Pointer<S extends object = JSONSchema> {
205
205
  /**
206
206
  * Parses a JSON pointer (or a path containing a JSON pointer in the hash)
207
207
  * and returns an array of the pointer's tokens.
208
- * (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
208
+ * (e.g., "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
209
209
  *
210
210
  * The pointer is parsed according to RFC 6901
211
211
  * {@link https://tools.ietf.org/html/rfc6901#section-3}
@@ -244,8 +244,8 @@ class Pointer<S extends object = JSONSchema> {
244
244
  /**
245
245
  * Creates a JSON pointer path, by joining one or more tokens to a base path.
246
246
  *
247
- * @param base - The base path (e.g. "schema.json#/definitions/person")
248
- * @param tokens - The token(s) to append (e.g. ["name", "first"])
247
+ * @param base - The base path (e.g., "schema.json#/definitions/person")
248
+ * @param tokens - The token(s) to append (e.g., ["name", "first"])
249
249
  * @returns
250
250
  */
251
251
  static join(base: string, tokens: string | string[]) {
package/src/ref.ts CHANGED
@@ -46,7 +46,7 @@ class $Ref<S extends object = JSONSchema> {
46
46
  $refs: $Refs<S>;
47
47
 
48
48
  /**
49
- * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
49
+ * Indicates the type of {@link $Ref#path} (e.g., "file", "http", etc.)
50
50
  */
51
51
  pathType: string | unknown;
52
52
 
@@ -152,7 +152,7 @@ class $Ref<S extends object = JSONSchema> {
152
152
  value !== null &&
153
153
  '$ref' in value &&
154
154
  typeof value.$ref === 'string' &&
155
- value.$ref.length > 0
155
+ Boolean(value.$ref.length)
156
156
  );
157
157
  }
158
158
 
package/src/refs.ts CHANGED
@@ -224,7 +224,7 @@ function getPaths<S extends object = JSONSchema>($refs: $RefsMap<S>, types: stri
224
224
 
225
225
  // Filter the paths by type
226
226
  types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types);
227
- if (types.length > 0 && types[0]) {
227
+ if (types.length && types[0]) {
228
228
  paths = paths.filter((key) => types.includes($refs[key]!.pathType as string));
229
229
  }
230
230