@apidevtools/json-schema-ref-parser 15.3.2 → 15.3.3

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.
@@ -45,7 +45,7 @@ function crawl(parent, key, path, scopeBase, dynamicIdScope, pathFromRoot, indir
45
45
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
46
46
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
47
47
  const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
48
- if ($Ref.isAllowed$Ref(obj)) {
48
+ if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
49
49
  inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
50
50
  }
51
51
  else {
@@ -71,7 +71,7 @@ function crawl(parent, key, path, scopeBase, dynamicIdScope, pathFromRoot, indir
71
71
  const keyPath = Pointer.join(path, key);
72
72
  const keyPathFromRoot = Pointer.join(pathFromRoot, key);
73
73
  const value = obj[key];
74
- if ($Ref.isAllowed$Ref(value)) {
74
+ if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
75
75
  const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
76
76
  inventory$Ref(obj, key, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
77
77
  }
@@ -53,7 +53,7 @@ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, proc
53
53
  parents.add(obj);
54
54
  processedObjects.add(obj);
55
55
  const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
56
- if ($Ref.isAllowed$Ref(obj, options)) {
56
+ if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
57
57
  dereferenced = dereference$Ref(obj, path, currentScopeBase, dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
58
58
  result.circular = dereferenced.circular;
59
59
  result.value = dereferenced.value;
@@ -68,7 +68,7 @@ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, proc
68
68
  }
69
69
  const value = obj[key];
70
70
  let circular;
71
- if ($Ref.isAllowed$Ref(value, options)) {
71
+ if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
72
72
  const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
73
73
  dereferenced = dereference$Ref(value, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
74
74
  circular = dereferenced.circular;
@@ -153,18 +153,10 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
153
153
  // If the cached object however is _not_ circular and there are additional keys alongside our
154
154
  // `$ref` pointer here we should merge them back in and return that.
155
155
  if (!cache.circular) {
156
- const refKeys = Object.keys($ref);
157
- if (refKeys.length > 1) {
158
- const extraKeys = {};
159
- for (const key of refKeys) {
160
- if (key !== "$ref" && !(key in cache.value)) {
161
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
162
- extraKeys[key] = $ref[key];
163
- }
164
- }
156
+ if (Object.keys($ref).length > 1) {
165
157
  return {
166
158
  circular: cache.circular,
167
- value: Object.assign({}, cache.value, extraKeys),
159
+ value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
168
160
  };
169
161
  }
170
162
  return cache;
@@ -210,7 +202,7 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
210
202
  foundCircularReference(path, $refs, options);
211
203
  }
212
204
  // Dereference the JSON reference
213
- let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
205
+ let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
214
206
  // Crawl the dereferenced value (unless it's circular)
215
207
  if (!circular) {
216
208
  // Determine if the dereferenced value is circular
@@ -256,7 +256,7 @@ class Pointer {
256
256
  */
257
257
  function resolveIf$Ref(pointer, options, pathFromRoot) {
258
258
  // Is the value a JSON reference? (and allowed?)
259
- if ($Ref.isAllowed$Ref(pointer.value, options)) {
259
+ if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
260
260
  const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
261
261
  const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
262
262
  if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
@@ -272,7 +272,7 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
272
272
  if ($Ref.isExtended$Ref(pointer.value)) {
273
273
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
274
274
  // So the resolved path does NOT change. Just the value does.
275
- pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
275
+ pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
276
276
  if (pointer.$ref.dynamicIdScope) {
277
277
  pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
278
278
  }
package/dist/lib/ref.d.ts CHANGED
@@ -114,7 +114,7 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
114
114
  * @param options
115
115
  * @returns
116
116
  */
117
- static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>): true | undefined;
117
+ static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>, allowPlainNameFragments?: boolean): true | undefined;
118
118
  /**
119
119
  * Determines whether the given value is a JSON reference that "extends" its resolved value.
120
120
  * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
@@ -182,6 +182,6 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
182
182
  * @param options - The options
183
183
  * @returns - Returns the dereferenced value
184
184
  */
185
- static dereference<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($ref: $Ref<S, O>, resolvedValue: S, options?: O): S;
185
+ static dereference<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($ref: $Ref<S, O>, resolvedValue: S, options?: O, useSpecCompliantRefSiblings?: boolean): S;
186
186
  }
187
187
  export default $Ref;
package/dist/lib/ref.js CHANGED
@@ -170,12 +170,16 @@ class $Ref {
170
170
  * @param options
171
171
  * @returns
172
172
  */
173
- static isAllowed$Ref(value, options) {
173
+ static isAllowed$Ref(value, options, allowPlainNameFragments = false) {
174
174
  if (this.is$Ref(value)) {
175
175
  if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
176
176
  // It's a JSON Pointer reference, which is always allowed
177
177
  return true;
178
178
  }
179
+ else if (allowPlainNameFragments && value.$ref[0] === "#") {
180
+ // JSON Schema 2019-09+ allows plain-name fragments declared via $anchor/$dynamicAnchor
181
+ return true;
182
+ }
179
183
  else if (value.$ref[0] !== "#" && (!options || options.resolve?.external)) {
180
184
  // It's an external reference, which is allowed by the options
181
185
  return true;
@@ -252,7 +256,10 @@ class $Ref {
252
256
  * @param options - The options
253
257
  * @returns - Returns the dereferenced value
254
258
  */
255
- static dereference($ref, resolvedValue, options) {
259
+ static dereference($ref, resolvedValue, options, useSpecCompliantRefSiblings = false) {
260
+ if ($Ref.isExtended$Ref($ref) && useSpecCompliantRefSiblings) {
261
+ return preserveRefSiblings($ref, resolvedValue);
262
+ }
256
263
  if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
257
264
  const merged = {};
258
265
  for (const key of Object.keys($ref)) {
@@ -287,6 +294,24 @@ class $Ref {
287
294
  }
288
295
  }
289
296
  }
297
+ function preserveRefSiblings(refValue, resolvedValue) {
298
+ const preserved = {};
299
+ const existingAllOf = "allOf" in refValue ? refValue.allOf : undefined;
300
+ for (const key of Object.keys(refValue)) {
301
+ if (key !== "$ref" && key !== "allOf") {
302
+ preserved[key] = refValue[key];
303
+ }
304
+ }
305
+ const allOf = [resolvedValue];
306
+ if (Array.isArray(existingAllOf)) {
307
+ allOf.push(...existingAllOf);
308
+ }
309
+ else if (existingAllOf !== undefined) {
310
+ allOf.push(existingAllOf);
311
+ }
312
+ preserved.allOf = allOf;
313
+ return preserved;
314
+ }
290
315
  function deepMerge(target, source) {
291
316
  //return {...target, ...source};
292
317
  // If either isn't an object, just return source (overwrite)
@@ -80,6 +80,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
80
80
  */
81
81
  _add(path: string): $Ref<S, O>;
82
82
  _addAlias(path: string, value: S, pathType?: string | unknown, dynamicIdScope?: boolean): $Ref<S, O>;
83
+ _addExactAlias(path: string, targetPath: string): string;
83
84
  /**
84
85
  * Resolves the given JSON reference.
85
86
  *
@@ -98,6 +99,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
98
99
  */
99
100
  _$refs: $RefsMap<S, O>;
100
101
  _aliases: $RefsMap<S, O>;
102
+ _exactAliases: Record<string, string>;
101
103
  /**
102
104
  * The {@link $Ref} object that is the root of the JSON schema.
103
105
  *
package/dist/lib/refs.js CHANGED
@@ -99,7 +99,7 @@ export default class $Refs {
99
99
  */
100
100
  _get$Ref(path) {
101
101
  path = url.resolve(this._root$Ref.path, path);
102
- return this._getRef(path);
102
+ return this._getRef(this._exactAliases[path] || path);
103
103
  }
104
104
  /**
105
105
  * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object.
@@ -127,6 +127,13 @@ export default class $Refs {
127
127
  this._aliases[withoutHash] = $ref;
128
128
  return $ref;
129
129
  }
130
+ _addExactAlias(path, targetPath) {
131
+ if (!path || this._exactAliases[path]) {
132
+ return this._exactAliases[path];
133
+ }
134
+ this._exactAliases[path] = targetPath;
135
+ return targetPath;
136
+ }
130
137
  /**
131
138
  * Resolves the given JSON reference.
132
139
  *
@@ -138,11 +145,12 @@ export default class $Refs {
138
145
  */
139
146
  _resolve(path, pathFromRoot, options) {
140
147
  const absPath = url.resolve(this._root$Ref.path, path);
141
- const $ref = this._getRef(absPath);
148
+ const canonicalPath = this._exactAliases[absPath] || absPath;
149
+ const $ref = this._getRef(canonicalPath);
142
150
  if (!$ref) {
143
151
  throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
144
152
  }
145
- return $ref.resolve(absPath, options, path, pathFromRoot);
153
+ return $ref.resolve(canonicalPath, options, path, pathFromRoot);
146
154
  }
147
155
  /**
148
156
  * A map of paths/urls to {@link $Ref} objects
@@ -152,6 +160,7 @@ export default class $Refs {
152
160
  */
153
161
  _$refs = {};
154
162
  _aliases = {};
163
+ _exactAliases = {};
155
164
  /**
156
165
  * The {@link $Ref} object that is the root of the JSON schema.
157
166
  *
@@ -168,6 +177,7 @@ export default class $Refs {
168
177
  this.circular = false;
169
178
  this._$refs = {};
170
179
  this._aliases = {};
180
+ this._exactAliases = {};
171
181
  // @ts-ignore
172
182
  this._root$Ref = null;
173
183
  }
@@ -20,20 +20,22 @@ export function registerSchemaResources($refs, basePath, value, pathType, dynami
20
20
  return;
21
21
  }
22
22
  const seen = new Set();
23
- const visit = (node, scopeBase) => {
23
+ const visit = (node, scopeBase, pointerTokens) => {
24
24
  if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
25
25
  return;
26
26
  }
27
27
  seen.add(node);
28
28
  const nextScopeBase = getSchemaBasePath(scopeBase, node);
29
+ const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
29
30
  if (nextScopeBase !== scopeBase) {
30
31
  $refs._addAlias(nextScopeBase, node, pathType, dynamicIdScope);
31
32
  }
33
+ registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
32
34
  for (const key of Object.keys(node)) {
33
- visit(node[key], nextScopeBase);
35
+ visit(node[key], nextScopeBase, [...resourcePointerTokens, key]);
34
36
  }
35
37
  };
36
- visit(value, basePath);
38
+ visit(value, basePath, []);
37
39
  }
38
40
  function getSchemaId(value) {
39
41
  if (value &&
@@ -45,3 +47,26 @@ function getSchemaId(value) {
45
47
  }
46
48
  return undefined;
47
49
  }
50
+ function registerAnchorAliases($refs, scopeBase, pointerTokens, value) {
51
+ if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
52
+ return;
53
+ }
54
+ const resourceBase = url.stripHash(scopeBase);
55
+ const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
56
+ const anchors = [
57
+ value.$anchor,
58
+ value.$dynamicAnchor,
59
+ ];
60
+ for (const anchor of anchors) {
61
+ if (typeof anchor === "string" && anchor.length > 0) {
62
+ $refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
63
+ }
64
+ }
65
+ }
66
+ function joinPointerPath(basePath, tokens) {
67
+ let path = `${basePath}#`;
68
+ for (const token of tokens) {
69
+ path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
70
+ }
71
+ return path;
72
+ }
package/lib/bundle.ts CHANGED
@@ -97,7 +97,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
97
97
 
98
98
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
99
99
  const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
100
- if ($Ref.isAllowed$Ref(obj)) {
100
+ if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
101
101
  inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
102
102
  } else {
103
103
  // Crawl the object in a specific order that's optimized for bundling.
@@ -122,7 +122,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
122
122
  const keyPathFromRoot = Pointer.join(pathFromRoot, key);
123
123
  const value = obj[key];
124
124
 
125
- if ($Ref.isAllowed$Ref(value)) {
125
+ if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
126
126
  const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
127
127
  inventory$Ref(
128
128
  obj,
@@ -94,7 +94,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
94
94
  processedObjects.add(obj);
95
95
  const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
96
96
 
97
- if ($Ref.isAllowed$Ref(obj, options)) {
97
+ if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
98
98
  dereferenced = dereference$Ref(
99
99
  obj,
100
100
  path,
@@ -125,7 +125,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
125
125
  const value = obj[key];
126
126
  let circular;
127
127
 
128
- if ($Ref.isAllowed$Ref(value, options)) {
128
+ if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
129
129
  const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
130
130
  dereferenced = dereference$Ref(
131
131
  value,
@@ -257,18 +257,10 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
257
257
  // If the cached object however is _not_ circular and there are additional keys alongside our
258
258
  // `$ref` pointer here we should merge them back in and return that.
259
259
  if (!cache.circular) {
260
- const refKeys = Object.keys($ref);
261
- if (refKeys.length > 1) {
262
- const extraKeys = {};
263
- for (const key of refKeys) {
264
- if (key !== "$ref" && !(key in cache.value)) {
265
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
266
- extraKeys[key] = $ref[key];
267
- }
268
- }
260
+ if (Object.keys($ref).length > 1) {
269
261
  return {
270
262
  circular: cache.circular,
271
- value: Object.assign({}, cache.value, extraKeys),
263
+ value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
272
264
  };
273
265
  }
274
266
 
@@ -318,7 +310,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
318
310
  }
319
311
 
320
312
  // Dereference the JSON reference
321
- let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
313
+ let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
322
314
 
323
315
  // Crawl the dereferenced value (unless it's circular)
324
316
  if (!circular) {
package/lib/pointer.ts CHANGED
@@ -308,7 +308,7 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
308
308
  ) {
309
309
  // Is the value a JSON reference? (and allowed?)
310
310
 
311
- if ($Ref.isAllowed$Ref(pointer.value, options)) {
311
+ if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
312
312
  const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
313
313
  const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
314
314
 
@@ -326,7 +326,7 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
326
326
  if ($Ref.isExtended$Ref(pointer.value)) {
327
327
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
328
328
  // So the resolved path does NOT change. Just the value does.
329
- pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
329
+ pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
330
330
  if (pointer.$ref.dynamicIdScope) {
331
331
  pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
332
332
  }
package/lib/ref.ts CHANGED
@@ -199,11 +199,18 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
199
199
  * @param options
200
200
  * @returns
201
201
  */
202
- static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>) {
202
+ static isAllowed$Ref<S extends object = JSONSchema>(
203
+ value: unknown,
204
+ options?: ParserOptions<S>,
205
+ allowPlainNameFragments = false,
206
+ ) {
203
207
  if (this.is$Ref(value)) {
204
208
  if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
205
209
  // It's a JSON Pointer reference, which is always allowed
206
210
  return true;
211
+ } else if (allowPlainNameFragments && value.$ref[0] === "#") {
212
+ // JSON Schema 2019-09+ allows plain-name fragments declared via $anchor/$dynamicAnchor
213
+ return true;
207
214
  } else if (value.$ref[0] !== "#" && (!options || options.resolve?.external)) {
208
215
  // It's an external reference, which is allowed by the options
209
216
  return true;
@@ -286,7 +293,12 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
286
293
  $ref: $Ref<S, O>,
287
294
  resolvedValue: S,
288
295
  options?: O,
296
+ useSpecCompliantRefSiblings = false,
289
297
  ): S {
298
+ if ($Ref.isExtended$Ref($ref) && useSpecCompliantRefSiblings) {
299
+ return preserveRefSiblings($ref, resolvedValue) as S;
300
+ }
301
+
290
302
  if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
291
303
  const merged = {} as Partial<S>;
292
304
  for (const key of Object.keys($ref)) {
@@ -325,6 +337,27 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
325
337
  }
326
338
  }
327
339
 
340
+ function preserveRefSiblings<T>(refValue: Record<string, any>, resolvedValue: T): T {
341
+ const preserved = {} as Record<string, any>;
342
+ const existingAllOf = "allOf" in refValue ? refValue.allOf : undefined;
343
+
344
+ for (const key of Object.keys(refValue)) {
345
+ if (key !== "$ref" && key !== "allOf") {
346
+ preserved[key] = refValue[key];
347
+ }
348
+ }
349
+
350
+ const allOf = [resolvedValue];
351
+ if (Array.isArray(existingAllOf)) {
352
+ allOf.push(...existingAllOf);
353
+ } else if (existingAllOf !== undefined) {
354
+ allOf.push(existingAllOf);
355
+ }
356
+
357
+ preserved.allOf = allOf;
358
+ return preserved as T;
359
+ }
360
+
328
361
  function deepMerge<T>(target: Partial<T>, source: Partial<T>): T {
329
362
  //return {...target, ...source};
330
363
 
package/lib/refs.ts CHANGED
@@ -112,7 +112,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
112
112
  */
113
113
  _get$Ref(path: string) {
114
114
  path = url.resolve(this._root$Ref.path!, path);
115
- return this._getRef(path);
115
+ return this._getRef(this._exactAliases[path] || path);
116
116
  }
117
117
 
118
118
  /**
@@ -149,6 +149,15 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
149
149
  return $ref;
150
150
  }
151
151
 
152
+ _addExactAlias(path: string, targetPath: string) {
153
+ if (!path || this._exactAliases[path]) {
154
+ return this._exactAliases[path];
155
+ }
156
+
157
+ this._exactAliases[path] = targetPath;
158
+ return targetPath;
159
+ }
160
+
152
161
  /**
153
162
  * Resolves the given JSON reference.
154
163
  *
@@ -160,13 +169,14 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
160
169
  */
161
170
  _resolve(path: string, pathFromRoot: string, options?: O) {
162
171
  const absPath = url.resolve(this._root$Ref.path!, path);
163
- const $ref = this._getRef(absPath);
172
+ const canonicalPath = this._exactAliases[absPath] || absPath;
173
+ const $ref = this._getRef(canonicalPath);
164
174
 
165
175
  if (!$ref) {
166
176
  throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
167
177
  }
168
178
 
169
- return $ref.resolve(absPath, options, path, pathFromRoot);
179
+ return $ref.resolve(canonicalPath, options, path, pathFromRoot);
170
180
  }
171
181
 
172
182
  /**
@@ -179,6 +189,8 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
179
189
 
180
190
  _aliases: $RefsMap<S, O> = {};
181
191
 
192
+ _exactAliases: Record<string, string> = {};
193
+
182
194
  /**
183
195
  * The {@link $Ref} object that is the root of the JSON schema.
184
196
  *
@@ -197,6 +209,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
197
209
 
198
210
  this._$refs = {};
199
211
  this._aliases = {};
212
+ this._exactAliases = {};
200
213
 
201
214
  // @ts-ignore
202
215
  this._root$Ref = null;
@@ -38,7 +38,7 @@ export function registerSchemaResources<S extends object = JSONSchema, O extends
38
38
 
39
39
  const seen = new Set<object>();
40
40
 
41
- const visit = (node: unknown, scopeBase: string) => {
41
+ const visit = (node: unknown, scopeBase: string, pointerTokens: string[]) => {
42
42
  if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
43
43
  return;
44
44
  }
@@ -46,16 +46,18 @@ export function registerSchemaResources<S extends object = JSONSchema, O extends
46
46
  seen.add(node);
47
47
 
48
48
  const nextScopeBase = getSchemaBasePath(scopeBase, node);
49
+ const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
49
50
  if (nextScopeBase !== scopeBase) {
50
51
  $refs._addAlias(nextScopeBase, node as S, pathType, dynamicIdScope);
51
52
  }
53
+ registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
52
54
 
53
55
  for (const key of Object.keys(node)) {
54
- visit((node as Record<string, unknown>)[key], nextScopeBase);
56
+ visit((node as Record<string, unknown>)[key], nextScopeBase, [...resourcePointerTokens, key]);
55
57
  }
56
58
  };
57
59
 
58
- visit(value, basePath);
60
+ visit(value, basePath, []);
59
61
  }
60
62
 
61
63
  function getSchemaId(value: unknown): string | undefined {
@@ -71,3 +73,37 @@ function getSchemaId(value: unknown): string | undefined {
71
73
 
72
74
  return undefined;
73
75
  }
76
+
77
+ function registerAnchorAliases<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
78
+ $refs: $Refs<S, O>,
79
+ scopeBase: string,
80
+ pointerTokens: string[],
81
+ value: unknown,
82
+ ) {
83
+ if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
84
+ return;
85
+ }
86
+
87
+ const resourceBase = url.stripHash(scopeBase);
88
+ const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
89
+ const anchors = [
90
+ (value as { $anchor?: unknown }).$anchor,
91
+ (value as { $dynamicAnchor?: unknown }).$dynamicAnchor,
92
+ ];
93
+
94
+ for (const anchor of anchors) {
95
+ if (typeof anchor === "string" && anchor.length > 0) {
96
+ $refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
97
+ }
98
+ }
99
+ }
100
+
101
+ function joinPointerPath(basePath: string, tokens: string[]) {
102
+ let path = `${basePath}#`;
103
+
104
+ for (const token of tokens) {
105
+ path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
106
+ }
107
+
108
+ return path;
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apidevtools/json-schema-ref-parser",
3
- "version": "15.3.2",
3
+ "version": "15.3.3",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "type": "module",
6
6
  "types": "dist/lib/index.d.ts",