@hey-api/json-schema-ref-parser 0.0.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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/lib/bundle.d.ts +26 -0
  4. package/dist/lib/bundle.js +293 -0
  5. package/dist/lib/dereference.d.ts +11 -0
  6. package/dist/lib/dereference.js +224 -0
  7. package/dist/lib/index.d.ts +74 -0
  8. package/dist/lib/index.js +208 -0
  9. package/dist/lib/options.d.ts +61 -0
  10. package/dist/lib/options.js +45 -0
  11. package/dist/lib/parse.d.ts +13 -0
  12. package/dist/lib/parse.js +87 -0
  13. package/dist/lib/parsers/binary.d.ts +2 -0
  14. package/dist/lib/parsers/binary.js +12 -0
  15. package/dist/lib/parsers/json.d.ts +2 -0
  16. package/dist/lib/parsers/json.js +38 -0
  17. package/dist/lib/parsers/text.d.ts +2 -0
  18. package/dist/lib/parsers/text.js +18 -0
  19. package/dist/lib/parsers/yaml.d.ts +2 -0
  20. package/dist/lib/parsers/yaml.js +28 -0
  21. package/dist/lib/pointer.d.ts +88 -0
  22. package/dist/lib/pointer.js +293 -0
  23. package/dist/lib/ref.d.ts +180 -0
  24. package/dist/lib/ref.js +226 -0
  25. package/dist/lib/refs.d.ts +127 -0
  26. package/dist/lib/refs.js +232 -0
  27. package/dist/lib/resolve-external.d.ts +13 -0
  28. package/dist/lib/resolve-external.js +147 -0
  29. package/dist/lib/resolvers/file.d.ts +4 -0
  30. package/dist/lib/resolvers/file.js +61 -0
  31. package/dist/lib/resolvers/url.d.ts +11 -0
  32. package/dist/lib/resolvers/url.js +57 -0
  33. package/dist/lib/types/index.d.ts +43 -0
  34. package/dist/lib/types/index.js +2 -0
  35. package/dist/lib/util/convert-path-to-posix.d.ts +1 -0
  36. package/dist/lib/util/convert-path-to-posix.js +14 -0
  37. package/dist/lib/util/errors.d.ts +56 -0
  38. package/dist/lib/util/errors.js +112 -0
  39. package/dist/lib/util/is-windows.d.ts +1 -0
  40. package/dist/lib/util/is-windows.js +6 -0
  41. package/dist/lib/util/plugins.d.ts +16 -0
  42. package/dist/lib/util/plugins.js +45 -0
  43. package/dist/lib/util/url.d.ts +79 -0
  44. package/dist/lib/util/url.js +285 -0
  45. package/dist/vite.config.d.ts +2 -0
  46. package/dist/vite.config.js +18 -0
  47. package/lib/bundle.ts +299 -0
  48. package/lib/dereference.ts +286 -0
  49. package/lib/index.ts +209 -0
  50. package/lib/options.ts +108 -0
  51. package/lib/parse.ts +56 -0
  52. package/lib/parsers/binary.ts +13 -0
  53. package/lib/parsers/json.ts +39 -0
  54. package/lib/parsers/text.ts +21 -0
  55. package/lib/parsers/yaml.ts +26 -0
  56. package/lib/pointer.ts +327 -0
  57. package/lib/ref.ts +279 -0
  58. package/lib/refs.ts +239 -0
  59. package/lib/resolve-external.ts +141 -0
  60. package/lib/resolvers/file.ts +24 -0
  61. package/lib/resolvers/url.ts +78 -0
  62. package/lib/types/index.ts +51 -0
  63. package/lib/util/convert-path-to-posix.ts +11 -0
  64. package/lib/util/errors.ts +153 -0
  65. package/lib/util/is-windows.ts +2 -0
  66. package/lib/util/plugins.ts +56 -0
  67. package/lib/util/url.ts +266 -0
  68. package/package.json +96 -0
package/lib/bundle.ts ADDED
@@ -0,0 +1,299 @@
1
+ import $Ref from "./ref.js";
2
+ import type { ParserOptions } from "./options.js";
3
+ import Pointer from "./pointer.js";
4
+ import * as url from "./util/url.js";
5
+ import type $Refs from "./refs.js";
6
+ import type { $RefParser } from "./index";
7
+ import type { JSONSchema } from "./types/index.js";
8
+
9
+ export interface InventoryEntry {
10
+ $ref: any;
11
+ parent: any;
12
+ key: any;
13
+ pathFromRoot: any;
14
+ depth: any;
15
+ file: any;
16
+ hash: any;
17
+ value: any;
18
+ circular: any;
19
+ extended: any;
20
+ external: any;
21
+ indirections: any;
22
+ }
23
+ /**
24
+ * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that
25
+ * only has *internal* references, not any *external* references.
26
+ * This method mutates the JSON schema object, adding new references and re-mapping existing ones.
27
+ *
28
+ * @param parser
29
+ * @param options
30
+ */
31
+ function bundle(
32
+ parser: $RefParser,
33
+ options: ParserOptions,
34
+ ) {
35
+ // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
36
+
37
+ // Build an inventory of all $ref pointers in the JSON Schema
38
+ const inventory: InventoryEntry[] = [];
39
+ crawl<JSONSchema>(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
40
+
41
+ // Remap all $ref pointers
42
+ remap(inventory);
43
+ }
44
+
45
+ /**
46
+ * Recursively crawls the given value, and inventories all JSON references.
47
+ *
48
+ * @param parent - The object containing the value to crawl. If the value is not an object or array, it will be ignored.
49
+ * @param key - The property key of `parent` to be crawled
50
+ * @param path - The full path of the property being crawled, possibly with a JSON Pointer in the hash
51
+ * @param pathFromRoot - The path of the property being crawled, from the schema root
52
+ * @param indirections
53
+ * @param inventory - An array of already-inventoried $ref pointers
54
+ * @param $refs
55
+ * @param options
56
+ */
57
+ function crawl<S extends object = JSONSchema>(
58
+ parent: object | $RefParser,
59
+ key: string | null,
60
+ path: string,
61
+ pathFromRoot: string,
62
+ indirections: number,
63
+ inventory: InventoryEntry[],
64
+ $refs: $Refs<S>,
65
+ options: ParserOptions,
66
+ ) {
67
+ const obj = key === null ? parent : parent[key as keyof typeof parent];
68
+
69
+ if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
70
+ if ($Ref.isAllowed$Ref(obj)) {
71
+ inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
72
+ } else {
73
+ // Crawl the object in a specific order that's optimized for bundling.
74
+ // This is important because it determines how `pathFromRoot` gets built,
75
+ // which later determines which keys get dereferenced and which ones get remapped
76
+ const keys = Object.keys(obj).sort((a, b) => {
77
+ // Most people will expect references to be bundled into the the "definitions" property,
78
+ // so we always crawl that property first, if it exists.
79
+ if (a === "definitions") {
80
+ return -1;
81
+ } else if (b === "definitions") {
82
+ return 1;
83
+ } else {
84
+ // Otherwise, crawl the keys based on their length.
85
+ // This produces the shortest possible bundled references
86
+ return a.length - b.length;
87
+ }
88
+ }) as (keyof typeof obj)[];
89
+
90
+ for (const key of keys) {
91
+ const keyPath = Pointer.join(path, key);
92
+ const keyPathFromRoot = Pointer.join(pathFromRoot, key);
93
+ const value = obj[key];
94
+
95
+ if ($Ref.isAllowed$Ref(value)) {
96
+ inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
97
+ } else {
98
+ crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Inventories the given JSON Reference (i.e. records detailed information about it so we can
107
+ * optimize all $refs in the schema), and then crawls the resolved value.
108
+ *
109
+ * @param $refParent - The object that contains a JSON Reference as one of its keys
110
+ * @param $refKey - The key in `$refParent` that is a JSON Reference
111
+ * @param path - The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash
112
+ * @param indirections - unknown
113
+ * @param pathFromRoot - The path of the JSON Reference at `$refKey`, from the schema root
114
+ * @param inventory - An array of already-inventoried $ref pointers
115
+ * @param $refs
116
+ * @param options
117
+ */
118
+ function inventory$Ref<S extends object = JSONSchema>(
119
+ $refParent: any,
120
+ $refKey: string | null,
121
+ path: string,
122
+ pathFromRoot: string,
123
+ indirections: number,
124
+ inventory: InventoryEntry[],
125
+ $refs: $Refs<S>,
126
+ options: ParserOptions,
127
+ ) {
128
+ const $ref = $refKey === null ? $refParent : $refParent[$refKey];
129
+ const $refPath = url.resolve(path, $ref.$ref);
130
+ const pointer = $refs._resolve($refPath, pathFromRoot, options);
131
+ if (pointer === null) {
132
+ return;
133
+ }
134
+ const parsed = Pointer.parse(pathFromRoot);
135
+ const depth = parsed.length;
136
+ const file = url.stripHash(pointer.path);
137
+ const hash = url.getHash(pointer.path);
138
+ const external = file !== $refs._root$Ref.path;
139
+ const extended = $Ref.isExtended$Ref($ref);
140
+ indirections += pointer.indirections;
141
+
142
+ const existingEntry = findInInventory(inventory, $refParent, $refKey);
143
+ if (existingEntry) {
144
+ // This $Ref has already been inventoried, so we don't need to process it again
145
+ if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
146
+ removeFromInventory(inventory, existingEntry);
147
+ } else {
148
+ return;
149
+ }
150
+ }
151
+
152
+ inventory.push({
153
+ $ref, // The JSON Reference (e.g. {$ref: string})
154
+ parent: $refParent, // The object that contains this $ref pointer
155
+ key: $refKey, // The key in `parent` that is the $ref pointer
156
+ pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
157
+ depth, // How far from the JSON Schema root is this $ref pointer?
158
+ file, // The file that the $ref pointer resolves to
159
+ hash, // The hash within `file` that the $ref pointer resolves to
160
+ value: pointer.value, // The resolved value of the $ref pointer
161
+ circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
162
+ extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
163
+ external, // Does this $ref pointer point to a file other than the main JSON Schema file?
164
+ indirections, // The number of indirect references that were traversed to resolve the value
165
+ });
166
+
167
+ // Recursively crawl the resolved value
168
+ if (!existingEntry || external) {
169
+ crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Re-maps every $ref pointer, so that they're all relative to the root of the JSON Schema.
175
+ * Each referenced value is dereferenced EXACTLY ONCE. All subsequent references to the same
176
+ * value are re-mapped to point to the first reference.
177
+ *
178
+ * @example: {
179
+ * first: { $ref: somefile.json#/some/part },
180
+ * second: { $ref: somefile.json#/another/part },
181
+ * third: { $ref: somefile.json },
182
+ * fourth: { $ref: somefile.json#/some/part/sub/part }
183
+ * }
184
+ *
185
+ * In this example, there are four references to the same file, but since the third reference points
186
+ * to the ENTIRE file, that's the only one we need to dereference. The other three can just be
187
+ * remapped to point inside the third one.
188
+ *
189
+ * On the other hand, if the third reference DIDN'T exist, then the first and second would both need
190
+ * to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
191
+ * need to be dereferenced, because it can be remapped to point inside the first one.
192
+ *
193
+ * @param inventory
194
+ */
195
+ function remap(inventory: InventoryEntry[]) {
196
+ // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
197
+ inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
198
+ if (a.file !== b.file) {
199
+ // Group all the $refs that point to the same file
200
+ return a.file < b.file ? -1 : +1;
201
+ } else if (a.hash !== b.hash) {
202
+ // Group all the $refs that point to the same part of the file
203
+ return a.hash < b.hash ? -1 : +1;
204
+ } else if (a.circular !== b.circular) {
205
+ // If the $ref points to itself, then sort it higher than other $refs that point to this $ref
206
+ return a.circular ? -1 : +1;
207
+ } else if (a.extended !== b.extended) {
208
+ // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
209
+ return a.extended ? +1 : -1;
210
+ } else if (a.indirections !== b.indirections) {
211
+ // Sort direct references higher than indirect references
212
+ return a.indirections - b.indirections;
213
+ } else if (a.depth !== b.depth) {
214
+ // Sort $refs by how close they are to the JSON Schema root
215
+ return a.depth - b.depth;
216
+ } else {
217
+ // Determine how far each $ref is from the "definitions" property.
218
+ // Most people will expect references to be bundled into the the "definitions" property if possible.
219
+ const aDefinitionsIndex = a.pathFromRoot.lastIndexOf("/definitions");
220
+ const bDefinitionsIndex = b.pathFromRoot.lastIndexOf("/definitions");
221
+
222
+ if (aDefinitionsIndex !== bDefinitionsIndex) {
223
+ // Give higher priority to the $ref that's closer to the "definitions" property
224
+ return bDefinitionsIndex - aDefinitionsIndex;
225
+ } else {
226
+ // All else is equal, so use the shorter path, which will produce the shortest possible reference
227
+ return a.pathFromRoot.length - b.pathFromRoot.length;
228
+ }
229
+ }
230
+ });
231
+
232
+ let file, hash, pathFromRoot;
233
+ for (const entry of inventory) {
234
+ // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
235
+
236
+ if (!entry.external) {
237
+ // This $ref already resolves to the main JSON Schema file
238
+ entry.$ref.$ref = entry.hash;
239
+ } else if (entry.file === file && entry.hash === hash) {
240
+ // This $ref points to the same value as the prevous $ref, so remap it to the same path
241
+ entry.$ref.$ref = pathFromRoot;
242
+ } else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
243
+ // This $ref points to a sub-value of the prevous $ref, so remap it beneath that path
244
+ entry.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
245
+ } else {
246
+ // We've moved to a new file or new hash
247
+ file = entry.file;
248
+ hash = entry.hash;
249
+ pathFromRoot = entry.pathFromRoot;
250
+
251
+ // This is the first $ref to point to this value, so dereference the value.
252
+ // Any other $refs that point to the same value will point to this $ref instead
253
+ entry.$ref = entry.parent[entry.key] = $Ref.dereference(entry.$ref, entry.value);
254
+
255
+ if (entry.circular) {
256
+ // This $ref points to itself
257
+ entry.$ref.$ref = entry.pathFromRoot;
258
+ }
259
+ }
260
+ }
261
+
262
+ // we want to ensure that any $refs that point to another $ref are remapped to point to the final value
263
+ // let hadChange = true;
264
+ // while (hadChange) {
265
+ // hadChange = false;
266
+ // for (const entry of inventory) {
267
+ // if (entry.$ref && typeof entry.$ref === "object" && "$ref" in entry.$ref) {
268
+ // const resolved = inventory.find((e: InventoryEntry) => e.pathFromRoot === entry.$ref.$ref);
269
+ // if (resolved) {
270
+ // const resolvedPointsToAnotherRef =
271
+ // resolved.$ref && typeof resolved.$ref === "object" && "$ref" in resolved.$ref;
272
+ // if (resolvedPointsToAnotherRef && entry.$ref.$ref !== resolved.$ref.$ref) {
273
+ // // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
274
+ // entry.$ref.$ref = resolved.$ref.$ref;
275
+ // hadChange = true;
276
+ // }
277
+ // }
278
+ // }
279
+ // }
280
+ // }
281
+ }
282
+
283
+ /**
284
+ * TODO
285
+ */
286
+ function findInInventory(inventory: InventoryEntry[], $refParent: any, $refKey: any) {
287
+ for (const existingEntry of inventory) {
288
+ if (existingEntry && existingEntry.parent === $refParent && existingEntry.key === $refKey) {
289
+ return existingEntry;
290
+ }
291
+ }
292
+ return undefined;
293
+ }
294
+
295
+ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
296
+ const index = inventory.indexOf(entry);
297
+ inventory.splice(index, 1);
298
+ }
299
+ export default bundle;
@@ -0,0 +1,286 @@
1
+ import $Ref from "./ref.js";
2
+ import Pointer from "./pointer.js";
3
+ import { ono } from "@jsdevtools/ono";
4
+ import * as url from "./util/url.js";
5
+ import type $Refs from "./refs.js";
6
+ import type { DereferenceOptions, ParserOptions } from "./options.js";
7
+ import type { JSONSchema } from "./types";
8
+ import type { $RefParser } from "./index";
9
+ import { TimeoutError } from "./util/errors";
10
+
11
+ export default dereference;
12
+
13
+ /**
14
+ * Crawls the JSON schema, finds all JSON references, and dereferences them.
15
+ * This method mutates the JSON schema object, replacing JSON references with their resolved value.
16
+ *
17
+ * @param parser
18
+ * @param options
19
+ */
20
+ function dereference(
21
+ parser: $RefParser,
22
+ options: ParserOptions,
23
+ ) {
24
+ const start = Date.now();
25
+ // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
26
+ const dereferenced = crawl<JSONSchema>(
27
+ parser.schema,
28
+ parser.$refs._root$Ref.path!,
29
+ "#",
30
+ new Set(),
31
+ new Set(),
32
+ new Map(),
33
+ parser.$refs,
34
+ options,
35
+ start,
36
+ );
37
+ parser.$refs.circular = dereferenced.circular;
38
+ parser.schema = dereferenced.value;
39
+ }
40
+
41
+ /**
42
+ * Recursively crawls the given value, and dereferences any JSON references.
43
+ *
44
+ * @param obj - The value to crawl. If it's not an object or array, it will be ignored.
45
+ * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash
46
+ * @param pathFromRoot - The path of `obj` from the schema root
47
+ * @param parents - An array of the parent objects that have already been dereferenced
48
+ * @param processedObjects - An array of all the objects that have already been processed
49
+ * @param dereferencedCache - An map of all the dereferenced objects
50
+ * @param $refs
51
+ * @param options
52
+ * @param startTime - The time when the dereferencing started
53
+ * @returns
54
+ */
55
+ function crawl<S extends object = JSONSchema>(
56
+ obj: any,
57
+ path: string,
58
+ pathFromRoot: string,
59
+ parents: Set<any>,
60
+ processedObjects: Set<any>,
61
+ dereferencedCache: any,
62
+ $refs: $Refs<S>,
63
+ options: ParserOptions,
64
+ startTime: number,
65
+ ) {
66
+ let dereferenced;
67
+ const result = {
68
+ value: obj,
69
+ circular: false,
70
+ };
71
+
72
+ if (options && options.timeoutMs) {
73
+ if (Date.now() - startTime > options.timeoutMs) {
74
+ throw new TimeoutError(options.timeoutMs);
75
+ }
76
+ }
77
+ const derefOptions = (options.dereference || {}) as DereferenceOptions;
78
+ const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
79
+
80
+ if (derefOptions?.circular === "ignore" || !processedObjects.has(obj)) {
81
+ if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
82
+ parents.add(obj);
83
+ processedObjects.add(obj);
84
+
85
+ if ($Ref.isAllowed$Ref(obj)) {
86
+ dereferenced = dereference$Ref(
87
+ obj,
88
+ path,
89
+ pathFromRoot,
90
+ parents,
91
+ processedObjects,
92
+ dereferencedCache,
93
+ $refs,
94
+ options,
95
+ startTime,
96
+ );
97
+ result.circular = dereferenced.circular;
98
+ result.value = dereferenced.value;
99
+ } else {
100
+ for (const key of Object.keys(obj)) {
101
+ const keyPath = Pointer.join(path, key);
102
+ const keyPathFromRoot = Pointer.join(pathFromRoot, key);
103
+
104
+ if (isExcludedPath(keyPathFromRoot)) {
105
+ continue;
106
+ }
107
+
108
+ const value = obj[key];
109
+ let circular = false;
110
+
111
+ if ($Ref.isAllowed$Ref(value)) {
112
+ dereferenced = dereference$Ref(
113
+ value,
114
+ keyPath,
115
+ keyPathFromRoot,
116
+ parents,
117
+ processedObjects,
118
+ dereferencedCache,
119
+ $refs,
120
+ options,
121
+ startTime,
122
+ );
123
+ circular = dereferenced.circular;
124
+ // Avoid pointless mutations; breaks frozen objects to no profit
125
+ if (obj[key] !== dereferenced.value) {
126
+ obj[key] = dereferenced.value;
127
+ derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
128
+ }
129
+ } else {
130
+ if (!parents.has(value)) {
131
+ dereferenced = crawl(
132
+ value,
133
+ keyPath,
134
+ keyPathFromRoot,
135
+ parents,
136
+ processedObjects,
137
+ dereferencedCache,
138
+ $refs,
139
+ options,
140
+ startTime,
141
+ );
142
+ circular = dereferenced.circular;
143
+ // Avoid pointless mutations; breaks frozen objects to no profit
144
+ if (obj[key] !== dereferenced.value) {
145
+ obj[key] = dereferenced.value;
146
+ }
147
+ } else {
148
+ circular = foundCircularReference(keyPath, $refs, options);
149
+ }
150
+ }
151
+
152
+ // Set the "isCircular" flag if this or any other property is circular
153
+ result.circular = result.circular || circular;
154
+ }
155
+ }
156
+
157
+ parents.delete(obj);
158
+ }
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Dereferences the given JSON Reference, and then crawls the resulting value.
166
+ *
167
+ * @param $ref - The JSON Reference to resolve
168
+ * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash
169
+ * @param pathFromRoot - The path of `$ref` from the schema root
170
+ * @param parents - An array of the parent objects that have already been dereferenced
171
+ * @param processedObjects - An array of all the objects that have already been dereferenced
172
+ * @param dereferencedCache - An map of all the dereferenced objects
173
+ * @param $refs
174
+ * @param options
175
+ * @returns
176
+ */
177
+ function dereference$Ref<S extends object = JSONSchema>(
178
+ $ref: any,
179
+ path: string,
180
+ pathFromRoot: string,
181
+ parents: Set<any>,
182
+ processedObjects: any,
183
+ dereferencedCache: any,
184
+ $refs: $Refs<S>,
185
+ options: ParserOptions,
186
+ startTime: number,
187
+ ) {
188
+ const $refPath = url.resolve(path, $ref.$ref);
189
+
190
+ const cache = dereferencedCache.get($refPath);
191
+ if (cache && !cache.circular) {
192
+ const refKeys = Object.keys($ref);
193
+ if (refKeys.length > 1) {
194
+ const extraKeys = {};
195
+ for (const key of refKeys) {
196
+ if (key !== "$ref" && !(key in cache.value)) {
197
+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
198
+ extraKeys[key] = $ref[key];
199
+ }
200
+ }
201
+ return {
202
+ circular: cache.circular,
203
+ value: Object.assign({}, cache.value, extraKeys),
204
+ };
205
+ }
206
+
207
+ return cache;
208
+ }
209
+
210
+ const pointer = $refs._resolve($refPath, path, options);
211
+
212
+ if (pointer === null) {
213
+ return {
214
+ circular: false,
215
+ value: null,
216
+ };
217
+ }
218
+
219
+ // Check for circular references
220
+ const directCircular = pointer.circular;
221
+ let circular = directCircular || parents.has(pointer.value);
222
+ if (circular) {
223
+ foundCircularReference(path, $refs, options);
224
+ }
225
+
226
+ // Dereference the JSON reference
227
+ let dereferencedValue = $Ref.dereference($ref, pointer.value);
228
+
229
+ // Crawl the dereferenced value (unless it's circular)
230
+ if (!circular) {
231
+ // Determine if the dereferenced value is circular
232
+ const dereferenced = crawl(
233
+ dereferencedValue,
234
+ pointer.path,
235
+ pathFromRoot,
236
+ parents,
237
+ processedObjects,
238
+ dereferencedCache,
239
+ $refs,
240
+ options,
241
+ startTime,
242
+ );
243
+ circular = dereferenced.circular;
244
+ dereferencedValue = dereferenced.value;
245
+ }
246
+
247
+ if (circular && !directCircular && options.dereference?.circular === "ignore") {
248
+ // The user has chosen to "ignore" circular references, so don't change the value
249
+ dereferencedValue = $ref;
250
+ }
251
+
252
+ if (directCircular) {
253
+ // The pointer is a DIRECT circular reference (i.e. it references itself).
254
+ // So replace the $ref path with the absolute path from the JSON Schema root
255
+ dereferencedValue.$ref = pathFromRoot;
256
+ }
257
+
258
+ const dereferencedObject = {
259
+ circular,
260
+ value: dereferencedValue,
261
+ };
262
+
263
+ // only cache if no extra properties than $ref
264
+ if (Object.keys($ref).length === 1) {
265
+ dereferencedCache.set($refPath, dereferencedObject);
266
+ }
267
+
268
+ return dereferencedObject;
269
+ }
270
+
271
+ /**
272
+ * Called when a circular reference is found.
273
+ * It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false.
274
+ *
275
+ * @param keyPath - The JSON Reference path of the circular reference
276
+ * @param $refs
277
+ * @param options
278
+ * @returns - always returns true, to indicate that a circular reference was found
279
+ */
280
+ function foundCircularReference(keyPath: any, $refs: any, options: any) {
281
+ $refs.circular = true;
282
+ if (!options.dereference.circular) {
283
+ throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
284
+ }
285
+ return true;
286
+ }