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

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.
@@ -1,6 +1,7 @@
1
1
  import $Ref from "./ref.js";
2
2
  import Pointer from "./pointer.js";
3
3
  import * as url from "./util/url.js";
4
+ import { getSchemaBasePath } from "./util/schema-resources.js";
4
5
  /**
5
6
  * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that
6
7
  * only has *internal* references, not any *external* references.
@@ -13,7 +14,7 @@ function bundle(parser, options) {
13
14
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
14
15
  // Build an inventory of all $ref pointers in the JSON Schema
15
16
  const inventory = [];
16
- crawl(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
17
+ crawl(parser, "schema", parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, "#", 0, inventory, parser.$refs, options);
17
18
  // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
18
19
  const rootId = parser.schema && typeof parser.schema === "object" && "$id" in parser.schema
19
20
  ? parser.schema.$id
@@ -38,13 +39,14 @@ function bundle(parser, options) {
38
39
  * @param $refs
39
40
  * @param options
40
41
  */
41
- function crawl(parent, key, path, pathFromRoot, indirections, inventory, $refs, options) {
42
+ function crawl(parent, key, path, scopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options) {
42
43
  const obj = key === null ? parent : parent[key];
43
44
  const bundleOptions = (options.bundle || {});
44
45
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
45
46
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
47
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
46
48
  if ($Ref.isAllowed$Ref(obj)) {
47
- inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
49
+ inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
48
50
  }
49
51
  else {
50
52
  // Crawl the object in a specific order that's optimized for bundling.
@@ -70,10 +72,11 @@ function crawl(parent, key, path, pathFromRoot, indirections, inventory, $refs,
70
72
  const keyPathFromRoot = Pointer.join(pathFromRoot, key);
71
73
  const value = obj[key];
72
74
  if ($Ref.isAllowed$Ref(value)) {
73
- inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
75
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
76
+ inventory$Ref(obj, key, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
74
77
  }
75
78
  else {
76
- crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
79
+ crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
77
80
  }
78
81
  // We need to ensure that we have an object to work with here because we may be crawling
79
82
  // an `examples` schema and `value` may be nullish.
@@ -99,9 +102,9 @@ function crawl(parent, key, path, pathFromRoot, indirections, inventory, $refs,
99
102
  * @param $refs
100
103
  * @param options
101
104
  */
102
- function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) {
105
+ function inventory$Ref($refParent, $refKey, path, scopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options) {
103
106
  const $ref = $refKey === null ? $refParent : $refParent[$refKey];
104
- const $refPath = url.resolve(path, $ref.$ref);
107
+ const $refPath = url.resolve(dynamicIdScope ? scopeBase : path, $ref.$ref);
105
108
  const pointer = $refs._resolve($refPath, pathFromRoot, options);
106
109
  if (pointer === null) {
107
110
  return;
@@ -110,7 +113,7 @@ function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, in
110
113
  const depth = parsed.length;
111
114
  const file = url.stripHash(pointer.path);
112
115
  const hash = url.getHash(pointer.path);
113
- const external = file !== $refs._root$Ref.path;
116
+ const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
114
117
  const extended = $Ref.isExtended$Ref($ref);
115
118
  indirections += pointer.indirections;
116
119
  const existingEntry = findInInventory(inventory, $refParent, $refKey);
@@ -139,7 +142,7 @@ function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, in
139
142
  });
140
143
  // Recursively crawl the resolved value
141
144
  if (!existingEntry || external) {
142
- crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
145
+ crawl(pointer.value, null, pointer.path, pointer.$ref.path, pointer.$ref.dynamicIdScope, pathFromRoot, indirections + 1, inventory, $refs, options);
143
146
  }
144
147
  }
145
148
  /**
@@ -1,6 +1,7 @@
1
1
  import $Ref from "./ref.js";
2
2
  import Pointer from "./pointer.js";
3
3
  import * as url from "./util/url.js";
4
+ import { getSchemaBasePath } from "./util/schema-resources.js";
4
5
  import { TimeoutError } from "./util/errors.js";
5
6
  export default dereference;
6
7
  /**
@@ -13,7 +14,7 @@ export default dereference;
13
14
  function dereference(parser, options) {
14
15
  const start = Date.now();
15
16
  // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
16
- const dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, "#", new Set(), new Set(), new Map(), parser.$refs, options, start, 0);
17
+ const dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, "#", new Set(), new Set(), new Map(), parser.$refs, options, start, 0);
17
18
  parser.$refs.circular = dereferenced.circular;
18
19
  parser.schema = dereferenced.value;
19
20
  }
@@ -32,7 +33,7 @@ function dereference(parser, options) {
32
33
  * @param depth - The current recursion depth
33
34
  * @returns
34
35
  */
35
- function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
36
+ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
36
37
  let dereferenced;
37
38
  const result = {
38
39
  value: obj,
@@ -51,8 +52,9 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
51
52
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
52
53
  parents.add(obj);
53
54
  processedObjects.add(obj);
55
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
54
56
  if ($Ref.isAllowed$Ref(obj, options)) {
55
- dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
57
+ dereferenced = dereference$Ref(obj, path, currentScopeBase, dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
56
58
  result.circular = dereferenced.circular;
57
59
  result.value = dereferenced.value;
58
60
  }
@@ -67,7 +69,8 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
67
69
  const value = obj[key];
68
70
  let circular;
69
71
  if ($Ref.isAllowed$Ref(value, options)) {
70
- dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
72
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
73
+ dereferenced = dereference$Ref(value, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
71
74
  circular = dereferenced.circular;
72
75
  // Avoid pointless mutations; breaks frozen objects to no profit
73
76
  if (obj[key] !== dereferenced.value) {
@@ -104,7 +107,7 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
104
107
  }
105
108
  else {
106
109
  if (!parents.has(value)) {
107
- dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
110
+ dereferenced = crawl(value, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
108
111
  circular = dereferenced.circular;
109
112
  // Avoid pointless mutations; breaks frozen objects to no profit
110
113
  if (obj[key] !== dereferenced.value) {
@@ -137,10 +140,11 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
137
140
  * @param options
138
141
  * @returns
139
142
  */
140
- function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
143
+ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
141
144
  const isExternalRef = $Ref.isExternal$Ref($ref);
142
145
  const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
143
- const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
146
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
147
+ const $refPath = url.resolve(resolutionBase, $ref.$ref);
144
148
  const cache = dereferencedCache.get($refPath);
145
149
  if (cache) {
146
150
  // If the object we found is circular we can immediately return it because it would have been
@@ -210,7 +214,7 @@ function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, de
210
214
  // Crawl the dereferenced value (unless it's circular)
211
215
  if (!circular) {
212
216
  // Determine if the dereferenced value is circular
213
- const dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
217
+ const dereferenced = crawl(dereferencedValue, pointer.path, pointer.$ref.path, pointer.$ref.dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
214
218
  circular = dereferenced.circular;
215
219
  dereferencedValue = dereferenced.value;
216
220
  }
package/dist/lib/index.js CHANGED
@@ -9,6 +9,7 @@ import FileResolver from "./resolvers/file.js";
9
9
  import HTTPResolver from "./resolvers/http.js";
10
10
  import { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError, JSONParserErrorGroup, } from "./util/errors.js";
11
11
  import maybe from "./util/maybe.js";
12
+ import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
12
13
  import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
13
14
  import { isUnsafeUrl } from "./util/url.js";
14
15
  /**
@@ -68,6 +69,8 @@ export class $RefParser {
68
69
  const $ref = this.$refs._add(args.path);
69
70
  $ref.value = args.schema;
70
71
  $ref.pathType = pathType;
72
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
73
+ registerSchemaResources(this.$refs, $ref.path, $ref.value, $ref.pathType, $ref.dynamicIdScope);
71
74
  promise = Promise.resolve(args.schema);
72
75
  }
73
76
  else {
@@ -1,8 +1,13 @@
1
1
  import type $Refs from "./refs.js";
2
2
  import type { ParserOptions } from "./options.js";
3
3
  import type { JSONSchema } from "./types/index.js";
4
+ interface ParseTarget {
5
+ url: string;
6
+ reference?: string;
7
+ baseUrl?: string;
8
+ }
4
9
  /**
5
10
  * Reads and parses the specified file path or URL.
6
11
  */
7
- declare function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(path: string, $refs: $Refs<S, O>, options: O): Promise<string | Buffer<ArrayBufferLike> | S | undefined>;
12
+ declare function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(target: string | ParseTarget, $refs: $Refs<S, O>, options: O): Promise<string | Buffer<ArrayBufferLike> | S | undefined>;
8
13
  export default parse;
package/dist/lib/parse.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import * as url from "./util/url.js";
2
+ import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
2
3
  import { filter, all, sort, run } from "./util/plugins.js";
3
4
  import { ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError, } from "./util/errors.js";
4
5
  /**
5
6
  * Reads and parses the specified file path or URL.
6
7
  */
7
- async function parse(path, $refs, options) {
8
+ async function parse(target, $refs, options) {
9
+ let path = typeof target === "string" ? target : target.url;
10
+ const baseUrl = typeof target === "string" ? undefined : target.baseUrl;
11
+ let reference = typeof target === "string" ? undefined : target.reference;
8
12
  // Remove the URL fragment, if any
9
13
  const hashIndex = path.indexOf("#");
10
14
  let hash = "";
@@ -13,6 +17,12 @@ async function parse(path, $refs, options) {
13
17
  // Remove the URL fragment, if any
14
18
  path = path.substring(0, hashIndex);
15
19
  }
20
+ if (reference) {
21
+ const referenceHashIndex = reference.indexOf("#");
22
+ if (referenceHashIndex >= 0) {
23
+ reference = reference.substring(0, referenceHashIndex);
24
+ }
25
+ }
16
26
  // Add a new $Ref for this file, even though we don't have the value yet.
17
27
  // This ensures that we don't simultaneously read & parse the same file multiple times
18
28
  const $ref = $refs._add(path);
@@ -21,6 +31,8 @@ async function parse(path, $refs, options) {
21
31
  url: path,
22
32
  hash,
23
33
  extension: url.getExtension(path),
34
+ ...(reference !== undefined ? { reference } : {}),
35
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
24
36
  };
25
37
  // Read the file and then parse the data
26
38
  try {
@@ -29,6 +41,8 @@ async function parse(path, $refs, options) {
29
41
  file.data = resolver.result;
30
42
  const parser = await parseFile(file, options, $refs);
31
43
  $ref.value = parser.result;
44
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
45
+ registerSchemaResources($refs, $ref.path, $ref.value, $ref.pathType, $ref.dynamicIdScope);
32
46
  return parser.result;
33
47
  }
34
48
  catch (err) {
@@ -25,6 +25,10 @@ declare class Pointer<S extends object = JSONSchema, O extends ParserOptions<S>
25
25
  * The original path or URL, used for error messages.
26
26
  */
27
27
  originalPath: string;
28
+ /**
29
+ * The current base URI used to resolve nested $ref pointers while walking this pointer.
30
+ */
31
+ scopeBase: string;
28
32
  /**
29
33
  * The value of the JSON pointer.
30
34
  * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
@@ -1,6 +1,7 @@
1
1
  import $Ref from "./ref.js";
2
2
  import * as url from "./util/url.js";
3
3
  import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js";
4
+ import { getSchemaBasePath } from "./util/schema-resources.js";
4
5
  export const nullSymbol = Symbol("null");
5
6
  const slashes = /\//g;
6
7
  const tildes = /~/g;
@@ -28,6 +29,10 @@ class Pointer {
28
29
  * The original path or URL, used for error messages.
29
30
  */
30
31
  originalPath;
32
+ /**
33
+ * The current base URI used to resolve nested $ref pointers while walking this pointer.
34
+ */
35
+ scopeBase;
31
36
  /**
32
37
  * The value of the JSON pointer.
33
38
  * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
@@ -46,6 +51,7 @@ class Pointer {
46
51
  this.$ref = $ref;
47
52
  this.path = path;
48
53
  this.originalPath = friendlyPath || path;
54
+ this.scopeBase = $ref.path || url.stripHash(path);
49
55
  this.value = undefined;
50
56
  this.circular = false;
51
57
  this.indirections = 0;
@@ -68,6 +74,9 @@ class Pointer {
68
74
  const found = [];
69
75
  // Crawl the object, one token at a time
70
76
  this.value = unwrapOrThrow(obj);
77
+ if (this.$ref.dynamicIdScope) {
78
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
79
+ }
71
80
  for (let i = 0; i < tokens.length; i++) {
72
81
  // During token walking, if the current value is an extended $ref (has sibling keys
73
82
  // alongside $ref, as allowed by JSON Schema 2019-09+), and resolveIf$Ref marks it
@@ -123,9 +132,13 @@ class Pointer {
123
132
  this.value = this.value[token];
124
133
  }
125
134
  found.push(token);
135
+ if (this.$ref.dynamicIdScope) {
136
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
137
+ }
126
138
  }
127
139
  // Resolve the final value
128
- if (!this.value || (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)) {
140
+ const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
141
+ if (!this.value || (this.value.$ref && url.resolve(finalResolutionBase, this.value.$ref) !== pathFromRoot)) {
129
142
  resolveIf$Ref(this, options, pathFromRoot);
130
143
  }
131
144
  return this;
@@ -150,6 +163,9 @@ class Pointer {
150
163
  }
151
164
  // Crawl the object, one token at a time
152
165
  this.value = unwrapOrThrow(obj);
166
+ if (this.$ref.dynamicIdScope) {
167
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
168
+ }
153
169
  for (let i = 0; i < tokens.length - 1; i++) {
154
170
  resolveIf$Ref(this, options);
155
171
  token = tokens[i];
@@ -161,6 +177,9 @@ class Pointer {
161
177
  // The token doesn't exist, so create it
162
178
  this.value = setValue(this, token, {});
163
179
  }
180
+ if (this.$ref.dynamicIdScope) {
181
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
182
+ }
164
183
  }
165
184
  // Set the value of the final token
166
185
  resolveIf$Ref(this, options);
@@ -238,7 +257,8 @@ class Pointer {
238
257
  function resolveIf$Ref(pointer, options, pathFromRoot) {
239
258
  // Is the value a JSON reference? (and allowed?)
240
259
  if ($Ref.isAllowed$Ref(pointer.value, options)) {
241
- const $refPath = url.resolve(pointer.path, pointer.value.$ref);
260
+ const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
261
+ const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
242
262
  if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
243
263
  // The value is a reference to itself, so there's nothing to do.
244
264
  pointer.circular = true;
@@ -253,6 +273,9 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
253
273
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
254
274
  // So the resolved path does NOT change. Just the value does.
255
275
  pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
276
+ if (pointer.$ref.dynamicIdScope) {
277
+ pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
278
+ }
256
279
  return false;
257
280
  }
258
281
  else {
@@ -260,6 +283,9 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
260
283
  pointer.$ref = resolved.$ref;
261
284
  pointer.path = resolved.path;
262
285
  pointer.value = resolved.value;
286
+ pointer.scopeBase = pointer.$ref.dynamicIdScope
287
+ ? getSchemaBasePath(pointer.$ref.path, pointer.value)
288
+ : pointer.$ref.path;
263
289
  }
264
290
  return true;
265
291
  }
package/dist/lib/ref.d.ts CHANGED
@@ -39,6 +39,10 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
39
39
  * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
40
40
  */
41
41
  pathType: string | unknown;
42
+ /**
43
+ * Whether this document/resource should use JSON Schema 2019-09+ nested $id scope semantics.
44
+ */
45
+ dynamicIdScope: boolean;
42
46
  /**
43
47
  * List of all errors. Undefined if no errors.
44
48
  */
package/dist/lib/ref.js CHANGED
@@ -35,6 +35,10 @@ class $Ref {
35
35
  * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
36
36
  */
37
37
  pathType;
38
+ /**
39
+ * Whether this document/resource should use JSON Schema 2019-09+ nested $id scope semantics.
40
+ */
41
+ dynamicIdScope = false;
38
42
  /**
39
43
  * List of all errors. Undefined if no errors.
40
44
  */
@@ -79,6 +79,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
79
79
  * @param path - The file path or URL of the referenced file
80
80
  */
81
81
  _add(path: string): $Ref<S, O>;
82
+ _addAlias(path: string, value: S, pathType?: string | unknown, dynamicIdScope?: boolean): $Ref<S, O>;
82
83
  /**
83
84
  * Resolves the given JSON reference.
84
85
  *
@@ -96,6 +97,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
96
97
  * @protected
97
98
  */
98
99
  _$refs: $RefsMap<S, O>;
100
+ _aliases: $RefsMap<S, O>;
99
101
  /**
100
102
  * The {@link $Ref} object that is the root of the JSON schema.
101
103
  *
@@ -123,5 +125,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
123
125
  * @returns {object}
124
126
  */
125
127
  toJSON: (...types: (string | string[])[]) => S;
128
+ private _getRef;
126
129
  }
127
130
  export {};
package/dist/lib/refs.js CHANGED
@@ -84,10 +84,9 @@ export default class $Refs {
84
84
  */
85
85
  set(path, value) {
86
86
  const absPath = url.resolve(this._root$Ref.path, path);
87
- const withoutHash = url.stripHash(absPath);
88
- const $ref = this._$refs[withoutHash];
87
+ const $ref = this._getRef(absPath);
89
88
  if (!$ref) {
90
- throw new Error(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
89
+ throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
91
90
  }
92
91
  $ref.set(absPath, value);
93
92
  }
@@ -100,8 +99,7 @@ export default class $Refs {
100
99
  */
101
100
  _get$Ref(path) {
102
101
  path = url.resolve(this._root$Ref.path, path);
103
- const withoutHash = url.stripHash(path);
104
- return this._$refs[withoutHash];
102
+ return this._getRef(path);
105
103
  }
106
104
  /**
107
105
  * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object.
@@ -116,6 +114,19 @@ export default class $Refs {
116
114
  this._root$Ref = this._root$Ref || $ref;
117
115
  return $ref;
118
116
  }
117
+ _addAlias(path, value, pathType, dynamicIdScope = false) {
118
+ const withoutHash = url.stripHash(path);
119
+ if (!withoutHash || this._$refs[withoutHash] || this._aliases[withoutHash]) {
120
+ return this._$refs[withoutHash] || this._aliases[withoutHash];
121
+ }
122
+ const $ref = new $Ref(this);
123
+ $ref.path = withoutHash;
124
+ $ref.pathType = pathType;
125
+ $ref.value = value;
126
+ $ref.dynamicIdScope = dynamicIdScope;
127
+ this._aliases[withoutHash] = $ref;
128
+ return $ref;
129
+ }
119
130
  /**
120
131
  * Resolves the given JSON reference.
121
132
  *
@@ -127,10 +138,9 @@ export default class $Refs {
127
138
  */
128
139
  _resolve(path, pathFromRoot, options) {
129
140
  const absPath = url.resolve(this._root$Ref.path, path);
130
- const withoutHash = url.stripHash(absPath);
131
- const $ref = this._$refs[withoutHash];
141
+ const $ref = this._getRef(absPath);
132
142
  if (!$ref) {
133
- throw new Error(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
143
+ throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
134
144
  }
135
145
  return $ref.resolve(absPath, options, path, pathFromRoot);
136
146
  }
@@ -141,6 +151,7 @@ export default class $Refs {
141
151
  * @protected
142
152
  */
143
153
  _$refs = {};
154
+ _aliases = {};
144
155
  /**
145
156
  * The {@link $Ref} object that is the root of the JSON schema.
146
157
  *
@@ -156,6 +167,7 @@ export default class $Refs {
156
167
  */
157
168
  this.circular = false;
158
169
  this._$refs = {};
170
+ this._aliases = {};
159
171
  // @ts-ignore
160
172
  this._root$Ref = null;
161
173
  }
@@ -178,6 +190,10 @@ export default class $Refs {
178
190
  * @returns {object}
179
191
  */
180
192
  toJSON = this.values;
193
+ _getRef(path) {
194
+ const withoutHash = url.stripHash(path);
195
+ return this._$refs[withoutHash] || this._aliases[withoutHash];
196
+ }
181
197
  }
182
198
  /**
183
199
  * Returns the encoded and decoded paths keys of the given object.
@@ -3,6 +3,7 @@ import Pointer from "./pointer.js";
3
3
  import parse from "./parse.js";
4
4
  import * as url from "./util/url.js";
5
5
  import { isHandledError } from "./util/errors.js";
6
+ import { getSchemaBasePath } from "./util/schema-resources.js";
6
7
  /**
7
8
  * Crawls the JSON schema, finds all external JSON references, and resolves their values.
8
9
  * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
@@ -20,7 +21,7 @@ function resolveExternal(parser, options) {
20
21
  }
21
22
  try {
22
23
  // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
23
- const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
24
+ const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, parser.$refs, options);
24
25
  return Promise.all(promises);
25
26
  }
26
27
  catch (e) {
@@ -43,19 +44,21 @@ function resolveExternal(parser, options) {
43
44
  * If any of the JSON references point to files that contain additional JSON references,
44
45
  * then the corresponding promise will internally reference an array of promises.
45
46
  */
46
- function crawl(obj, path, $refs, options, seen, external) {
47
+ function crawl(obj, path, scopeBase, dynamicIdScope, $refs, options, seen, external) {
47
48
  seen ||= new Set();
48
49
  let promises = [];
49
50
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
50
51
  seen.add(obj); // Track previously seen objects to avoid infinite recursion
52
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
51
53
  if ($Ref.isExternal$Ref(obj)) {
52
- promises.push(resolve$Ref(obj, path, $refs, options));
54
+ promises.push(resolve$Ref(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
53
55
  }
54
56
  const keys = Object.keys(obj);
55
57
  for (const key of keys) {
56
58
  const keyPath = Pointer.join(path, key);
57
59
  const value = obj[key];
58
- promises = promises.concat(crawl(value, keyPath, $refs, options, seen, external));
60
+ const childScopeBase = dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
61
+ promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
59
62
  }
60
63
  }
61
64
  return promises;
@@ -72,23 +75,33 @@ function crawl(obj, path, $refs, options, seen, external) {
72
75
  * The promise resolves once all JSON references in the object have been resolved,
73
76
  * including nested references that are contained in externally-referenced files.
74
77
  */
75
- async function resolve$Ref($ref, path, $refs, options) {
78
+ async function resolve$Ref($ref, path, scopeBase, dynamicIdScope, $refs, options) {
76
79
  const shouldResolveOnCwd = options.dereference?.externalReferenceResolution === "root";
77
- const resolvedPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
80
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
81
+ const resolvedPath = url.resolve(resolutionBase, $ref.$ref);
78
82
  const withoutHash = url.stripHash(resolvedPath);
79
83
  // $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
80
84
  // Do we already have this $ref?
81
- const ref = $refs._$refs[withoutHash];
85
+ const ref = $refs._get$Ref(withoutHash);
82
86
  if (ref) {
83
87
  // We've already parsed this $ref, so use the existing value
84
88
  return Promise.resolve(ref.value);
85
89
  }
86
90
  // Parse the $referenced file/url
87
91
  try {
88
- const result = await parse(resolvedPath, $refs, options);
92
+ const reference = $ref.$ref;
93
+ const parseTarget = {
94
+ url: resolvedPath,
95
+ baseUrl: resolutionBase,
96
+ };
97
+ if (typeof reference === "string") {
98
+ parseTarget.reference = reference;
99
+ }
100
+ const result = await parse(parseTarget, $refs, options);
89
101
  // Crawl the parsed value
90
102
  // console.log('Resolving $ref pointers in %s', withoutHash);
91
- const promises = crawl(result, withoutHash + "#", $refs, options, new Set(), true);
103
+ const parsedRef = $refs._get$Ref(withoutHash);
104
+ const promises = crawl(result, withoutHash + "#", withoutHash, parsedRef?.dynamicIdScope ?? false, $refs, options, new Set(), true);
92
105
  return Promise.all(promises);
93
106
  }
94
107
  catch (err) {
@@ -103,6 +103,17 @@ export interface FileInfo {
103
103
  * The full URL of the file. This could be any type of URL, including "http://", "https://", "file://", "ftp://", "mongodb://", or even a local filesystem path (when running in Node.js).
104
104
  */
105
105
  url: string;
106
+ /**
107
+ * The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash.
108
+ * This is useful for custom resolvers that need to preserve relative reference semantics instead
109
+ * of relying on the already-resolved `url`.
110
+ */
111
+ reference?: string;
112
+ /**
113
+ * The URL that `reference` was resolved against to produce `url`.
114
+ * This may include a JSON Pointer fragment when the reference originated from a nested location.
115
+ */
116
+ baseUrl?: string;
106
117
  /**
107
118
  * The hash (URL fragment) of the file URL, including the # symbol. If the URL doesn't have a hash, then this will be an empty string.
108
119
  */
@@ -0,0 +1,6 @@
1
+ import type { ParserOptions } from "../options.js";
2
+ import type { JSONSchema } from "../index.js";
3
+ import type $Refs from "../refs.js";
4
+ export declare function getSchemaBasePath(basePath: string, value: unknown): string;
5
+ export declare function usesDynamicIdScope(value: unknown): boolean;
6
+ export declare function registerSchemaResources<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($refs: $Refs<S, O>, basePath: string, value: unknown, pathType?: string | unknown, dynamicIdScope?: boolean): void;
@@ -0,0 +1,47 @@
1
+ import * as url from "./url.js";
2
+ export function getSchemaBasePath(basePath, value) {
3
+ const schemaId = getSchemaId(value);
4
+ return schemaId ? url.resolve(basePath, schemaId) : basePath;
5
+ }
6
+ export function usesDynamicIdScope(value) {
7
+ if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
8
+ return false;
9
+ }
10
+ const schema = value.$schema;
11
+ if (typeof schema === "string" &&
12
+ (schema.includes("draft/2019-09/") || schema.includes("draft/2020-12/") || schema.includes("oas/3.1/"))) {
13
+ return true;
14
+ }
15
+ const openapi = value.openapi;
16
+ return typeof openapi === "string" && /^3\.1(?:\.|$)/.test(openapi);
17
+ }
18
+ export function registerSchemaResources($refs, basePath, value, pathType, dynamicIdScope = false) {
19
+ if (!dynamicIdScope) {
20
+ return;
21
+ }
22
+ const seen = new Set();
23
+ const visit = (node, scopeBase) => {
24
+ if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
25
+ return;
26
+ }
27
+ seen.add(node);
28
+ const nextScopeBase = getSchemaBasePath(scopeBase, node);
29
+ if (nextScopeBase !== scopeBase) {
30
+ $refs._addAlias(nextScopeBase, node, pathType, dynamicIdScope);
31
+ }
32
+ for (const key of Object.keys(node)) {
33
+ visit(node[key], nextScopeBase);
34
+ }
35
+ };
36
+ visit(value, basePath);
37
+ }
38
+ function getSchemaId(value) {
39
+ if (value &&
40
+ typeof value === "object" &&
41
+ "$id" in value &&
42
+ typeof value.$id === "string" &&
43
+ value.$id.length > 0) {
44
+ return value.$id;
45
+ }
46
+ return undefined;
47
+ }