@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.
package/lib/bundle.ts CHANGED
@@ -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 type $Refs from "./refs.js";
5
6
  import type $RefParser from "./index.js";
6
7
  import type { ParserOptions } from "./index.js";
@@ -37,7 +38,18 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
37
38
 
38
39
  // Build an inventory of all $ref pointers in the JSON Schema
39
40
  const inventory: InventoryEntry[] = [];
40
- crawl<S, O>(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
41
+ crawl<S, O>(
42
+ parser,
43
+ "schema",
44
+ parser.$refs._root$Ref.path + "#",
45
+ parser.$refs._root$Ref.path!,
46
+ parser.$refs._root$Ref.dynamicIdScope,
47
+ "#",
48
+ 0,
49
+ inventory,
50
+ parser.$refs,
51
+ options,
52
+ );
41
53
 
42
54
  // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
43
55
  const rootId =
@@ -71,6 +83,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
71
83
  parent: object | $RefParser<S, O>,
72
84
  key: string | null,
73
85
  path: string,
86
+ scopeBase: string,
87
+ dynamicIdScope: boolean,
74
88
  pathFromRoot: string,
75
89
  indirections: number,
76
90
  inventory: InventoryEntry[],
@@ -82,8 +96,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
82
96
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
83
97
 
84
98
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
99
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
85
100
  if ($Ref.isAllowed$Ref(obj)) {
86
- inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
101
+ inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
87
102
  } else {
88
103
  // Crawl the object in a specific order that's optimized for bundling.
89
104
  // This is important because it determines how `pathFromRoot` gets built,
@@ -108,9 +123,21 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
108
123
  const value = obj[key];
109
124
 
110
125
  if ($Ref.isAllowed$Ref(value)) {
111
- inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
126
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
127
+ inventory$Ref(
128
+ obj,
129
+ key,
130
+ keyPath,
131
+ valueScopeBase,
132
+ dynamicIdScope,
133
+ keyPathFromRoot,
134
+ indirections,
135
+ inventory,
136
+ $refs,
137
+ options,
138
+ );
112
139
  } else {
113
- crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
140
+ crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
114
141
  }
115
142
 
116
143
  // We need to ensure that we have an object to work with here because we may be crawling
@@ -142,6 +169,8 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
142
169
  $refParent: any,
143
170
  $refKey: string | null,
144
171
  path: string,
172
+ scopeBase: string,
173
+ dynamicIdScope: boolean,
145
174
  pathFromRoot: string,
146
175
  indirections: number,
147
176
  inventory: InventoryEntry[],
@@ -149,7 +178,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
149
178
  options: O,
150
179
  ) {
151
180
  const $ref = $refKey === null ? $refParent : $refParent[$refKey];
152
- const $refPath = url.resolve(path, $ref.$ref);
181
+ const $refPath = url.resolve(dynamicIdScope ? scopeBase : path, $ref.$ref);
153
182
  const pointer = $refs._resolve($refPath, pathFromRoot, options);
154
183
  if (pointer === null) {
155
184
  return;
@@ -158,7 +187,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
158
187
  const depth = parsed.length;
159
188
  const file = url.stripHash(pointer.path);
160
189
  const hash = url.getHash(pointer.path);
161
- const external = file !== $refs._root$Ref.path;
190
+ const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
162
191
  const extended = $Ref.isExtended$Ref($ref);
163
192
  indirections += pointer.indirections;
164
193
 
@@ -189,7 +218,18 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
189
218
 
190
219
  // Recursively crawl the resolved value
191
220
  if (!existingEntry || external) {
192
- crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
221
+ crawl(
222
+ pointer.value,
223
+ null,
224
+ pointer.path,
225
+ pointer.$ref.path!,
226
+ pointer.$ref.dynamicIdScope,
227
+ pathFromRoot,
228
+ indirections + 1,
229
+ inventory,
230
+ $refs,
231
+ options,
232
+ );
193
233
  }
194
234
  }
195
235
 
@@ -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 type $Refs from "./refs.js";
5
6
  import type { DereferenceOptions, ParserOptions } from "./options.js";
6
7
  import { type $RefParser, type JSONSchema } from "./index.js";
@@ -24,6 +25,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
24
25
  const dereferenced = crawl<S, O>(
25
26
  parser.schema,
26
27
  parser.$refs._root$Ref.path!,
28
+ parser.$refs._root$Ref.path!,
29
+ parser.$refs._root$Ref.dynamicIdScope,
27
30
  "#",
28
31
  new Set(),
29
32
  new Set(),
@@ -55,6 +58,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
55
58
  function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
56
59
  obj: any,
57
60
  path: string,
61
+ scopeBase: string,
62
+ dynamicIdScope: boolean,
58
63
  pathFromRoot: string,
59
64
  parents: Set<any>,
60
65
  processedObjects: Set<any>,
@@ -87,11 +92,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
87
92
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
88
93
  parents.add(obj);
89
94
  processedObjects.add(obj);
95
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
90
96
 
91
97
  if ($Ref.isAllowed$Ref(obj, options)) {
92
98
  dereferenced = dereference$Ref(
93
99
  obj,
94
100
  path,
101
+ currentScopeBase,
102
+ dynamicIdScope,
95
103
  pathFromRoot,
96
104
  parents,
97
105
  processedObjects,
@@ -118,9 +126,12 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
118
126
  let circular;
119
127
 
120
128
  if ($Ref.isAllowed$Ref(value, options)) {
129
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
121
130
  dereferenced = dereference$Ref(
122
131
  value,
123
132
  keyPath,
133
+ valueScopeBase,
134
+ dynamicIdScope,
124
135
  keyPathFromRoot,
125
136
  parents,
126
137
  processedObjects,
@@ -172,6 +183,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
172
183
  dereferenced = crawl(
173
184
  value,
174
185
  keyPath,
186
+ currentScopeBase,
187
+ dynamicIdScope,
175
188
  keyPathFromRoot,
176
189
  parents,
177
190
  processedObjects,
@@ -219,6 +232,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
219
232
  function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
220
233
  $ref: any,
221
234
  path: string,
235
+ scopeBase: string,
236
+ dynamicIdScope: boolean,
222
237
  pathFromRoot: string,
223
238
  parents: Set<any>,
224
239
  processedObjects: any,
@@ -230,7 +245,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
230
245
  ) {
231
246
  const isExternalRef = $Ref.isExternal$Ref($ref);
232
247
  const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
233
- const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
248
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
249
+ const $refPath = url.resolve(resolutionBase, $ref.$ref);
234
250
 
235
251
  const cache = dereferencedCache.get($refPath);
236
252
 
@@ -310,6 +326,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
310
326
  const dereferenced = crawl(
311
327
  dereferencedValue,
312
328
  pointer.path,
329
+ pointer.$ref.path!,
330
+ pointer.$ref.dynamicIdScope,
313
331
  pathFromRoot,
314
332
  parents,
315
333
  processedObjects,
package/lib/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  JSONParserErrorGroup,
20
20
  } from "./util/errors.js";
21
21
  import maybe from "./util/maybe.js";
22
+ import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
22
23
  import type { ParserOptions } from "./options.js";
23
24
  import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
24
25
  import type {
@@ -115,6 +116,8 @@ export class $RefParser<S extends object = JSONSchema, O extends ParserOptions<S
115
116
  const $ref = this.$refs._add(args.path);
116
117
  $ref.value = args.schema;
117
118
  $ref.pathType = pathType;
119
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
120
+ registerSchemaResources(this.$refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
118
121
  promise = Promise.resolve(args.schema);
119
122
  } else {
120
123
  // Parse the schema file/url
package/lib/parse.ts CHANGED
@@ -1,4 +1,5 @@
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 {
4
5
  ResolverError,
@@ -11,14 +12,24 @@ import type $Refs from "./refs.js";
11
12
  import type { ParserOptions } from "./options.js";
12
13
  import type { FileInfo, JSONSchema } from "./types/index.js";
13
14
 
15
+ interface ParseTarget {
16
+ url: string;
17
+ reference?: string;
18
+ baseUrl?: string;
19
+ }
20
+
14
21
  /**
15
22
  * Reads and parses the specified file path or URL.
16
23
  */
17
24
  async function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
18
- path: string,
25
+ target: string | ParseTarget,
19
26
  $refs: $Refs<S, O>,
20
27
  options: O,
21
28
  ) {
29
+ let path = typeof target === "string" ? target : target.url;
30
+ const baseUrl = typeof target === "string" ? undefined : target.baseUrl;
31
+ let reference = typeof target === "string" ? undefined : target.reference;
32
+
22
33
  // Remove the URL fragment, if any
23
34
  const hashIndex = path.indexOf("#");
24
35
  let hash = "";
@@ -27,6 +38,12 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
27
38
  // Remove the URL fragment, if any
28
39
  path = path.substring(0, hashIndex);
29
40
  }
41
+ if (reference) {
42
+ const referenceHashIndex = reference.indexOf("#");
43
+ if (referenceHashIndex >= 0) {
44
+ reference = reference.substring(0, referenceHashIndex);
45
+ }
46
+ }
30
47
 
31
48
  // Add a new $Ref for this file, even though we don't have the value yet.
32
49
  // This ensures that we don't simultaneously read & parse the same file multiple times
@@ -37,6 +54,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
37
54
  url: path,
38
55
  hash,
39
56
  extension: url.getExtension(path),
57
+ ...(reference !== undefined ? { reference } : {}),
58
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
40
59
  } as FileInfo;
41
60
 
42
61
  // Read the file and then parse the data
@@ -47,6 +66,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
47
66
 
48
67
  const parser = await parseFile<S, O>(file, options, $refs);
49
68
  $ref.value = parser.result;
69
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
70
+ registerSchemaResources($refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
50
71
 
51
72
  return parser.result;
52
73
  } catch (err) {
package/lib/pointer.ts CHANGED
@@ -3,6 +3,7 @@ import type { ParserOptions } from "./options.js";
3
3
  import $Ref from "./ref.js";
4
4
  import * as url from "./util/url.js";
5
5
  import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js";
6
+ import { getSchemaBasePath } from "./util/schema-resources.js";
6
7
  import type { JSONSchema } from "./index.js";
7
8
  import type { JSONSchema4Type, JSONSchema6Type, JSONSchema7Type } from "json-schema";
8
9
 
@@ -38,6 +39,11 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
38
39
  */
39
40
  originalPath: string;
40
41
 
42
+ /**
43
+ * The current base URI used to resolve nested $ref pointers while walking this pointer.
44
+ */
45
+ scopeBase: string;
46
+
41
47
  /**
42
48
  * The value of the JSON pointer.
43
49
  * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
@@ -61,6 +67,8 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
61
67
 
62
68
  this.originalPath = friendlyPath || path;
63
69
 
70
+ this.scopeBase = $ref.path || url.stripHash(path);
71
+
64
72
  this.value = undefined;
65
73
 
66
74
  this.circular = false;
@@ -87,6 +95,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
87
95
 
88
96
  // Crawl the object, one token at a time
89
97
  this.value = unwrapOrThrow(obj);
98
+ if (this.$ref.dynamicIdScope) {
99
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
100
+ }
90
101
 
91
102
  for (let i = 0; i < tokens.length; i++) {
92
103
  // During token walking, if the current value is an extended $ref (has sibling keys
@@ -149,10 +160,14 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
149
160
  }
150
161
 
151
162
  found.push(token);
163
+ if (this.$ref.dynamicIdScope) {
164
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
165
+ }
152
166
  }
153
167
 
154
168
  // Resolve the final value
155
- if (!this.value || (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)) {
169
+ const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
170
+ if (!this.value || (this.value.$ref && url.resolve(finalResolutionBase, this.value.$ref) !== pathFromRoot)) {
156
171
  resolveIf$Ref(this, options, pathFromRoot);
157
172
  }
158
173
 
@@ -181,6 +196,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
181
196
 
182
197
  // Crawl the object, one token at a time
183
198
  this.value = unwrapOrThrow(obj);
199
+ if (this.$ref.dynamicIdScope) {
200
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
201
+ }
184
202
 
185
203
  for (let i = 0; i < tokens.length - 1; i++) {
186
204
  resolveIf$Ref(this, options);
@@ -193,6 +211,10 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
193
211
  // The token doesn't exist, so create it
194
212
  this.value = setValue(this, token, {});
195
213
  }
214
+
215
+ if (this.$ref.dynamicIdScope) {
216
+ this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
217
+ }
196
218
  }
197
219
 
198
220
  // Set the value of the final token
@@ -287,7 +309,8 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
287
309
  // Is the value a JSON reference? (and allowed?)
288
310
 
289
311
  if ($Ref.isAllowed$Ref(pointer.value, options)) {
290
- const $refPath = url.resolve(pointer.path, pointer.value.$ref);
312
+ const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
313
+ const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
291
314
 
292
315
  if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
293
316
  // The value is a reference to itself, so there's nothing to do.
@@ -304,12 +327,18 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
304
327
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
305
328
  // So the resolved path does NOT change. Just the value does.
306
329
  pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
330
+ if (pointer.$ref.dynamicIdScope) {
331
+ pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
332
+ }
307
333
  return false;
308
334
  } else {
309
335
  // Resolve the reference
310
336
  pointer.$ref = resolved.$ref;
311
337
  pointer.path = resolved.path;
312
338
  pointer.value = resolved.value;
339
+ pointer.scopeBase = pointer.$ref.dynamicIdScope
340
+ ? getSchemaBasePath(pointer.$ref.path!, pointer.value)
341
+ : pointer.$ref.path!;
313
342
  }
314
343
 
315
344
  return true;
package/lib/ref.ts CHANGED
@@ -47,6 +47,11 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
47
47
  */
48
48
  pathType: string | unknown;
49
49
 
50
+ /**
51
+ * Whether this document/resource should use JSON Schema 2019-09+ nested $id scope semantics.
52
+ */
53
+ dynamicIdScope = false;
54
+
50
55
  /**
51
56
  * List of all errors. Undefined if no errors.
52
57
  */
package/lib/refs.ts CHANGED
@@ -95,11 +95,10 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
95
95
  */
96
96
  set(path: string, value: JSONSchema4Type | JSONSchema6Type | JSONSchema7Type) {
97
97
  const absPath = url.resolve(this._root$Ref.path!, path);
98
- const withoutHash = url.stripHash(absPath);
99
- const $ref = this._$refs[withoutHash];
98
+ const $ref = this._getRef(absPath);
100
99
 
101
100
  if (!$ref) {
102
- throw new Error(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
101
+ throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
103
102
  }
104
103
 
105
104
  $ref.set(absPath, value);
@@ -113,8 +112,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
113
112
  */
114
113
  _get$Ref(path: string) {
115
114
  path = url.resolve(this._root$Ref.path!, path);
116
- const withoutHash = url.stripHash(path);
117
- return this._$refs[withoutHash];
115
+ return this._getRef(path);
118
116
  }
119
117
 
120
118
  /**
@@ -134,6 +132,23 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
134
132
  return $ref;
135
133
  }
136
134
 
135
+ _addAlias(path: string, value: S, pathType?: string | unknown, dynamicIdScope = false) {
136
+ const withoutHash = url.stripHash(path);
137
+
138
+ if (!withoutHash || this._$refs[withoutHash] || this._aliases[withoutHash]) {
139
+ return this._$refs[withoutHash] || this._aliases[withoutHash];
140
+ }
141
+
142
+ const $ref = new $Ref<S, O>(this);
143
+ $ref.path = withoutHash;
144
+ $ref.pathType = pathType;
145
+ $ref.value = value;
146
+ $ref.dynamicIdScope = dynamicIdScope;
147
+
148
+ this._aliases[withoutHash] = $ref;
149
+ return $ref;
150
+ }
151
+
137
152
  /**
138
153
  * Resolves the given JSON reference.
139
154
  *
@@ -145,11 +160,10 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
145
160
  */
146
161
  _resolve(path: string, pathFromRoot: string, options?: O) {
147
162
  const absPath = url.resolve(this._root$Ref.path!, path);
148
- const withoutHash = url.stripHash(absPath);
149
- const $ref = this._$refs[withoutHash];
163
+ const $ref = this._getRef(absPath);
150
164
 
151
165
  if (!$ref) {
152
- throw new Error(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
166
+ throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
153
167
  }
154
168
 
155
169
  return $ref.resolve(absPath, options, path, pathFromRoot);
@@ -163,6 +177,8 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
163
177
  */
164
178
  _$refs: $RefsMap<S, O> = {};
165
179
 
180
+ _aliases: $RefsMap<S, O> = {};
181
+
166
182
  /**
167
183
  * The {@link $Ref} object that is the root of the JSON schema.
168
184
  *
@@ -180,6 +196,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
180
196
  this.circular = false;
181
197
 
182
198
  this._$refs = {};
199
+ this._aliases = {};
183
200
 
184
201
  // @ts-ignore
185
202
  this._root$Ref = null;
@@ -205,6 +222,11 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
205
222
  * @returns {object}
206
223
  */
207
224
  toJSON = this.values;
225
+
226
+ private _getRef(path: string) {
227
+ const withoutHash = url.stripHash(path);
228
+ return this._$refs[withoutHash] || this._aliases[withoutHash];
229
+ }
208
230
  }
209
231
 
210
232
  /**
@@ -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
  import type $Refs from "./refs.js";
7
8
  import type { ParserOptions } from "./options.js";
8
9
  import type { JSONSchema } from "./types/index.js";
@@ -29,7 +30,14 @@ function resolveExternal<S extends object = JSONSchema, O extends ParserOptions<
29
30
 
30
31
  try {
31
32
  // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
32
- const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
33
+ const promises = crawl(
34
+ parser.schema,
35
+ parser.$refs._root$Ref.path + "#",
36
+ parser.$refs._root$Ref.path!,
37
+ parser.$refs._root$Ref.dynamicIdScope,
38
+ parser.$refs,
39
+ options,
40
+ );
33
41
  return Promise.all(promises);
34
42
  } catch (e) {
35
43
  return Promise.reject(e);
@@ -55,6 +63,8 @@ function resolveExternal<S extends object = JSONSchema, O extends ParserOptions<
55
63
  function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
56
64
  obj: string | Buffer | S | undefined | null,
57
65
  path: string,
66
+ scopeBase: string,
67
+ dynamicIdScope: boolean,
58
68
  $refs: $Refs<S, O>,
59
69
  options: O,
60
70
  seen?: Set<any>,
@@ -65,15 +75,18 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
65
75
 
66
76
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
67
77
  seen.add(obj); // Track previously seen objects to avoid infinite recursion
78
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
68
79
  if ($Ref.isExternal$Ref(obj)) {
69
- promises.push(resolve$Ref<S, O>(obj, path, $refs, options));
80
+ promises.push(resolve$Ref<S, O>(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
70
81
  }
71
82
 
72
83
  const keys = Object.keys(obj) as string[];
73
84
  for (const key of keys) {
74
85
  const keyPath = Pointer.join(path, key);
75
86
  const value = obj[key as keyof typeof obj] as string | JSONSchema | Buffer | undefined;
76
- promises = promises.concat(crawl(value, keyPath, $refs, options, seen, external));
87
+ const childScopeBase =
88
+ dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
89
+ promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
77
90
  }
78
91
  }
79
92
 
@@ -95,17 +108,20 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
95
108
  async function resolve$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
96
109
  $ref: S,
97
110
  path: string,
111
+ scopeBase: string,
112
+ dynamicIdScope: boolean,
98
113
  $refs: $Refs<S, O>,
99
114
  options: O,
100
115
  ) {
101
116
  const shouldResolveOnCwd = options.dereference?.externalReferenceResolution === "root";
102
- const resolvedPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, ($ref as JSONSchema).$ref!);
117
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
118
+ const resolvedPath = url.resolve(resolutionBase, ($ref as JSONSchema).$ref!);
103
119
  const withoutHash = url.stripHash(resolvedPath);
104
120
 
105
121
  // $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
106
122
 
107
123
  // Do we already have this $ref?
108
- const ref = $refs._$refs[withoutHash];
124
+ const ref = $refs._get$Ref(withoutHash);
109
125
  if (ref) {
110
126
  // We've already parsed this $ref, so use the existing value
111
127
  return Promise.resolve(ref.value);
@@ -113,11 +129,33 @@ async function resolve$Ref<S extends object = JSONSchema, O extends ParserOption
113
129
 
114
130
  // Parse the $referenced file/url
115
131
  try {
116
- const result = await parse(resolvedPath, $refs, options);
132
+ const reference = ($ref as JSONSchema).$ref;
133
+ const parseTarget: { url: string; baseUrl: string; reference?: string } = {
134
+ url: resolvedPath,
135
+ baseUrl: resolutionBase,
136
+ };
137
+ if (typeof reference === "string") {
138
+ parseTarget.reference = reference;
139
+ }
140
+ const result = await parse(
141
+ parseTarget,
142
+ $refs,
143
+ options,
144
+ );
117
145
 
118
146
  // Crawl the parsed value
119
147
  // console.log('Resolving $ref pointers in %s', withoutHash);
120
- const promises = crawl(result, withoutHash + "#", $refs, options, new Set(), true);
148
+ const parsedRef = $refs._get$Ref(withoutHash);
149
+ const promises = crawl(
150
+ result,
151
+ withoutHash + "#",
152
+ withoutHash,
153
+ parsedRef?.dynamicIdScope ?? false,
154
+ $refs,
155
+ options,
156
+ new Set(),
157
+ true,
158
+ );
121
159
 
122
160
  return Promise.all(promises);
123
161
  } catch (err) {
@@ -139,6 +139,19 @@ export interface FileInfo {
139
139
  */
140
140
  url: string;
141
141
 
142
+ /**
143
+ * The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash.
144
+ * This is useful for custom resolvers that need to preserve relative reference semantics instead
145
+ * of relying on the already-resolved `url`.
146
+ */
147
+ reference?: string;
148
+
149
+ /**
150
+ * The URL that `reference` was resolved against to produce `url`.
151
+ * This may include a JSON Pointer fragment when the reference originated from a nested location.
152
+ */
153
+ baseUrl?: string;
154
+
142
155
  /**
143
156
  * 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.
144
157
  */