@apidevtools/json-schema-ref-parser 15.3.0 → 15.3.1
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.js +53 -7
- package/dist/lib/dereference.js +7 -1
- package/dist/lib/options.d.ts +22 -0
- package/lib/bundle.ts +54 -6
- package/lib/dereference.ts +8 -1
- package/lib/options.ts +24 -0
- package/package.json +1 -1
package/dist/lib/bundle.js
CHANGED
|
@@ -14,10 +14,17 @@ function bundle(parser, options) {
|
|
|
14
14
|
// Build an inventory of all $ref pointers in the JSON Schema
|
|
15
15
|
const inventory = [];
|
|
16
16
|
crawl(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
|
|
17
|
+
// Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
|
|
18
|
+
const rootId = parser.schema && typeof parser.schema === "object" && "$id" in parser.schema
|
|
19
|
+
? parser.schema.$id
|
|
20
|
+
: undefined;
|
|
17
21
|
// Remap all $ref pointers
|
|
18
|
-
remap(inventory, options);
|
|
22
|
+
remap(inventory, options, rootId);
|
|
19
23
|
// Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
|
|
20
|
-
|
|
24
|
+
const bundleOptions = (options.bundle || {});
|
|
25
|
+
if (bundleOptions.optimizeInternalRefs !== false) {
|
|
26
|
+
fixRefsThroughRefs(inventory, parser.schema);
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Recursively crawls the given value, and inventories all JSON references.
|
|
@@ -157,7 +164,7 @@ function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, in
|
|
|
157
164
|
*
|
|
158
165
|
* @param inventory
|
|
159
166
|
*/
|
|
160
|
-
function remap(inventory, options) {
|
|
167
|
+
function remap(inventory, options, rootId) {
|
|
161
168
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
162
169
|
inventory.sort((a, b) => {
|
|
163
170
|
if (a.file !== b.file) {
|
|
@@ -202,17 +209,35 @@ function remap(inventory, options) {
|
|
|
202
209
|
let file, hash, pathFromRoot;
|
|
203
210
|
for (const entry of inventory) {
|
|
204
211
|
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
|
212
|
+
const bundleOpts = (options.bundle || {});
|
|
205
213
|
if (!entry.external) {
|
|
206
|
-
// This $ref already resolves to the main JSON Schema file
|
|
207
|
-
|
|
214
|
+
// This $ref already resolves to the main JSON Schema file.
|
|
215
|
+
// When optimizeInternalRefs is false, preserve the original internal ref path
|
|
216
|
+
// instead of rewriting it to the fully resolved hash.
|
|
217
|
+
if (bundleOpts.optimizeInternalRefs !== false) {
|
|
218
|
+
entry.$ref.$ref = entry.hash;
|
|
219
|
+
}
|
|
208
220
|
}
|
|
209
221
|
else if (entry.file === file && entry.hash === hash) {
|
|
210
222
|
// This $ref points to the same value as the previous $ref, so remap it to the same path
|
|
211
|
-
entry
|
|
223
|
+
if (rootId && isInsideIdScope(inventory, entry)) {
|
|
224
|
+
// This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
|
|
225
|
+
// would be resolved relative to that $id, not the document root. Qualify with the root $id.
|
|
226
|
+
entry.$ref.$ref = rootId + pathFromRoot;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
entry.$ref.$ref = pathFromRoot;
|
|
230
|
+
}
|
|
212
231
|
}
|
|
213
232
|
else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
|
|
214
233
|
// This $ref points to a sub-value of the previous $ref, so remap it beneath that path
|
|
215
|
-
|
|
234
|
+
const subPath = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
|
|
235
|
+
if (rootId && isInsideIdScope(inventory, entry)) {
|
|
236
|
+
entry.$ref.$ref = rootId + subPath;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
entry.$ref.$ref = subPath;
|
|
240
|
+
}
|
|
216
241
|
}
|
|
217
242
|
else {
|
|
218
243
|
// We've moved to a new file or new hash
|
|
@@ -339,4 +364,25 @@ function walkPath(schema, path) {
|
|
|
339
364
|
}
|
|
340
365
|
return current;
|
|
341
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
|
|
369
|
+
* If so, root-relative JSON Pointer $refs placed at this location would be resolved against
|
|
370
|
+
* the $id base URI rather than the document root, making them invalid.
|
|
371
|
+
*/
|
|
372
|
+
function isInsideIdScope(inventory, entry) {
|
|
373
|
+
for (const other of inventory) {
|
|
374
|
+
// Skip root-level entries
|
|
375
|
+
if (other.pathFromRoot === "#" || other.pathFromRoot === "#/") {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
// Check if the other entry is an ancestor of the current entry
|
|
379
|
+
if (entry.pathFromRoot.startsWith(other.pathFromRoot + "/")) {
|
|
380
|
+
// Check if the ancestor's resolved value has a $id
|
|
381
|
+
if (other.value && typeof other.value === "object" && "$id" in other.value) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
342
388
|
export default bundle;
|
package/dist/lib/dereference.js
CHANGED
|
@@ -83,7 +83,13 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// Clone the dereferenced value if cloneReferences is enabled and this is not a
|
|
87
|
+
// circular reference. This prevents mutations to one location from affecting others.
|
|
88
|
+
let assignedValue = dereferenced.value;
|
|
89
|
+
if (derefOptions?.cloneReferences && !circular && assignedValue && typeof assignedValue === "object") {
|
|
90
|
+
assignedValue = structuredClone(assignedValue);
|
|
91
|
+
}
|
|
92
|
+
obj[key] = assignedValue;
|
|
87
93
|
// If we have data to preserve and our dereferenced object is still an object then
|
|
88
94
|
// we need copy back our preserved data into our dereferenced schema.
|
|
89
95
|
if (derefOptions?.preservedProperties) {
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface BundleOptions {
|
|
|
18
18
|
* @argument {string} parentPropName - The prop name of the parent object whose value was processed
|
|
19
19
|
*/
|
|
20
20
|
onBundle?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to optimize internal `$ref` paths by following intermediate `$ref` chains and
|
|
23
|
+
* rewriting them to point directly to the final target. When `false`, intermediate `$ref`
|
|
24
|
+
* indirections are preserved as-is.
|
|
25
|
+
*
|
|
26
|
+
* Default: `true`
|
|
27
|
+
*/
|
|
28
|
+
optimizeInternalRefs?: boolean;
|
|
21
29
|
}
|
|
22
30
|
export interface DereferenceOptions {
|
|
23
31
|
/**
|
|
@@ -78,6 +86,20 @@ export interface DereferenceOptions {
|
|
|
78
86
|
* Default: 500
|
|
79
87
|
*/
|
|
80
88
|
maxDepth?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Whether to create independent clones of each `$ref` target value instead of
|
|
91
|
+
* reusing the same JS object reference. When `false` (the default), multiple
|
|
92
|
+
* `$ref` pointers that resolve to the same value will all share the same object
|
|
93
|
+
* in memory, so modifying one will affect all others. When `true`, each `$ref`
|
|
94
|
+
* replacement gets its own deep copy, preventing unintended side effects from
|
|
95
|
+
* post-dereference mutations.
|
|
96
|
+
*
|
|
97
|
+
* Note: circular references are never cloned — they always maintain reference
|
|
98
|
+
* equality to correctly represent the circular structure.
|
|
99
|
+
*
|
|
100
|
+
* Default: `false`
|
|
101
|
+
*/
|
|
102
|
+
cloneReferences?: boolean;
|
|
81
103
|
}
|
|
82
104
|
/**
|
|
83
105
|
* Options that determine how JSON schemas are parsed, resolved, and dereferenced.
|
package/lib/bundle.ts
CHANGED
|
@@ -39,11 +39,20 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
|
|
|
39
39
|
const inventory: InventoryEntry[] = [];
|
|
40
40
|
crawl<S, O>(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
|
|
41
41
|
|
|
42
|
+
// Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
|
|
43
|
+
const rootId =
|
|
44
|
+
parser.schema && typeof parser.schema === "object" && "$id" in (parser.schema as any)
|
|
45
|
+
? (parser.schema as any).$id
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
42
48
|
// Remap all $ref pointers
|
|
43
|
-
remap<S, O>(inventory, options);
|
|
49
|
+
remap<S, O>(inventory, options, rootId);
|
|
44
50
|
|
|
45
51
|
// Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
|
|
46
|
-
|
|
52
|
+
const bundleOptions = (options.bundle || {}) as BundleOptions;
|
|
53
|
+
if (bundleOptions.optimizeInternalRefs !== false) {
|
|
54
|
+
fixRefsThroughRefs(inventory, parser.schema as any);
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
@@ -209,6 +218,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
209
218
|
function remap<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
210
219
|
inventory: InventoryEntry[],
|
|
211
220
|
options: O,
|
|
221
|
+
rootId?: string,
|
|
212
222
|
) {
|
|
213
223
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
214
224
|
inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
|
|
@@ -256,15 +266,31 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
256
266
|
for (const entry of inventory) {
|
|
257
267
|
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
|
258
268
|
|
|
269
|
+
const bundleOpts = (options.bundle || {}) as BundleOptions;
|
|
259
270
|
if (!entry.external) {
|
|
260
|
-
// This $ref already resolves to the main JSON Schema file
|
|
261
|
-
|
|
271
|
+
// This $ref already resolves to the main JSON Schema file.
|
|
272
|
+
// When optimizeInternalRefs is false, preserve the original internal ref path
|
|
273
|
+
// instead of rewriting it to the fully resolved hash.
|
|
274
|
+
if (bundleOpts.optimizeInternalRefs !== false) {
|
|
275
|
+
entry.$ref.$ref = entry.hash;
|
|
276
|
+
}
|
|
262
277
|
} else if (entry.file === file && entry.hash === hash) {
|
|
263
278
|
// This $ref points to the same value as the previous $ref, so remap it to the same path
|
|
264
|
-
entry
|
|
279
|
+
if (rootId && isInsideIdScope(inventory, entry)) {
|
|
280
|
+
// This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
|
|
281
|
+
// would be resolved relative to that $id, not the document root. Qualify with the root $id.
|
|
282
|
+
entry.$ref.$ref = rootId + pathFromRoot;
|
|
283
|
+
} else {
|
|
284
|
+
entry.$ref.$ref = pathFromRoot;
|
|
285
|
+
}
|
|
265
286
|
} else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
|
|
266
287
|
// This $ref points to a sub-value of the previous $ref, so remap it beneath that path
|
|
267
|
-
|
|
288
|
+
const subPath = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
|
|
289
|
+
if (rootId && isInsideIdScope(inventory, entry)) {
|
|
290
|
+
entry.$ref.$ref = rootId + subPath;
|
|
291
|
+
} else {
|
|
292
|
+
entry.$ref.$ref = subPath;
|
|
293
|
+
}
|
|
268
294
|
} else {
|
|
269
295
|
// We've moved to a new file or new hash
|
|
270
296
|
file = entry.file;
|
|
@@ -409,4 +435,26 @@ function walkPath(schema: any, path: string): any {
|
|
|
409
435
|
return current;
|
|
410
436
|
}
|
|
411
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
|
|
440
|
+
* If so, root-relative JSON Pointer $refs placed at this location would be resolved against
|
|
441
|
+
* the $id base URI rather than the document root, making them invalid.
|
|
442
|
+
*/
|
|
443
|
+
function isInsideIdScope(inventory: InventoryEntry[], entry: InventoryEntry): boolean {
|
|
444
|
+
for (const other of inventory) {
|
|
445
|
+
// Skip root-level entries
|
|
446
|
+
if (other.pathFromRoot === "#" || other.pathFromRoot === "#/") {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// Check if the other entry is an ancestor of the current entry
|
|
450
|
+
if (entry.pathFromRoot.startsWith(other.pathFromRoot + "/")) {
|
|
451
|
+
// Check if the ancestor's resolved value has a $id
|
|
452
|
+
if (other.value && typeof other.value === "object" && "$id" in other.value) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
412
460
|
export default bundle;
|
package/lib/dereference.ts
CHANGED
|
@@ -146,7 +146,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// Clone the dereferenced value if cloneReferences is enabled and this is not a
|
|
150
|
+
// circular reference. This prevents mutations to one location from affecting others.
|
|
151
|
+
let assignedValue = dereferenced.value;
|
|
152
|
+
if (derefOptions?.cloneReferences && !circular && assignedValue && typeof assignedValue === "object") {
|
|
153
|
+
assignedValue = structuredClone(assignedValue);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
obj[key] = assignedValue;
|
|
150
157
|
|
|
151
158
|
// If we have data to preserve and our dereferenced object is still an object then
|
|
152
159
|
// we need copy back our preserved data into our dereferenced schema.
|
package/lib/options.ts
CHANGED
|
@@ -30,6 +30,15 @@ export interface BundleOptions {
|
|
|
30
30
|
* @argument {string} parentPropName - The prop name of the parent object whose value was processed
|
|
31
31
|
*/
|
|
32
32
|
onBundle?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether to optimize internal `$ref` paths by following intermediate `$ref` chains and
|
|
36
|
+
* rewriting them to point directly to the final target. When `false`, intermediate `$ref`
|
|
37
|
+
* indirections are preserved as-is.
|
|
38
|
+
*
|
|
39
|
+
* Default: `true`
|
|
40
|
+
*/
|
|
41
|
+
optimizeInternalRefs?: boolean;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export interface DereferenceOptions {
|
|
@@ -98,6 +107,21 @@ export interface DereferenceOptions {
|
|
|
98
107
|
* Default: 500
|
|
99
108
|
*/
|
|
100
109
|
maxDepth?: number;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether to create independent clones of each `$ref` target value instead of
|
|
113
|
+
* reusing the same JS object reference. When `false` (the default), multiple
|
|
114
|
+
* `$ref` pointers that resolve to the same value will all share the same object
|
|
115
|
+
* in memory, so modifying one will affect all others. When `true`, each `$ref`
|
|
116
|
+
* replacement gets its own deep copy, preventing unintended side effects from
|
|
117
|
+
* post-dereference mutations.
|
|
118
|
+
*
|
|
119
|
+
* Note: circular references are never cloned — they always maintain reference
|
|
120
|
+
* equality to correctly represent the circular structure.
|
|
121
|
+
*
|
|
122
|
+
* Default: `false`
|
|
123
|
+
*/
|
|
124
|
+
cloneReferences?: boolean;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
/**
|