@apidevtools/json-schema-ref-parser 15.3.6 → 15.5.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/dist/lib/bundle.d.ts +1 -0
- package/dist/lib/bundle.js +6 -2
- package/dist/lib/dereference.js +2 -2
- package/dist/lib/options.d.ts +1 -0
- package/dist/lib/pointer.d.ts +5 -1
- package/dist/lib/pointer.js +81 -24
- package/dist/lib/ref.d.ts +3 -1
- package/dist/lib/ref.js +4 -2
- package/dist/lib/refs.d.ts +3 -1
- package/dist/lib/refs.js +4 -2
- package/lib/bundle.ts +7 -2
- package/lib/dereference.ts +2 -2
- package/lib/options.ts +1 -0
- package/lib/pointer.ts +99 -23
- package/lib/ref.ts +11 -2
- package/lib/refs.ts +10 -2
- package/package.json +1 -1
package/dist/lib/bundle.d.ts
CHANGED
package/dist/lib/bundle.js
CHANGED
|
@@ -119,6 +119,7 @@ function inventory$Ref($refParent, $refKey, path, scopeBase, dynamicIdScope, pat
|
|
|
119
119
|
const file = url.stripHash(pointer.path);
|
|
120
120
|
const hash = url.getHash(pointer.path);
|
|
121
121
|
const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
|
|
122
|
+
const nestedResource = Boolean($refs._aliases[file]) && pointer.$ref.value !== $refs._root$Ref.value;
|
|
122
123
|
const extended = $Ref.isExtended$Ref($ref);
|
|
123
124
|
indirections += pointer.indirections;
|
|
124
125
|
const existingEntry = findInInventory(inventory, $refParent, $refKey);
|
|
@@ -143,6 +144,7 @@ function inventory$Ref($refParent, $refKey, path, scopeBase, dynamicIdScope, pat
|
|
|
143
144
|
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
|
|
144
145
|
extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
|
|
145
146
|
external, // Does this $ref pointer point to a file other than the main JSON Schema file?
|
|
147
|
+
nestedResource, // Does this $ref resolve to an embedded schema resource with its own $id?
|
|
146
148
|
indirections, // The number of indirect references that were traversed to resolve the value
|
|
147
149
|
});
|
|
148
150
|
// Recursively crawl the resolved value
|
|
@@ -221,8 +223,10 @@ function remap(inventory, options, rootId) {
|
|
|
221
223
|
if (!entry.external) {
|
|
222
224
|
// This $ref already resolves to the main JSON Schema file.
|
|
223
225
|
// When optimizeInternalRefs is false, preserve the original internal ref path
|
|
224
|
-
// instead of rewriting it to the fully resolved hash.
|
|
225
|
-
|
|
226
|
+
// instead of rewriting it to the fully resolved hash. References to nested
|
|
227
|
+
// resources must also retain their resource URI so that "#" does not point
|
|
228
|
+
// at the document root instead.
|
|
229
|
+
if (bundleOpts.optimizeInternalRefs !== false && !entry.nestedResource) {
|
|
226
230
|
entry.$ref.$ref = entry.hash;
|
|
227
231
|
}
|
|
228
232
|
}
|
package/dist/lib/dereference.js
CHANGED
|
@@ -210,7 +210,7 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
|
|
|
210
210
|
}
|
|
211
211
|
// Check for circular references
|
|
212
212
|
const directCircular = pointer.circular;
|
|
213
|
-
let circular = directCircular || parents.has(pointer.value);
|
|
213
|
+
let circular = directCircular || pointer.chainCircular || parents.has(pointer.value);
|
|
214
214
|
if (circular) {
|
|
215
215
|
foundCircularReference(path, $refs, options);
|
|
216
216
|
}
|
|
@@ -227,7 +227,7 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
|
|
|
227
227
|
// The user has chosen to "ignore" circular references, so don't change the value
|
|
228
228
|
dereferencedValue = $ref;
|
|
229
229
|
}
|
|
230
|
-
if (directCircular) {
|
|
230
|
+
if (directCircular && dereferencedValue !== $ref) {
|
|
231
231
|
// The pointer is a DIRECT circular reference (i.e. it references itself).
|
|
232
232
|
// So replace the $ref path with the absolute path from the JSON Schema root
|
|
233
233
|
dereferencedValue.$ref = pathFromRoot;
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export interface DereferenceOptions {
|
|
|
82
82
|
* The maximum recursion depth for dereferencing nested schemas.
|
|
83
83
|
* If the schema nesting exceeds this depth, a RangeError will be thrown
|
|
84
84
|
* with a descriptive message instead of crashing with a stack overflow.
|
|
85
|
+
* This limit also applies to `$ref` resolution chains.
|
|
85
86
|
*
|
|
86
87
|
* Default: 500
|
|
87
88
|
*/
|
package/dist/lib/pointer.d.ts
CHANGED
|
@@ -38,6 +38,10 @@ declare class Pointer<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
38
38
|
* Indicates whether the pointer references itself.
|
|
39
39
|
*/
|
|
40
40
|
circular: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Indicates whether the pointer encountered a cycle elsewhere in its reference chain.
|
|
43
|
+
*/
|
|
44
|
+
chainCircular: boolean;
|
|
41
45
|
/**
|
|
42
46
|
* The number of indirect references that were traversed to resolve the value.
|
|
43
47
|
* Resolving a single pointer may require resolving multiple $Refs.
|
|
@@ -57,7 +61,7 @@ declare class Pointer<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
57
61
|
* the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
|
|
58
62
|
* of the resolved value.
|
|
59
63
|
*/
|
|
60
|
-
resolve(obj: S, options?: O, pathFromRoot?: string): this;
|
|
64
|
+
resolve(obj: S, options?: O, pathFromRoot?: string, visitedRefPaths?: Set<string>, resolveFinalReference?: boolean): this;
|
|
61
65
|
/**
|
|
62
66
|
* Sets the value of a nested property within the given object.
|
|
63
67
|
*
|
package/dist/lib/pointer.js
CHANGED
|
@@ -43,6 +43,10 @@ class Pointer {
|
|
|
43
43
|
* Indicates whether the pointer references itself.
|
|
44
44
|
*/
|
|
45
45
|
circular;
|
|
46
|
+
/**
|
|
47
|
+
* Indicates whether the pointer encountered a cycle elsewhere in its reference chain.
|
|
48
|
+
*/
|
|
49
|
+
chainCircular;
|
|
46
50
|
/**
|
|
47
51
|
* The number of indirect references that were traversed to resolve the value.
|
|
48
52
|
* Resolving a single pointer may require resolving multiple $Refs.
|
|
@@ -55,6 +59,7 @@ class Pointer {
|
|
|
55
59
|
this.scopeBase = $ref.path || url.stripHash(path);
|
|
56
60
|
this.value = undefined;
|
|
57
61
|
this.circular = false;
|
|
62
|
+
this.chainCircular = false;
|
|
58
63
|
this.indirections = 0;
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
@@ -70,7 +75,7 @@ class Pointer {
|
|
|
70
75
|
* the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
|
|
71
76
|
* of the resolved value.
|
|
72
77
|
*/
|
|
73
|
-
resolve(obj, options, pathFromRoot) {
|
|
78
|
+
resolve(obj, options, pathFromRoot, visitedRefPaths = new Set(), resolveFinalReference = true) {
|
|
74
79
|
const tokens = Pointer.parse(this.path, this.originalPath);
|
|
75
80
|
const found = [];
|
|
76
81
|
// Crawl the object, one token at a time
|
|
@@ -86,16 +91,18 @@ class Pointer {
|
|
|
86
91
|
// This prevents false circular detection when e.g. a root schema has both
|
|
87
92
|
// $ref: "#/$defs/Foo" and $defs: { Foo: {...} } as siblings.
|
|
88
93
|
const wasCircular = this.circular;
|
|
94
|
+
const wasChainCircular = this.chainCircular;
|
|
89
95
|
const isExtendedRef = $Ref.isExtended$Ref(this.value);
|
|
90
|
-
if (resolveIf$Ref(this, options, pathFromRoot)) {
|
|
96
|
+
if (resolveIf$Ref(this, options, pathFromRoot, visitedRefPaths)) {
|
|
91
97
|
// The $ref path has changed, so append the remaining tokens to the path
|
|
92
98
|
this.path = Pointer.join(this.path, tokens.slice(i));
|
|
93
99
|
}
|
|
94
|
-
else if (
|
|
100
|
+
else if (isExtendedRef) {
|
|
95
101
|
// resolveIf$Ref set circular=true on an extended $ref during token walking.
|
|
96
102
|
// Since we still have tokens to process, the object should be walked by its
|
|
97
103
|
// properties, not treated as a circular self-reference.
|
|
98
|
-
this.circular =
|
|
104
|
+
this.circular = wasCircular;
|
|
105
|
+
this.chainCircular = wasChainCircular;
|
|
99
106
|
}
|
|
100
107
|
const token = tokens[i];
|
|
101
108
|
if (this.value[token] === undefined || (this.value[token] === null && i === tokens.length - 1)) {
|
|
@@ -111,6 +118,7 @@ class Pointer {
|
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
if (didFindSubstringSlashMatch) {
|
|
121
|
+
this.chainCircular = wasChainCircular;
|
|
114
122
|
continue;
|
|
115
123
|
}
|
|
116
124
|
// If the token we're looking for ended up not containing any slashes but is
|
|
@@ -120,6 +128,7 @@ class Pointer {
|
|
|
120
128
|
// We use a `null` symbol for internal tracking to differentiate between a general `null`
|
|
121
129
|
// value and our expected `null` value.
|
|
122
130
|
this.value = nullSymbol;
|
|
131
|
+
this.chainCircular = wasChainCircular;
|
|
123
132
|
continue;
|
|
124
133
|
}
|
|
125
134
|
this.value = null;
|
|
@@ -132,6 +141,7 @@ class Pointer {
|
|
|
132
141
|
else {
|
|
133
142
|
this.value = this.value[token];
|
|
134
143
|
}
|
|
144
|
+
this.chainCircular = wasChainCircular;
|
|
135
145
|
found.push(token);
|
|
136
146
|
if (this.$ref.dynamicIdScope) {
|
|
137
147
|
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
@@ -139,8 +149,17 @@ class Pointer {
|
|
|
139
149
|
}
|
|
140
150
|
// Resolve the final value
|
|
141
151
|
const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
|
|
142
|
-
if (
|
|
143
|
-
|
|
152
|
+
if (resolveFinalReference) {
|
|
153
|
+
const finalRefPath = this.value?.$ref ? url.resolve(finalResolutionBase, this.value.$ref) : undefined;
|
|
154
|
+
const canonicalPathFromRoot = typeof pathFromRoot === "string" ? url.resolve(this.$ref.$refs._root$Ref.path, pathFromRoot) : pathFromRoot;
|
|
155
|
+
if ($Ref.isAllowed$Ref(this.value, options) &&
|
|
156
|
+
finalRefPath === canonicalPathFromRoot &&
|
|
157
|
+
finalRefPath !== this.path) {
|
|
158
|
+
this.chainCircular = true;
|
|
159
|
+
}
|
|
160
|
+
else if (!this.value || finalRefPath) {
|
|
161
|
+
resolveIf$Ref(this, options, pathFromRoot, visitedRefPaths);
|
|
162
|
+
}
|
|
144
163
|
}
|
|
145
164
|
return this;
|
|
146
165
|
}
|
|
@@ -256,26 +275,58 @@ class Pointer {
|
|
|
256
275
|
* @param [pathFromRoot] - the path of place that initiated resolving
|
|
257
276
|
* @returns - Returns `true` if the resolution path changed
|
|
258
277
|
*/
|
|
259
|
-
function resolveIf$Ref(pointer, options, pathFromRoot) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
278
|
+
function resolveIf$Ref(pointer, options, pathFromRoot, visitedRefPaths = new Set()) {
|
|
279
|
+
let pathChanged = false;
|
|
280
|
+
let currentPathFromRoot = pathFromRoot;
|
|
281
|
+
const addedPaths = [];
|
|
282
|
+
try {
|
|
283
|
+
// Pure reference chains can be followed iteratively. Extended refs still use
|
|
284
|
+
// the existing one-hop behavior because their values must be merged on return.
|
|
285
|
+
while ($Ref.isAllowed$Ref(pointer.value, options)) {
|
|
286
|
+
const extended = $Ref.isExtended$Ref(pointer.value);
|
|
287
|
+
const sourceValue = pointer.value;
|
|
288
|
+
const parentPath = pointer.path;
|
|
289
|
+
const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
|
|
290
|
+
const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
|
|
291
|
+
if ($refPath === pointer.path && !isRootPath(currentPathFromRoot)) {
|
|
292
|
+
// The value is a reference to itself, so there's nothing to do.
|
|
293
|
+
pointer.circular = true;
|
|
294
|
+
pointer.chainCircular = true;
|
|
295
|
+
return pathChanged;
|
|
296
|
+
}
|
|
297
|
+
const canonicalPathFromRoot = typeof currentPathFromRoot === "string"
|
|
298
|
+
? url.resolve(pointer.$ref.$refs._root$Ref.path, currentPathFromRoot)
|
|
299
|
+
: currentPathFromRoot;
|
|
300
|
+
if ($refPath === canonicalPathFromRoot && $refPath !== pointer.path) {
|
|
301
|
+
pointer.chainCircular = true;
|
|
302
|
+
return pathChanged;
|
|
303
|
+
}
|
|
304
|
+
if (visitedRefPaths.has($refPath)) {
|
|
305
|
+
pointer.chainCircular = true;
|
|
306
|
+
return pathChanged;
|
|
307
|
+
}
|
|
308
|
+
const maxDepth = options?.dereference?.maxDepth ?? 500;
|
|
309
|
+
if (visitedRefPaths.size >= maxDepth) {
|
|
310
|
+
throw new RangeError(`Maximum dereference depth (${maxDepth}) exceeded at ${pointer.path}. ` +
|
|
311
|
+
`This likely indicates an extremely deep or recursive schema. ` +
|
|
312
|
+
`You can increase this limit with the dereference.maxDepth option.`);
|
|
313
|
+
}
|
|
314
|
+
visitedRefPaths.add($refPath);
|
|
315
|
+
addedPaths.push($refPath);
|
|
316
|
+
const resolved = pointer.$ref.$refs._resolve($refPath, parentPath, options, visitedRefPaths, extended);
|
|
270
317
|
if (resolved === null) {
|
|
271
|
-
return
|
|
318
|
+
return pathChanged;
|
|
272
319
|
}
|
|
273
320
|
pointer.indirections += resolved.indirections + 1;
|
|
274
|
-
|
|
321
|
+
pointer.chainCircular ||= resolved.circular || resolved.chainCircular;
|
|
322
|
+
if (extended) {
|
|
275
323
|
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
|
|
276
324
|
// So the resolved path does NOT change. Just the value does.
|
|
277
|
-
|
|
278
|
-
|
|
325
|
+
if (pointer.chainCircular) {
|
|
326
|
+
return pathChanged;
|
|
327
|
+
}
|
|
328
|
+
pointer.value = $Ref.dereference(sourceValue, resolved.value, options);
|
|
329
|
+
return pathChanged;
|
|
279
330
|
}
|
|
280
331
|
else {
|
|
281
332
|
// Resolve the reference
|
|
@@ -285,12 +336,18 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
|
|
|
285
336
|
// `pointer.$ref.path` is already the canonical location of the resolved resource.
|
|
286
337
|
// Re-applying the resource's own `$id` here would duplicate nested path segments
|
|
287
338
|
// such as `nested/nested/foo.json`.
|
|
288
|
-
pointer.scopeBase =
|
|
339
|
+
pointer.scopeBase = resolved.scopeBase;
|
|
340
|
+
currentPathFromRoot = parentPath;
|
|
341
|
+
pathChanged = true;
|
|
289
342
|
}
|
|
290
|
-
|
|
343
|
+
}
|
|
344
|
+
return pathChanged;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
for (const path of addedPaths) {
|
|
348
|
+
visitedRefPaths.delete(path);
|
|
291
349
|
}
|
|
292
350
|
}
|
|
293
|
-
return undefined;
|
|
294
351
|
}
|
|
295
352
|
export default Pointer;
|
|
296
353
|
/**
|
package/dist/lib/ref.d.ts
CHANGED
|
@@ -78,9 +78,11 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
|
|
|
78
78
|
* @param options
|
|
79
79
|
* @param friendlyPath - The original user-specified path (used for error messages)
|
|
80
80
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
81
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
82
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
81
83
|
* @returns
|
|
82
84
|
*/
|
|
83
|
-
resolve(path: string, options?: O, friendlyPath?: string, pathFromRoot?: string): Pointer<S, O> | null;
|
|
85
|
+
resolve(path: string, options?: O, friendlyPath?: string, pathFromRoot?: string, visitedRefPaths?: Set<string>, resolveFinalReference?: boolean): Pointer<S, O> | null;
|
|
84
86
|
/**
|
|
85
87
|
* Sets the value of a nested property within this {@link $Ref#value}.
|
|
86
88
|
* If the property, or any of its parents don't exist, they will be created.
|
package/dist/lib/ref.js
CHANGED
|
@@ -100,12 +100,14 @@ class $Ref {
|
|
|
100
100
|
* @param options
|
|
101
101
|
* @param friendlyPath - The original user-specified path (used for error messages)
|
|
102
102
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
103
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
104
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
103
105
|
* @returns
|
|
104
106
|
*/
|
|
105
|
-
resolve(path, options, friendlyPath, pathFromRoot) {
|
|
107
|
+
resolve(path, options, friendlyPath, pathFromRoot, visitedRefPaths, resolveFinalReference = true) {
|
|
106
108
|
const pointer = new Pointer(this, path, friendlyPath);
|
|
107
109
|
try {
|
|
108
|
-
const resolved = pointer.resolve(this.value, options, pathFromRoot);
|
|
110
|
+
const resolved = pointer.resolve(this.value, options, pathFromRoot, visitedRefPaths, resolveFinalReference);
|
|
109
111
|
if (resolved.value === nullSymbol) {
|
|
110
112
|
resolved.value = null;
|
|
111
113
|
}
|
package/dist/lib/refs.d.ts
CHANGED
|
@@ -86,10 +86,12 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
|
|
|
86
86
|
* @param path - The path being resolved, optionally with a JSON pointer in the hash
|
|
87
87
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
88
88
|
* @param [options]
|
|
89
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
90
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
89
91
|
* @returns
|
|
90
92
|
* @protected
|
|
91
93
|
*/
|
|
92
|
-
_resolve(path: string, pathFromRoot: string, options?: O): import("./pointer.js").default<S, O> | null;
|
|
94
|
+
_resolve(path: string, pathFromRoot: string, options?: O, visitedRefPaths?: Set<string>, resolveFinalReference?: boolean): import("./pointer.js").default<S, O> | null;
|
|
93
95
|
/**
|
|
94
96
|
* A map of paths/urls to {@link $Ref} objects
|
|
95
97
|
*
|
package/dist/lib/refs.js
CHANGED
|
@@ -133,16 +133,18 @@ export default class $Refs {
|
|
|
133
133
|
* @param path - The path being resolved, optionally with a JSON pointer in the hash
|
|
134
134
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
135
135
|
* @param [options]
|
|
136
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
137
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
136
138
|
* @returns
|
|
137
139
|
* @protected
|
|
138
140
|
*/
|
|
139
|
-
_resolve(path, pathFromRoot, options) {
|
|
141
|
+
_resolve(path, pathFromRoot, options, visitedRefPaths, resolveFinalReference = true) {
|
|
140
142
|
const absPath = url.resolve(this._root$Ref.path, path);
|
|
141
143
|
const $ref = this._getRef(absPath);
|
|
142
144
|
if (!$ref) {
|
|
143
145
|
throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
|
|
144
146
|
}
|
|
145
|
-
return $ref.resolve(absPath, options, path, pathFromRoot);
|
|
147
|
+
return $ref.resolve(absPath, options, path, pathFromRoot, visitedRefPaths, resolveFinalReference);
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* A map of paths/urls to {@link $Ref} objects
|
package/lib/bundle.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface InventoryEntry {
|
|
|
20
20
|
circular: any;
|
|
21
21
|
extended: any;
|
|
22
22
|
external: any;
|
|
23
|
+
nestedResource: boolean;
|
|
23
24
|
indirections: any;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
@@ -194,6 +195,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
194
195
|
const file = url.stripHash(pointer.path);
|
|
195
196
|
const hash = url.getHash(pointer.path);
|
|
196
197
|
const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
|
|
198
|
+
const nestedResource = Boolean($refs._aliases[file]) && pointer.$ref.value !== $refs._root$Ref.value;
|
|
197
199
|
const extended = $Ref.isExtended$Ref($ref);
|
|
198
200
|
indirections += pointer.indirections;
|
|
199
201
|
|
|
@@ -219,6 +221,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
219
221
|
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
|
|
220
222
|
extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
|
|
221
223
|
external, // Does this $ref pointer point to a file other than the main JSON Schema file?
|
|
224
|
+
nestedResource, // Does this $ref resolve to an embedded schema resource with its own $id?
|
|
222
225
|
indirections, // The number of indirect references that were traversed to resolve the value
|
|
223
226
|
});
|
|
224
227
|
|
|
@@ -316,8 +319,10 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
316
319
|
if (!entry.external) {
|
|
317
320
|
// This $ref already resolves to the main JSON Schema file.
|
|
318
321
|
// When optimizeInternalRefs is false, preserve the original internal ref path
|
|
319
|
-
// instead of rewriting it to the fully resolved hash.
|
|
320
|
-
|
|
322
|
+
// instead of rewriting it to the fully resolved hash. References to nested
|
|
323
|
+
// resources must also retain their resource URI so that "#" does not point
|
|
324
|
+
// at the document root instead.
|
|
325
|
+
if (bundleOpts.optimizeInternalRefs !== false && !entry.nestedResource) {
|
|
321
326
|
entry.$ref.$ref = entry.hash;
|
|
322
327
|
}
|
|
323
328
|
} else if (entry.file === file && entry.hash === hash) {
|
package/lib/dereference.ts
CHANGED
|
@@ -318,7 +318,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
318
318
|
|
|
319
319
|
// Check for circular references
|
|
320
320
|
const directCircular = pointer.circular;
|
|
321
|
-
let circular = directCircular || parents.has(pointer.value);
|
|
321
|
+
let circular = directCircular || pointer.chainCircular || parents.has(pointer.value);
|
|
322
322
|
if (circular) {
|
|
323
323
|
foundCircularReference(path, $refs, options);
|
|
324
324
|
}
|
|
@@ -352,7 +352,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
352
352
|
dereferencedValue = $ref;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
if (directCircular) {
|
|
355
|
+
if (directCircular && dereferencedValue !== $ref) {
|
|
356
356
|
// The pointer is a DIRECT circular reference (i.e. it references itself).
|
|
357
357
|
// So replace the $ref path with the absolute path from the JSON Schema root
|
|
358
358
|
dereferencedValue.$ref = pathFromRoot;
|
package/lib/options.ts
CHANGED
|
@@ -103,6 +103,7 @@ export interface DereferenceOptions {
|
|
|
103
103
|
* The maximum recursion depth for dereferencing nested schemas.
|
|
104
104
|
* If the schema nesting exceeds this depth, a RangeError will be thrown
|
|
105
105
|
* with a descriptive message instead of crashing with a stack overflow.
|
|
106
|
+
* This limit also applies to `$ref` resolution chains.
|
|
106
107
|
*
|
|
107
108
|
* Default: 500
|
|
108
109
|
*/
|
package/lib/pointer.ts
CHANGED
|
@@ -55,6 +55,10 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
55
55
|
* Indicates whether the pointer references itself.
|
|
56
56
|
*/
|
|
57
57
|
circular: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Indicates whether the pointer encountered a cycle elsewhere in its reference chain.
|
|
60
|
+
*/
|
|
61
|
+
chainCircular: boolean;
|
|
58
62
|
/**
|
|
59
63
|
* The number of indirect references that were traversed to resolve the value.
|
|
60
64
|
* Resolving a single pointer may require resolving multiple $Refs.
|
|
@@ -74,6 +78,8 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
74
78
|
|
|
75
79
|
this.circular = false;
|
|
76
80
|
|
|
81
|
+
this.chainCircular = false;
|
|
82
|
+
|
|
77
83
|
this.indirections = 0;
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -90,7 +96,13 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
90
96
|
* the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
|
|
91
97
|
* of the resolved value.
|
|
92
98
|
*/
|
|
93
|
-
resolve(
|
|
99
|
+
resolve(
|
|
100
|
+
obj: S,
|
|
101
|
+
options?: O,
|
|
102
|
+
pathFromRoot?: string,
|
|
103
|
+
visitedRefPaths = new Set<string>(),
|
|
104
|
+
resolveFinalReference = true,
|
|
105
|
+
) {
|
|
94
106
|
const tokens = Pointer.parse(this.path, this.originalPath);
|
|
95
107
|
const found: string[] = [];
|
|
96
108
|
|
|
@@ -108,15 +120,17 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
108
120
|
// This prevents false circular detection when e.g. a root schema has both
|
|
109
121
|
// $ref: "#/$defs/Foo" and $defs: { Foo: {...} } as siblings.
|
|
110
122
|
const wasCircular = this.circular;
|
|
123
|
+
const wasChainCircular = this.chainCircular;
|
|
111
124
|
const isExtendedRef = $Ref.isExtended$Ref(this.value);
|
|
112
|
-
if (resolveIf$Ref(this, options, pathFromRoot)) {
|
|
125
|
+
if (resolveIf$Ref(this, options, pathFromRoot, visitedRefPaths)) {
|
|
113
126
|
// The $ref path has changed, so append the remaining tokens to the path
|
|
114
127
|
this.path = Pointer.join(this.path, tokens.slice(i));
|
|
115
|
-
} else if (
|
|
128
|
+
} else if (isExtendedRef) {
|
|
116
129
|
// resolveIf$Ref set circular=true on an extended $ref during token walking.
|
|
117
130
|
// Since we still have tokens to process, the object should be walked by its
|
|
118
131
|
// properties, not treated as a circular self-reference.
|
|
119
|
-
this.circular =
|
|
132
|
+
this.circular = wasCircular;
|
|
133
|
+
this.chainCircular = wasChainCircular;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
const token = tokens[i];
|
|
@@ -134,6 +148,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
150
|
if (didFindSubstringSlashMatch) {
|
|
151
|
+
this.chainCircular = wasChainCircular;
|
|
137
152
|
continue;
|
|
138
153
|
}
|
|
139
154
|
|
|
@@ -144,6 +159,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
144
159
|
// We use a `null` symbol for internal tracking to differentiate between a general `null`
|
|
145
160
|
// value and our expected `null` value.
|
|
146
161
|
this.value = nullSymbol;
|
|
162
|
+
this.chainCircular = wasChainCircular;
|
|
147
163
|
continue;
|
|
148
164
|
}
|
|
149
165
|
|
|
@@ -160,6 +176,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
160
176
|
this.value = this.value[token];
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
this.chainCircular = wasChainCircular;
|
|
163
180
|
found.push(token);
|
|
164
181
|
if (this.$ref.dynamicIdScope) {
|
|
165
182
|
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
@@ -168,8 +185,20 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
168
185
|
|
|
169
186
|
// Resolve the final value
|
|
170
187
|
const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
|
|
171
|
-
if (
|
|
172
|
-
|
|
188
|
+
if (resolveFinalReference) {
|
|
189
|
+
const finalRefPath = this.value?.$ref ? url.resolve(finalResolutionBase, this.value.$ref) : undefined;
|
|
190
|
+
const canonicalPathFromRoot =
|
|
191
|
+
typeof pathFromRoot === "string" ? url.resolve(this.$ref.$refs._root$Ref.path!, pathFromRoot) : pathFromRoot;
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
$Ref.isAllowed$Ref(this.value, options) &&
|
|
195
|
+
finalRefPath === canonicalPathFromRoot &&
|
|
196
|
+
finalRefPath !== this.path
|
|
197
|
+
) {
|
|
198
|
+
this.chainCircular = true;
|
|
199
|
+
} else if (!this.value || finalRefPath) {
|
|
200
|
+
resolveIf$Ref(this, options, pathFromRoot, visitedRefPaths);
|
|
201
|
+
}
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
return this;
|
|
@@ -305,32 +334,74 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
305
334
|
* @returns - Returns `true` if the resolution path changed
|
|
306
335
|
*/
|
|
307
336
|
function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
308
|
-
pointer: Pointer,
|
|
337
|
+
pointer: Pointer<S, O>,
|
|
309
338
|
options: O | undefined,
|
|
310
339
|
pathFromRoot?: string,
|
|
340
|
+
visitedRefPaths = new Set<string>(),
|
|
311
341
|
) {
|
|
312
|
-
|
|
342
|
+
let pathChanged = false;
|
|
343
|
+
let currentPathFromRoot = pathFromRoot;
|
|
344
|
+
const addedPaths: string[] = [];
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
// Pure reference chains can be followed iteratively. Extended refs still use
|
|
348
|
+
// the existing one-hop behavior because their values must be merged on return.
|
|
349
|
+
while ($Ref.isAllowed$Ref(pointer.value, options)) {
|
|
350
|
+
const extended = $Ref.isExtended$Ref(pointer.value);
|
|
351
|
+
const sourceValue = pointer.value;
|
|
352
|
+
const parentPath = pointer.path;
|
|
353
|
+
const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
|
|
354
|
+
const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
|
|
355
|
+
|
|
356
|
+
if ($refPath === pointer.path && !isRootPath(currentPathFromRoot)) {
|
|
357
|
+
// The value is a reference to itself, so there's nothing to do.
|
|
358
|
+
pointer.circular = true;
|
|
359
|
+
pointer.chainCircular = true;
|
|
360
|
+
return pathChanged;
|
|
361
|
+
}
|
|
313
362
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
363
|
+
const canonicalPathFromRoot =
|
|
364
|
+
typeof currentPathFromRoot === "string"
|
|
365
|
+
? url.resolve(pointer.$ref.$refs._root$Ref.path!, currentPathFromRoot)
|
|
366
|
+
: currentPathFromRoot;
|
|
367
|
+
if ($refPath === canonicalPathFromRoot && $refPath !== pointer.path) {
|
|
368
|
+
pointer.chainCircular = true;
|
|
369
|
+
return pathChanged;
|
|
370
|
+
}
|
|
317
371
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
372
|
+
if (visitedRefPaths.has($refPath)) {
|
|
373
|
+
pointer.chainCircular = true;
|
|
374
|
+
return pathChanged;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const maxDepth = options?.dereference?.maxDepth ?? 500;
|
|
378
|
+
if (visitedRefPaths.size >= maxDepth) {
|
|
379
|
+
throw new RangeError(
|
|
380
|
+
`Maximum dereference depth (${maxDepth}) exceeded at ${pointer.path}. ` +
|
|
381
|
+
`This likely indicates an extremely deep or recursive schema. ` +
|
|
382
|
+
`You can increase this limit with the dereference.maxDepth option.`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
visitedRefPaths.add($refPath);
|
|
387
|
+
addedPaths.push($refPath);
|
|
388
|
+
|
|
389
|
+
const resolved = pointer.$ref.$refs._resolve($refPath, parentPath, options, visitedRefPaths, extended);
|
|
323
390
|
if (resolved === null) {
|
|
324
|
-
return
|
|
391
|
+
return pathChanged;
|
|
325
392
|
}
|
|
326
393
|
|
|
327
394
|
pointer.indirections += resolved.indirections + 1;
|
|
395
|
+
pointer.chainCircular ||= resolved.circular || resolved.chainCircular;
|
|
328
396
|
|
|
329
|
-
if (
|
|
397
|
+
if (extended) {
|
|
330
398
|
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
|
|
331
399
|
// So the resolved path does NOT change. Just the value does.
|
|
332
|
-
|
|
333
|
-
|
|
400
|
+
if (pointer.chainCircular) {
|
|
401
|
+
return pathChanged;
|
|
402
|
+
}
|
|
403
|
+
pointer.value = $Ref.dereference(sourceValue, resolved.value, options);
|
|
404
|
+
return pathChanged;
|
|
334
405
|
} else {
|
|
335
406
|
// Resolve the reference
|
|
336
407
|
pointer.$ref = resolved.$ref;
|
|
@@ -339,13 +410,18 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
339
410
|
// `pointer.$ref.path` is already the canonical location of the resolved resource.
|
|
340
411
|
// Re-applying the resource's own `$id` here would duplicate nested path segments
|
|
341
412
|
// such as `nested/nested/foo.json`.
|
|
342
|
-
pointer.scopeBase =
|
|
413
|
+
pointer.scopeBase = resolved.scopeBase;
|
|
414
|
+
currentPathFromRoot = parentPath;
|
|
415
|
+
pathChanged = true;
|
|
343
416
|
}
|
|
417
|
+
}
|
|
344
418
|
|
|
345
|
-
|
|
419
|
+
return pathChanged;
|
|
420
|
+
} finally {
|
|
421
|
+
for (const path of addedPaths) {
|
|
422
|
+
visitedRefPaths.delete(path);
|
|
346
423
|
}
|
|
347
424
|
}
|
|
348
|
-
return undefined;
|
|
349
425
|
}
|
|
350
426
|
export default Pointer;
|
|
351
427
|
|
package/lib/ref.ts
CHANGED
|
@@ -120,12 +120,21 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
|
|
|
120
120
|
* @param options
|
|
121
121
|
* @param friendlyPath - The original user-specified path (used for error messages)
|
|
122
122
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
123
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
124
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
123
125
|
* @returns
|
|
124
126
|
*/
|
|
125
|
-
resolve(
|
|
127
|
+
resolve(
|
|
128
|
+
path: string,
|
|
129
|
+
options?: O,
|
|
130
|
+
friendlyPath?: string,
|
|
131
|
+
pathFromRoot?: string,
|
|
132
|
+
visitedRefPaths?: Set<string>,
|
|
133
|
+
resolveFinalReference = true,
|
|
134
|
+
) {
|
|
126
135
|
const pointer = new Pointer<S, O>(this, path, friendlyPath);
|
|
127
136
|
try {
|
|
128
|
-
const resolved = pointer.resolve(this.value, options, pathFromRoot);
|
|
137
|
+
const resolved = pointer.resolve(this.value, options, pathFromRoot, visitedRefPaths, resolveFinalReference);
|
|
129
138
|
if (resolved.value === nullSymbol) {
|
|
130
139
|
resolved.value = null;
|
|
131
140
|
}
|
package/lib/refs.ts
CHANGED
|
@@ -155,10 +155,18 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
|
|
|
155
155
|
* @param path - The path being resolved, optionally with a JSON pointer in the hash
|
|
156
156
|
* @param pathFromRoot - The path of `obj` from the schema root
|
|
157
157
|
* @param [options]
|
|
158
|
+
* @param visitedRefPaths - the active paths in the current reference chain
|
|
159
|
+
* @param resolveFinalReference - whether to follow a `$ref` at the resolved value
|
|
158
160
|
* @returns
|
|
159
161
|
* @protected
|
|
160
162
|
*/
|
|
161
|
-
_resolve(
|
|
163
|
+
_resolve(
|
|
164
|
+
path: string,
|
|
165
|
+
pathFromRoot: string,
|
|
166
|
+
options?: O,
|
|
167
|
+
visitedRefPaths?: Set<string>,
|
|
168
|
+
resolveFinalReference = true,
|
|
169
|
+
) {
|
|
162
170
|
const absPath = url.resolve(this._root$Ref.path!, path);
|
|
163
171
|
const $ref = this._getRef(absPath);
|
|
164
172
|
|
|
@@ -166,7 +174,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
|
|
|
166
174
|
throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
return $ref.resolve(absPath, options, path, pathFromRoot);
|
|
177
|
+
return $ref.resolve(absPath, options, path, pathFromRoot, visitedRefPaths, resolveFinalReference);
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
/**
|