@apidevtools/json-schema-ref-parser 15.2.2 → 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 +130 -6
- package/dist/lib/dereference.js +22 -9
- package/dist/lib/options.d.ts +30 -0
- package/dist/lib/parse.js +2 -2
- package/dist/lib/pointer.js +14 -0
- package/lib/bundle.ts +146 -5
- package/lib/dereference.ts +25 -2
- package/lib/options.ts +33 -0
- package/lib/parse.ts +2 -2
- package/lib/pointer.ts +13 -0
- package/package.json +11 -11
package/dist/lib/bundle.js
CHANGED
|
@@ -14,8 +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);
|
|
23
|
+
// Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
|
|
24
|
+
const bundleOptions = (options.bundle || {});
|
|
25
|
+
if (bundleOptions.optimizeInternalRefs !== false) {
|
|
26
|
+
fixRefsThroughRefs(inventory, parser.schema);
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
30
|
* Recursively crawls the given value, and inventories all JSON references.
|
|
@@ -155,7 +164,7 @@ function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, in
|
|
|
155
164
|
*
|
|
156
165
|
* @param inventory
|
|
157
166
|
*/
|
|
158
|
-
function remap(inventory, options) {
|
|
167
|
+
function remap(inventory, options, rootId) {
|
|
159
168
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
160
169
|
inventory.sort((a, b) => {
|
|
161
170
|
if (a.file !== b.file) {
|
|
@@ -200,17 +209,35 @@ function remap(inventory, options) {
|
|
|
200
209
|
let file, hash, pathFromRoot;
|
|
201
210
|
for (const entry of inventory) {
|
|
202
211
|
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
|
212
|
+
const bundleOpts = (options.bundle || {});
|
|
203
213
|
if (!entry.external) {
|
|
204
|
-
// This $ref already resolves to the main JSON Schema file
|
|
205
|
-
|
|
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
|
+
}
|
|
206
220
|
}
|
|
207
221
|
else if (entry.file === file && entry.hash === hash) {
|
|
208
222
|
// This $ref points to the same value as the previous $ref, so remap it to the same path
|
|
209
|
-
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
|
+
}
|
|
210
231
|
}
|
|
211
232
|
else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
|
|
212
233
|
// This $ref points to a sub-value of the previous $ref, so remap it beneath that path
|
|
213
|
-
|
|
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
|
+
}
|
|
214
241
|
}
|
|
215
242
|
else {
|
|
216
243
|
// We've moved to a new file or new hash
|
|
@@ -261,4 +288,101 @@ function removeFromInventory(inventory, entry) {
|
|
|
261
288
|
const index = inventory.indexOf(entry);
|
|
262
289
|
inventory.splice(index, 1);
|
|
263
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* After remapping, some $ref paths may traverse through other $ref nodes.
|
|
293
|
+
* JSON pointer resolution does not follow $ref indirection, so these paths are invalid.
|
|
294
|
+
* This function detects and fixes such paths by following any intermediate $refs
|
|
295
|
+
* to compute a valid direct path.
|
|
296
|
+
*/
|
|
297
|
+
function fixRefsThroughRefs(inventory, schema) {
|
|
298
|
+
for (const entry of inventory) {
|
|
299
|
+
if (!entry.$ref || typeof entry.$ref !== "object" || !("$ref" in entry.$ref)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const refValue = entry.$ref.$ref;
|
|
303
|
+
if (typeof refValue !== "string" || !refValue.startsWith("#/")) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const fixedPath = resolvePathThroughRefs(schema, refValue);
|
|
307
|
+
if (fixedPath !== refValue) {
|
|
308
|
+
entry.$ref.$ref = fixedPath;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Walks a JSON pointer path through the schema. If any intermediate value
|
|
314
|
+
* is a $ref, follows it and adjusts the path accordingly.
|
|
315
|
+
* Returns the corrected path that doesn't traverse through any $ref.
|
|
316
|
+
*/
|
|
317
|
+
function resolvePathThroughRefs(schema, refPath) {
|
|
318
|
+
if (!refPath.startsWith("#/")) {
|
|
319
|
+
return refPath;
|
|
320
|
+
}
|
|
321
|
+
const segments = refPath.slice(2).split("/");
|
|
322
|
+
let current = schema;
|
|
323
|
+
const resolvedSegments = [];
|
|
324
|
+
for (const seg of segments) {
|
|
325
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
326
|
+
// Can't walk further, return original path
|
|
327
|
+
return refPath;
|
|
328
|
+
}
|
|
329
|
+
// If the current value is a $ref, follow it
|
|
330
|
+
if ("$ref" in current && typeof current.$ref === "string" && current.$ref.startsWith("#/")) {
|
|
331
|
+
// Follow the $ref and restart the path from its target
|
|
332
|
+
const targetSegments = current.$ref.slice(2).split("/");
|
|
333
|
+
resolvedSegments.length = 0;
|
|
334
|
+
resolvedSegments.push(...targetSegments);
|
|
335
|
+
current = walkPath(schema, current.$ref);
|
|
336
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
337
|
+
return refPath;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
341
|
+
const idx = Array.isArray(current) ? parseInt(decoded) : decoded;
|
|
342
|
+
current = current[idx];
|
|
343
|
+
resolvedSegments.push(seg);
|
|
344
|
+
}
|
|
345
|
+
const result = "#/" + resolvedSegments.join("/");
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Walks a JSON pointer path through a schema object, returning the value at that path.
|
|
350
|
+
*/
|
|
351
|
+
function walkPath(schema, path) {
|
|
352
|
+
if (!path.startsWith("#/")) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const segments = path.slice(2).split("/");
|
|
356
|
+
let current = schema;
|
|
357
|
+
for (const seg of segments) {
|
|
358
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
362
|
+
const idx = Array.isArray(current) ? parseInt(decoded) : decoded;
|
|
363
|
+
current = current[idx];
|
|
364
|
+
}
|
|
365
|
+
return current;
|
|
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
|
+
}
|
|
264
388
|
export default bundle;
|
package/dist/lib/dereference.js
CHANGED
|
@@ -13,7 +13,7 @@ export default dereference;
|
|
|
13
13
|
function dereference(parser, options) {
|
|
14
14
|
const start = Date.now();
|
|
15
15
|
// 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);
|
|
16
|
+
const dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, "#", new Set(), new Set(), new Map(), parser.$refs, options, start, 0);
|
|
17
17
|
parser.$refs.circular = dereferenced.circular;
|
|
18
18
|
parser.schema = dereferenced.value;
|
|
19
19
|
}
|
|
@@ -29,9 +29,10 @@ function dereference(parser, options) {
|
|
|
29
29
|
* @param $refs
|
|
30
30
|
* @param options
|
|
31
31
|
* @param startTime - The time when the dereferencing started
|
|
32
|
+
* @param depth - The current recursion depth
|
|
32
33
|
* @returns
|
|
33
34
|
*/
|
|
34
|
-
function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime) {
|
|
35
|
+
function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
|
|
35
36
|
let dereferenced;
|
|
36
37
|
const result = {
|
|
37
38
|
value: obj,
|
|
@@ -39,13 +40,19 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
39
40
|
};
|
|
40
41
|
checkDereferenceTimeout(startTime, options);
|
|
41
42
|
const derefOptions = (options.dereference || {});
|
|
43
|
+
const maxDepth = derefOptions.maxDepth ?? 500;
|
|
44
|
+
if (depth > maxDepth) {
|
|
45
|
+
throw new RangeError(`Maximum dereference depth (${maxDepth}) exceeded at ${pathFromRoot}. ` +
|
|
46
|
+
`This likely indicates an extremely deep or recursive schema. ` +
|
|
47
|
+
`You can increase this limit with the dereference.maxDepth option.`);
|
|
48
|
+
}
|
|
42
49
|
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
|
|
43
50
|
if (derefOptions?.circular === "ignore" || !processedObjects.has(obj)) {
|
|
44
51
|
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
|
|
45
52
|
parents.add(obj);
|
|
46
53
|
processedObjects.add(obj);
|
|
47
54
|
if ($Ref.isAllowed$Ref(obj, options)) {
|
|
48
|
-
dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime);
|
|
55
|
+
dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
|
|
49
56
|
result.circular = dereferenced.circular;
|
|
50
57
|
result.value = dereferenced.value;
|
|
51
58
|
}
|
|
@@ -58,9 +65,9 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
58
65
|
continue;
|
|
59
66
|
}
|
|
60
67
|
const value = obj[key];
|
|
61
|
-
let circular
|
|
68
|
+
let circular;
|
|
62
69
|
if ($Ref.isAllowed$Ref(value, options)) {
|
|
63
|
-
dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime);
|
|
70
|
+
dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
|
|
64
71
|
circular = dereferenced.circular;
|
|
65
72
|
// Avoid pointless mutations; breaks frozen objects to no profit
|
|
66
73
|
if (obj[key] !== dereferenced.value) {
|
|
@@ -76,7 +83,13 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
76
83
|
});
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
|
-
|
|
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;
|
|
80
93
|
// If we have data to preserve and our dereferenced object is still an object then
|
|
81
94
|
// we need copy back our preserved data into our dereferenced schema.
|
|
82
95
|
if (derefOptions?.preservedProperties) {
|
|
@@ -91,7 +104,7 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
91
104
|
}
|
|
92
105
|
else {
|
|
93
106
|
if (!parents.has(value)) {
|
|
94
|
-
dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime);
|
|
107
|
+
dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
|
|
95
108
|
circular = dereferenced.circular;
|
|
96
109
|
// Avoid pointless mutations; breaks frozen objects to no profit
|
|
97
110
|
if (obj[key] !== dereferenced.value) {
|
|
@@ -124,7 +137,7 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
|
|
|
124
137
|
* @param options
|
|
125
138
|
* @returns
|
|
126
139
|
*/
|
|
127
|
-
function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime) {
|
|
140
|
+
function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth) {
|
|
128
141
|
const isExternalRef = $Ref.isExternal$Ref($ref);
|
|
129
142
|
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
|
|
130
143
|
const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
|
|
@@ -197,7 +210,7 @@ function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, de
|
|
|
197
210
|
// Crawl the dereferenced value (unless it's circular)
|
|
198
211
|
if (!circular) {
|
|
199
212
|
// Determine if the dereferenced value is circular
|
|
200
|
-
const dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime);
|
|
213
|
+
const dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
|
|
201
214
|
circular = dereferenced.circular;
|
|
202
215
|
dereferencedValue = dereferenced.value;
|
|
203
216
|
}
|
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
|
/**
|
|
@@ -70,6 +78,28 @@ export interface DereferenceOptions {
|
|
|
70
78
|
* Default: `true`
|
|
71
79
|
*/
|
|
72
80
|
mergeKeys?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The maximum recursion depth for dereferencing nested schemas.
|
|
83
|
+
* If the schema nesting exceeds this depth, a RangeError will be thrown
|
|
84
|
+
* with a descriptive message instead of crashing with a stack overflow.
|
|
85
|
+
*
|
|
86
|
+
* Default: 500
|
|
87
|
+
*/
|
|
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;
|
|
73
103
|
}
|
|
74
104
|
/**
|
|
75
105
|
* Options that determine how JSON schemas are parsed, resolved, and dereferenced.
|
package/dist/lib/parse.js
CHANGED
|
@@ -67,7 +67,7 @@ async function readFile(file, options, $refs) {
|
|
|
67
67
|
}
|
|
68
68
|
else if (!err || !("error" in err)) {
|
|
69
69
|
// Throw a generic, friendly error.
|
|
70
|
-
throw new SyntaxError(`Unable to resolve $ref pointer "${file.url}"
|
|
70
|
+
throw new SyntaxError(`Unable to resolve $ref pointer "${file.url}"`, { cause: err });
|
|
71
71
|
}
|
|
72
72
|
// Throw the original error, if it's one of our own (user-friendly) errors.
|
|
73
73
|
else if (err.error instanceof ResolverError) {
|
|
@@ -118,7 +118,7 @@ async function parseFile(file, options, $refs) {
|
|
|
118
118
|
throw err;
|
|
119
119
|
}
|
|
120
120
|
else if (!err || !("error" in err)) {
|
|
121
|
-
throw new SyntaxError(`Unable to parse ${file.url}
|
|
121
|
+
throw new SyntaxError(`Unable to parse ${file.url}`, { cause: err });
|
|
122
122
|
}
|
|
123
123
|
else if (err.error instanceof ParserError) {
|
|
124
124
|
throw err.error;
|
package/dist/lib/pointer.js
CHANGED
|
@@ -69,10 +69,24 @@ class Pointer {
|
|
|
69
69
|
// Crawl the object, one token at a time
|
|
70
70
|
this.value = unwrapOrThrow(obj);
|
|
71
71
|
for (let i = 0; i < tokens.length; i++) {
|
|
72
|
+
// During token walking, if the current value is an extended $ref (has sibling keys
|
|
73
|
+
// alongside $ref, as allowed by JSON Schema 2019-09+), and resolveIf$Ref marks it
|
|
74
|
+
// as circular because the $ref resolves to the same path we're walking, we should
|
|
75
|
+
// reset the circular flag and continue walking the object's own properties.
|
|
76
|
+
// This prevents false circular detection when e.g. a root schema has both
|
|
77
|
+
// $ref: "#/$defs/Foo" and $defs: { Foo: {...} } as siblings.
|
|
78
|
+
const wasCircular = this.circular;
|
|
79
|
+
const isExtendedRef = $Ref.isExtended$Ref(this.value);
|
|
72
80
|
if (resolveIf$Ref(this, options, pathFromRoot)) {
|
|
73
81
|
// The $ref path has changed, so append the remaining tokens to the path
|
|
74
82
|
this.path = Pointer.join(this.path, tokens.slice(i));
|
|
75
83
|
}
|
|
84
|
+
else if (!wasCircular && this.circular && isExtendedRef) {
|
|
85
|
+
// resolveIf$Ref set circular=true on an extended $ref during token walking.
|
|
86
|
+
// Since we still have tokens to process, the object should be walked by its
|
|
87
|
+
// properties, not treated as a circular self-reference.
|
|
88
|
+
this.circular = false;
|
|
89
|
+
}
|
|
76
90
|
const token = tokens[i];
|
|
77
91
|
if (this.value[token] === undefined || (this.value[token] === null && i === tokens.length - 1)) {
|
|
78
92
|
// one final case is if the entry itself includes slashes, and was parsed out as a token - we can join the remaining tokens and try again
|
package/lib/bundle.ts
CHANGED
|
@@ -39,8 +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);
|
|
50
|
+
|
|
51
|
+
// Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
|
|
52
|
+
const bundleOptions = (options.bundle || {}) as BundleOptions;
|
|
53
|
+
if (bundleOptions.optimizeInternalRefs !== false) {
|
|
54
|
+
fixRefsThroughRefs(inventory, parser.schema as any);
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
/**
|
|
@@ -206,6 +218,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
206
218
|
function remap<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
207
219
|
inventory: InventoryEntry[],
|
|
208
220
|
options: O,
|
|
221
|
+
rootId?: string,
|
|
209
222
|
) {
|
|
210
223
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
211
224
|
inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
|
|
@@ -253,15 +266,31 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
253
266
|
for (const entry of inventory) {
|
|
254
267
|
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
|
255
268
|
|
|
269
|
+
const bundleOpts = (options.bundle || {}) as BundleOptions;
|
|
256
270
|
if (!entry.external) {
|
|
257
|
-
// This $ref already resolves to the main JSON Schema file
|
|
258
|
-
|
|
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
|
+
}
|
|
259
277
|
} else if (entry.file === file && entry.hash === hash) {
|
|
260
278
|
// This $ref points to the same value as the previous $ref, so remap it to the same path
|
|
261
|
-
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
|
+
}
|
|
262
286
|
} else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
|
|
263
287
|
// This $ref points to a sub-value of the previous $ref, so remap it beneath that path
|
|
264
|
-
|
|
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
|
+
}
|
|
265
294
|
} else {
|
|
266
295
|
// We've moved to a new file or new hash
|
|
267
296
|
file = entry.file;
|
|
@@ -316,4 +345,116 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
|
|
|
316
345
|
const index = inventory.indexOf(entry);
|
|
317
346
|
inventory.splice(index, 1);
|
|
318
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* After remapping, some $ref paths may traverse through other $ref nodes.
|
|
351
|
+
* JSON pointer resolution does not follow $ref indirection, so these paths are invalid.
|
|
352
|
+
* This function detects and fixes such paths by following any intermediate $refs
|
|
353
|
+
* to compute a valid direct path.
|
|
354
|
+
*/
|
|
355
|
+
function fixRefsThroughRefs(inventory: InventoryEntry[], schema: any) {
|
|
356
|
+
for (const entry of inventory) {
|
|
357
|
+
if (!entry.$ref || typeof entry.$ref !== "object" || !("$ref" in entry.$ref)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const refValue = entry.$ref.$ref;
|
|
362
|
+
if (typeof refValue !== "string" || !refValue.startsWith("#/")) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const fixedPath = resolvePathThroughRefs(schema, refValue);
|
|
367
|
+
if (fixedPath !== refValue) {
|
|
368
|
+
entry.$ref.$ref = fixedPath;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Walks a JSON pointer path through the schema. If any intermediate value
|
|
375
|
+
* is a $ref, follows it and adjusts the path accordingly.
|
|
376
|
+
* Returns the corrected path that doesn't traverse through any $ref.
|
|
377
|
+
*/
|
|
378
|
+
function resolvePathThroughRefs(schema: any, refPath: string): string {
|
|
379
|
+
if (!refPath.startsWith("#/")) {
|
|
380
|
+
return refPath;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const segments = refPath.slice(2).split("/");
|
|
384
|
+
let current = schema;
|
|
385
|
+
const resolvedSegments: string[] = [];
|
|
386
|
+
|
|
387
|
+
for (const seg of segments) {
|
|
388
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
389
|
+
// Can't walk further, return original path
|
|
390
|
+
return refPath;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// If the current value is a $ref, follow it
|
|
394
|
+
if ("$ref" in current && typeof current.$ref === "string" && current.$ref.startsWith("#/")) {
|
|
395
|
+
// Follow the $ref and restart the path from its target
|
|
396
|
+
const targetSegments = current.$ref.slice(2).split("/");
|
|
397
|
+
resolvedSegments.length = 0;
|
|
398
|
+
resolvedSegments.push(...targetSegments);
|
|
399
|
+
current = walkPath(schema, current.$ref);
|
|
400
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
401
|
+
return refPath;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
406
|
+
const idx = Array.isArray(current) ? parseInt(decoded) : decoded;
|
|
407
|
+
current = current[idx];
|
|
408
|
+
resolvedSegments.push(seg);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const result = "#/" + resolvedSegments.join("/");
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Walks a JSON pointer path through a schema object, returning the value at that path.
|
|
417
|
+
*/
|
|
418
|
+
function walkPath(schema: any, path: string): any {
|
|
419
|
+
if (!path.startsWith("#/")) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const segments = path.slice(2).split("/");
|
|
424
|
+
let current = schema;
|
|
425
|
+
|
|
426
|
+
for (const seg of segments) {
|
|
427
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
431
|
+
const idx = Array.isArray(current) ? parseInt(decoded) : decoded;
|
|
432
|
+
current = current[idx];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return current;
|
|
436
|
+
}
|
|
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
|
+
|
|
319
460
|
export default bundle;
|
package/lib/dereference.ts
CHANGED
|
@@ -31,6 +31,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
31
31
|
parser.$refs,
|
|
32
32
|
options,
|
|
33
33
|
start,
|
|
34
|
+
0,
|
|
34
35
|
);
|
|
35
36
|
parser.$refs.circular = dereferenced.circular;
|
|
36
37
|
parser.schema = dereferenced.value;
|
|
@@ -48,6 +49,7 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
48
49
|
* @param $refs
|
|
49
50
|
* @param options
|
|
50
51
|
* @param startTime - The time when the dereferencing started
|
|
52
|
+
* @param depth - The current recursion depth
|
|
51
53
|
* @returns
|
|
52
54
|
*/
|
|
53
55
|
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
@@ -60,6 +62,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
60
62
|
$refs: $Refs<S, O>,
|
|
61
63
|
options: O,
|
|
62
64
|
startTime: number,
|
|
65
|
+
depth: number,
|
|
63
66
|
) {
|
|
64
67
|
let dereferenced;
|
|
65
68
|
const result = {
|
|
@@ -70,6 +73,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
70
73
|
checkDereferenceTimeout<S, O>(startTime, options);
|
|
71
74
|
|
|
72
75
|
const derefOptions = (options.dereference || {}) as DereferenceOptions;
|
|
76
|
+
const maxDepth = derefOptions.maxDepth ?? 500;
|
|
77
|
+
if (depth > maxDepth) {
|
|
78
|
+
throw new RangeError(
|
|
79
|
+
`Maximum dereference depth (${maxDepth}) exceeded at ${pathFromRoot}. ` +
|
|
80
|
+
`This likely indicates an extremely deep or recursive schema. ` +
|
|
81
|
+
`You can increase this limit with the dereference.maxDepth option.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
73
84
|
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
|
|
74
85
|
|
|
75
86
|
if (derefOptions?.circular === "ignore" || !processedObjects.has(obj)) {
|
|
@@ -88,6 +99,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
88
99
|
$refs,
|
|
89
100
|
options,
|
|
90
101
|
startTime,
|
|
102
|
+
depth,
|
|
91
103
|
);
|
|
92
104
|
result.circular = dereferenced.circular;
|
|
93
105
|
result.value = dereferenced.value;
|
|
@@ -103,7 +115,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
const value = obj[key];
|
|
106
|
-
let circular
|
|
118
|
+
let circular;
|
|
107
119
|
|
|
108
120
|
if ($Ref.isAllowed$Ref(value, options)) {
|
|
109
121
|
dereferenced = dereference$Ref(
|
|
@@ -116,6 +128,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
116
128
|
$refs,
|
|
117
129
|
options,
|
|
118
130
|
startTime,
|
|
131
|
+
depth,
|
|
119
132
|
);
|
|
120
133
|
circular = dereferenced.circular;
|
|
121
134
|
// Avoid pointless mutations; breaks frozen objects to no profit
|
|
@@ -133,7 +146,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
|
|
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;
|
|
137
157
|
|
|
138
158
|
// If we have data to preserve and our dereferenced object is still an object then
|
|
139
159
|
// we need copy back our preserved data into our dereferenced schema.
|
|
@@ -159,6 +179,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
159
179
|
$refs,
|
|
160
180
|
options,
|
|
161
181
|
startTime,
|
|
182
|
+
depth + 1,
|
|
162
183
|
);
|
|
163
184
|
circular = dereferenced.circular;
|
|
164
185
|
// Avoid pointless mutations; breaks frozen objects to no profit
|
|
@@ -205,6 +226,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
205
226
|
$refs: $Refs<S, O>,
|
|
206
227
|
options: O,
|
|
207
228
|
startTime: number,
|
|
229
|
+
depth: number,
|
|
208
230
|
) {
|
|
209
231
|
const isExternalRef = $Ref.isExternal$Ref($ref);
|
|
210
232
|
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
|
|
@@ -295,6 +317,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
295
317
|
$refs,
|
|
296
318
|
options,
|
|
297
319
|
startTime,
|
|
320
|
+
depth + 1,
|
|
298
321
|
);
|
|
299
322
|
circular = dereferenced.circular;
|
|
300
323
|
dereferencedValue = dereferenced.value;
|
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 {
|
|
@@ -89,6 +98,30 @@ export interface DereferenceOptions {
|
|
|
89
98
|
* Default: `true`
|
|
90
99
|
*/
|
|
91
100
|
mergeKeys?: boolean;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The maximum recursion depth for dereferencing nested schemas.
|
|
104
|
+
* If the schema nesting exceeds this depth, a RangeError will be thrown
|
|
105
|
+
* with a descriptive message instead of crashing with a stack overflow.
|
|
106
|
+
*
|
|
107
|
+
* Default: 500
|
|
108
|
+
*/
|
|
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;
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
/**
|
package/lib/parse.ts
CHANGED
|
@@ -91,7 +91,7 @@ async function readFile<S extends object = JSONSchema, O extends ParserOptions<S
|
|
|
91
91
|
throw new UnmatchedResolverError(file.url);
|
|
92
92
|
} else if (!err || !("error" in err)) {
|
|
93
93
|
// Throw a generic, friendly error.
|
|
94
|
-
throw new SyntaxError(`Unable to resolve $ref pointer "${file.url}"
|
|
94
|
+
throw new SyntaxError(`Unable to resolve $ref pointer "${file.url}"`, { cause: err });
|
|
95
95
|
}
|
|
96
96
|
// Throw the original error, if it's one of our own (user-friendly) errors.
|
|
97
97
|
else if (err.error instanceof ResolverError) {
|
|
@@ -143,7 +143,7 @@ async function parseFile<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
143
143
|
} else if (err && err.message && err.message.startsWith("Error parsing")) {
|
|
144
144
|
throw err;
|
|
145
145
|
} else if (!err || !("error" in err)) {
|
|
146
|
-
throw new SyntaxError(`Unable to parse ${file.url}
|
|
146
|
+
throw new SyntaxError(`Unable to parse ${file.url}`, { cause: err });
|
|
147
147
|
} else if (err.error instanceof ParserError) {
|
|
148
148
|
throw err.error;
|
|
149
149
|
} else {
|
package/lib/pointer.ts
CHANGED
|
@@ -89,9 +89,22 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
89
89
|
this.value = unwrapOrThrow(obj);
|
|
90
90
|
|
|
91
91
|
for (let i = 0; i < tokens.length; i++) {
|
|
92
|
+
// During token walking, if the current value is an extended $ref (has sibling keys
|
|
93
|
+
// alongside $ref, as allowed by JSON Schema 2019-09+), and resolveIf$Ref marks it
|
|
94
|
+
// as circular because the $ref resolves to the same path we're walking, we should
|
|
95
|
+
// reset the circular flag and continue walking the object's own properties.
|
|
96
|
+
// This prevents false circular detection when e.g. a root schema has both
|
|
97
|
+
// $ref: "#/$defs/Foo" and $defs: { Foo: {...} } as siblings.
|
|
98
|
+
const wasCircular = this.circular;
|
|
99
|
+
const isExtendedRef = $Ref.isExtended$Ref(this.value);
|
|
92
100
|
if (resolveIf$Ref(this, options, pathFromRoot)) {
|
|
93
101
|
// The $ref path has changed, so append the remaining tokens to the path
|
|
94
102
|
this.path = Pointer.join(this.path, tokens.slice(i));
|
|
103
|
+
} else if (!wasCircular && this.circular && isExtendedRef) {
|
|
104
|
+
// resolveIf$Ref set circular=true on an extended $ref during token walking.
|
|
105
|
+
// Since we still have tokens to process, the object should be walked by its
|
|
106
|
+
// properties, not treated as a circular self-reference.
|
|
107
|
+
this.circular = false;
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
const token = tokens[i];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apidevtools/json-schema-ref-parser",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.3.1",
|
|
4
4
|
"description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/lib/index.d.ts",
|
|
@@ -75,28 +75,28 @@
|
|
|
75
75
|
"js-yaml": "^4.1.1"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@eslint/compat": "^2.0.
|
|
79
|
-
"@eslint/js": "^
|
|
78
|
+
"@eslint/compat": "^2.0.2",
|
|
79
|
+
"@eslint/js": "^10.0.1",
|
|
80
80
|
"@types/eslint": "^9.6.1",
|
|
81
81
|
"@types/js-yaml": "^4.0.9",
|
|
82
82
|
"@types/json-schema": "^7.0.15",
|
|
83
83
|
"@types/node": "^25",
|
|
84
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
85
|
-
"@typescript-eslint/parser": "^8.
|
|
84
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
85
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
86
86
|
"@vitest/coverage-v8": "^4.0.18",
|
|
87
87
|
"cross-env": "^10.1.0",
|
|
88
|
-
"eslint": "^
|
|
88
|
+
"eslint": "^10.0.2",
|
|
89
89
|
"eslint-config-prettier": "^10.1.8",
|
|
90
90
|
"eslint-plugin-import": "^2.32.0",
|
|
91
91
|
"eslint-plugin-prettier": "^5.5.5",
|
|
92
92
|
"eslint-plugin-promise": "^7.2.1",
|
|
93
|
-
"eslint-plugin-unused-imports": "^4.
|
|
94
|
-
"globals": "^17.
|
|
95
|
-
"jsdom": "^
|
|
93
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
94
|
+
"globals": "^17.3.0",
|
|
95
|
+
"jsdom": "^28.1.0",
|
|
96
96
|
"prettier": "^3.8.1",
|
|
97
|
-
"rimraf": "^6.1.
|
|
97
|
+
"rimraf": "^6.1.3",
|
|
98
98
|
"typescript": "^5.9.3",
|
|
99
|
-
"typescript-eslint": "^8.
|
|
99
|
+
"typescript-eslint": "^8.56.1",
|
|
100
100
|
"vitest": "^4.0.18"
|
|
101
101
|
},
|
|
102
102
|
"release": {
|