@apidevtools/json-schema-ref-parser 15.3.0 → 15.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import Pointer from "./pointer.js";
3
3
  import parse from "./parse.js";
4
4
  import * as url from "./util/url.js";
5
5
  import { isHandledError } from "./util/errors.js";
6
+ import { getSchemaBasePath } from "./util/schema-resources.js";
6
7
  /**
7
8
  * Crawls the JSON schema, finds all external JSON references, and resolves their values.
8
9
  * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
@@ -20,7 +21,7 @@ function resolveExternal(parser, options) {
20
21
  }
21
22
  try {
22
23
  // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
23
- const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
24
+ const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, parser.$refs, options);
24
25
  return Promise.all(promises);
25
26
  }
26
27
  catch (e) {
@@ -43,19 +44,21 @@ function resolveExternal(parser, options) {
43
44
  * If any of the JSON references point to files that contain additional JSON references,
44
45
  * then the corresponding promise will internally reference an array of promises.
45
46
  */
46
- function crawl(obj, path, $refs, options, seen, external) {
47
+ function crawl(obj, path, scopeBase, dynamicIdScope, $refs, options, seen, external) {
47
48
  seen ||= new Set();
48
49
  let promises = [];
49
50
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
50
51
  seen.add(obj); // Track previously seen objects to avoid infinite recursion
52
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
51
53
  if ($Ref.isExternal$Ref(obj)) {
52
- promises.push(resolve$Ref(obj, path, $refs, options));
54
+ promises.push(resolve$Ref(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
53
55
  }
54
56
  const keys = Object.keys(obj);
55
57
  for (const key of keys) {
56
58
  const keyPath = Pointer.join(path, key);
57
59
  const value = obj[key];
58
- promises = promises.concat(crawl(value, keyPath, $refs, options, seen, external));
60
+ const childScopeBase = dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
61
+ promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
59
62
  }
60
63
  }
61
64
  return promises;
@@ -72,23 +75,33 @@ function crawl(obj, path, $refs, options, seen, external) {
72
75
  * The promise resolves once all JSON references in the object have been resolved,
73
76
  * including nested references that are contained in externally-referenced files.
74
77
  */
75
- async function resolve$Ref($ref, path, $refs, options) {
78
+ async function resolve$Ref($ref, path, scopeBase, dynamicIdScope, $refs, options) {
76
79
  const shouldResolveOnCwd = options.dereference?.externalReferenceResolution === "root";
77
- const resolvedPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
80
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
81
+ const resolvedPath = url.resolve(resolutionBase, $ref.$ref);
78
82
  const withoutHash = url.stripHash(resolvedPath);
79
83
  // $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
80
84
  // Do we already have this $ref?
81
- const ref = $refs._$refs[withoutHash];
85
+ const ref = $refs._get$Ref(withoutHash);
82
86
  if (ref) {
83
87
  // We've already parsed this $ref, so use the existing value
84
88
  return Promise.resolve(ref.value);
85
89
  }
86
90
  // Parse the $referenced file/url
87
91
  try {
88
- const result = await parse(resolvedPath, $refs, options);
92
+ const reference = $ref.$ref;
93
+ const parseTarget = {
94
+ url: resolvedPath,
95
+ baseUrl: resolutionBase,
96
+ };
97
+ if (typeof reference === "string") {
98
+ parseTarget.reference = reference;
99
+ }
100
+ const result = await parse(parseTarget, $refs, options);
89
101
  // Crawl the parsed value
90
102
  // console.log('Resolving $ref pointers in %s', withoutHash);
91
- const promises = crawl(result, withoutHash + "#", $refs, options, new Set(), true);
103
+ const parsedRef = $refs._get$Ref(withoutHash);
104
+ const promises = crawl(result, withoutHash + "#", withoutHash, parsedRef?.dynamicIdScope ?? false, $refs, options, new Set(), true);
92
105
  return Promise.all(promises);
93
106
  }
94
107
  catch (err) {
@@ -103,6 +103,17 @@ export interface FileInfo {
103
103
  * The full URL of the file. This could be any type of URL, including "http://", "https://", "file://", "ftp://", "mongodb://", or even a local filesystem path (when running in Node.js).
104
104
  */
105
105
  url: string;
106
+ /**
107
+ * The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash.
108
+ * This is useful for custom resolvers that need to preserve relative reference semantics instead
109
+ * of relying on the already-resolved `url`.
110
+ */
111
+ reference?: string;
112
+ /**
113
+ * The URL that `reference` was resolved against to produce `url`.
114
+ * This may include a JSON Pointer fragment when the reference originated from a nested location.
115
+ */
116
+ baseUrl?: string;
106
117
  /**
107
118
  * The hash (URL fragment) of the file URL, including the # symbol. If the URL doesn't have a hash, then this will be an empty string.
108
119
  */
@@ -0,0 +1,6 @@
1
+ import type { ParserOptions } from "../options.js";
2
+ import type { JSONSchema } from "../index.js";
3
+ import type $Refs from "../refs.js";
4
+ export declare function getSchemaBasePath(basePath: string, value: unknown): string;
5
+ export declare function usesDynamicIdScope(value: unknown): boolean;
6
+ export declare function registerSchemaResources<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($refs: $Refs<S, O>, basePath: string, value: unknown, pathType?: string | unknown, dynamicIdScope?: boolean): void;
@@ -0,0 +1,47 @@
1
+ import * as url from "./url.js";
2
+ export function getSchemaBasePath(basePath, value) {
3
+ const schemaId = getSchemaId(value);
4
+ return schemaId ? url.resolve(basePath, schemaId) : basePath;
5
+ }
6
+ export function usesDynamicIdScope(value) {
7
+ if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
8
+ return false;
9
+ }
10
+ const schema = value.$schema;
11
+ if (typeof schema === "string" &&
12
+ (schema.includes("draft/2019-09/") || schema.includes("draft/2020-12/") || schema.includes("oas/3.1/"))) {
13
+ return true;
14
+ }
15
+ const openapi = value.openapi;
16
+ return typeof openapi === "string" && /^3\.1(?:\.|$)/.test(openapi);
17
+ }
18
+ export function registerSchemaResources($refs, basePath, value, pathType, dynamicIdScope = false) {
19
+ if (!dynamicIdScope) {
20
+ return;
21
+ }
22
+ const seen = new Set();
23
+ const visit = (node, scopeBase) => {
24
+ if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
25
+ return;
26
+ }
27
+ seen.add(node);
28
+ const nextScopeBase = getSchemaBasePath(scopeBase, node);
29
+ if (nextScopeBase !== scopeBase) {
30
+ $refs._addAlias(nextScopeBase, node, pathType, dynamicIdScope);
31
+ }
32
+ for (const key of Object.keys(node)) {
33
+ visit(node[key], nextScopeBase);
34
+ }
35
+ };
36
+ visit(value, basePath);
37
+ }
38
+ function getSchemaId(value) {
39
+ if (value &&
40
+ typeof value === "object" &&
41
+ "$id" in value &&
42
+ typeof value.$id === "string" &&
43
+ value.$id.length > 0) {
44
+ return value.$id;
45
+ }
46
+ return undefined;
47
+ }
package/lib/bundle.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import $Ref from "./ref.js";
2
2
  import Pointer from "./pointer.js";
3
3
  import * as url from "./util/url.js";
4
+ import { getSchemaBasePath } from "./util/schema-resources.js";
4
5
  import type $Refs from "./refs.js";
5
6
  import type $RefParser from "./index.js";
6
7
  import type { ParserOptions } from "./index.js";
@@ -37,13 +38,33 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
37
38
 
38
39
  // Build an inventory of all $ref pointers in the JSON Schema
39
40
  const inventory: InventoryEntry[] = [];
40
- crawl<S, O>(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
41
+ crawl<S, O>(
42
+ parser,
43
+ "schema",
44
+ parser.$refs._root$Ref.path + "#",
45
+ parser.$refs._root$Ref.path!,
46
+ parser.$refs._root$Ref.dynamicIdScope,
47
+ "#",
48
+ 0,
49
+ inventory,
50
+ parser.$refs,
51
+ options,
52
+ );
53
+
54
+ // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
55
+ const rootId =
56
+ parser.schema && typeof parser.schema === "object" && "$id" in (parser.schema as any)
57
+ ? (parser.schema as any).$id
58
+ : undefined;
41
59
 
42
60
  // Remap all $ref pointers
43
- remap<S, O>(inventory, options);
61
+ remap<S, O>(inventory, options, rootId);
44
62
 
45
63
  // Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
46
- fixRefsThroughRefs(inventory, parser.schema as any);
64
+ const bundleOptions = (options.bundle || {}) as BundleOptions;
65
+ if (bundleOptions.optimizeInternalRefs !== false) {
66
+ fixRefsThroughRefs(inventory, parser.schema as any);
67
+ }
47
68
  }
48
69
 
49
70
  /**
@@ -62,6 +83,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
62
83
  parent: object | $RefParser<S, O>,
63
84
  key: string | null,
64
85
  path: string,
86
+ scopeBase: string,
87
+ dynamicIdScope: boolean,
65
88
  pathFromRoot: string,
66
89
  indirections: number,
67
90
  inventory: InventoryEntry[],
@@ -73,8 +96,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
73
96
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
74
97
 
75
98
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
99
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
76
100
  if ($Ref.isAllowed$Ref(obj)) {
77
- inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
101
+ inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
78
102
  } else {
79
103
  // Crawl the object in a specific order that's optimized for bundling.
80
104
  // This is important because it determines how `pathFromRoot` gets built,
@@ -99,9 +123,21 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
99
123
  const value = obj[key];
100
124
 
101
125
  if ($Ref.isAllowed$Ref(value)) {
102
- inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
126
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
127
+ inventory$Ref(
128
+ obj,
129
+ key,
130
+ keyPath,
131
+ valueScopeBase,
132
+ dynamicIdScope,
133
+ keyPathFromRoot,
134
+ indirections,
135
+ inventory,
136
+ $refs,
137
+ options,
138
+ );
103
139
  } else {
104
- crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
140
+ crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
105
141
  }
106
142
 
107
143
  // We need to ensure that we have an object to work with here because we may be crawling
@@ -133,6 +169,8 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
133
169
  $refParent: any,
134
170
  $refKey: string | null,
135
171
  path: string,
172
+ scopeBase: string,
173
+ dynamicIdScope: boolean,
136
174
  pathFromRoot: string,
137
175
  indirections: number,
138
176
  inventory: InventoryEntry[],
@@ -140,7 +178,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
140
178
  options: O,
141
179
  ) {
142
180
  const $ref = $refKey === null ? $refParent : $refParent[$refKey];
143
- const $refPath = url.resolve(path, $ref.$ref);
181
+ const $refPath = url.resolve(dynamicIdScope ? scopeBase : path, $ref.$ref);
144
182
  const pointer = $refs._resolve($refPath, pathFromRoot, options);
145
183
  if (pointer === null) {
146
184
  return;
@@ -149,7 +187,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
149
187
  const depth = parsed.length;
150
188
  const file = url.stripHash(pointer.path);
151
189
  const hash = url.getHash(pointer.path);
152
- const external = file !== $refs._root$Ref.path;
190
+ const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
153
191
  const extended = $Ref.isExtended$Ref($ref);
154
192
  indirections += pointer.indirections;
155
193
 
@@ -180,7 +218,18 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
180
218
 
181
219
  // Recursively crawl the resolved value
182
220
  if (!existingEntry || external) {
183
- crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
221
+ crawl(
222
+ pointer.value,
223
+ null,
224
+ pointer.path,
225
+ pointer.$ref.path!,
226
+ pointer.$ref.dynamicIdScope,
227
+ pathFromRoot,
228
+ indirections + 1,
229
+ inventory,
230
+ $refs,
231
+ options,
232
+ );
184
233
  }
185
234
  }
186
235
 
@@ -209,6 +258,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
209
258
  function remap<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
210
259
  inventory: InventoryEntry[],
211
260
  options: O,
261
+ rootId?: string,
212
262
  ) {
213
263
  // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
214
264
  inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
@@ -256,15 +306,31 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
256
306
  for (const entry of inventory) {
257
307
  // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
258
308
 
309
+ const bundleOpts = (options.bundle || {}) as BundleOptions;
259
310
  if (!entry.external) {
260
- // This $ref already resolves to the main JSON Schema file
261
- entry.$ref.$ref = entry.hash;
311
+ // This $ref already resolves to the main JSON Schema file.
312
+ // When optimizeInternalRefs is false, preserve the original internal ref path
313
+ // instead of rewriting it to the fully resolved hash.
314
+ if (bundleOpts.optimizeInternalRefs !== false) {
315
+ entry.$ref.$ref = entry.hash;
316
+ }
262
317
  } else if (entry.file === file && entry.hash === hash) {
263
318
  // This $ref points to the same value as the previous $ref, so remap it to the same path
264
- entry.$ref.$ref = pathFromRoot;
319
+ if (rootId && isInsideIdScope(inventory, entry)) {
320
+ // This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
321
+ // would be resolved relative to that $id, not the document root. Qualify with the root $id.
322
+ entry.$ref.$ref = rootId + pathFromRoot;
323
+ } else {
324
+ entry.$ref.$ref = pathFromRoot;
325
+ }
265
326
  } else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
266
327
  // This $ref points to a sub-value of the previous $ref, so remap it beneath that path
267
- entry.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
328
+ const subPath = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
329
+ if (rootId && isInsideIdScope(inventory, entry)) {
330
+ entry.$ref.$ref = rootId + subPath;
331
+ } else {
332
+ entry.$ref.$ref = subPath;
333
+ }
268
334
  } else {
269
335
  // We've moved to a new file or new hash
270
336
  file = entry.file;
@@ -409,4 +475,26 @@ function walkPath(schema: any, path: string): any {
409
475
  return current;
410
476
  }
411
477
 
478
+ /**
479
+ * Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
480
+ * If so, root-relative JSON Pointer $refs placed at this location would be resolved against
481
+ * the $id base URI rather than the document root, making them invalid.
482
+ */
483
+ function isInsideIdScope(inventory: InventoryEntry[], entry: InventoryEntry): boolean {
484
+ for (const other of inventory) {
485
+ // Skip root-level entries
486
+ if (other.pathFromRoot === "#" || other.pathFromRoot === "#/") {
487
+ continue;
488
+ }
489
+ // Check if the other entry is an ancestor of the current entry
490
+ if (entry.pathFromRoot.startsWith(other.pathFromRoot + "/")) {
491
+ // Check if the ancestor's resolved value has a $id
492
+ if (other.value && typeof other.value === "object" && "$id" in other.value) {
493
+ return true;
494
+ }
495
+ }
496
+ }
497
+ return false;
498
+ }
499
+
412
500
  export default bundle;
@@ -1,6 +1,7 @@
1
1
  import $Ref from "./ref.js";
2
2
  import Pointer from "./pointer.js";
3
3
  import * as url from "./util/url.js";
4
+ import { getSchemaBasePath } from "./util/schema-resources.js";
4
5
  import type $Refs from "./refs.js";
5
6
  import type { DereferenceOptions, ParserOptions } from "./options.js";
6
7
  import { type $RefParser, type JSONSchema } from "./index.js";
@@ -24,6 +25,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
24
25
  const dereferenced = crawl<S, O>(
25
26
  parser.schema,
26
27
  parser.$refs._root$Ref.path!,
28
+ parser.$refs._root$Ref.path!,
29
+ parser.$refs._root$Ref.dynamicIdScope,
27
30
  "#",
28
31
  new Set(),
29
32
  new Set(),
@@ -55,6 +58,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
55
58
  function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
56
59
  obj: any,
57
60
  path: string,
61
+ scopeBase: string,
62
+ dynamicIdScope: boolean,
58
63
  pathFromRoot: string,
59
64
  parents: Set<any>,
60
65
  processedObjects: Set<any>,
@@ -87,11 +92,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
87
92
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
88
93
  parents.add(obj);
89
94
  processedObjects.add(obj);
95
+ const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
90
96
 
91
97
  if ($Ref.isAllowed$Ref(obj, options)) {
92
98
  dereferenced = dereference$Ref(
93
99
  obj,
94
100
  path,
101
+ currentScopeBase,
102
+ dynamicIdScope,
95
103
  pathFromRoot,
96
104
  parents,
97
105
  processedObjects,
@@ -118,9 +126,12 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
118
126
  let circular;
119
127
 
120
128
  if ($Ref.isAllowed$Ref(value, options)) {
129
+ const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
121
130
  dereferenced = dereference$Ref(
122
131
  value,
123
132
  keyPath,
133
+ valueScopeBase,
134
+ dynamicIdScope,
124
135
  keyPathFromRoot,
125
136
  parents,
126
137
  processedObjects,
@@ -146,7 +157,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
146
157
  }
147
158
  }
148
159
 
149
- obj[key] = dereferenced.value;
160
+ // Clone the dereferenced value if cloneReferences is enabled and this is not a
161
+ // circular reference. This prevents mutations to one location from affecting others.
162
+ let assignedValue = dereferenced.value;
163
+ if (derefOptions?.cloneReferences && !circular && assignedValue && typeof assignedValue === "object") {
164
+ assignedValue = structuredClone(assignedValue);
165
+ }
166
+
167
+ obj[key] = assignedValue;
150
168
 
151
169
  // If we have data to preserve and our dereferenced object is still an object then
152
170
  // we need copy back our preserved data into our dereferenced schema.
@@ -165,6 +183,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
165
183
  dereferenced = crawl(
166
184
  value,
167
185
  keyPath,
186
+ currentScopeBase,
187
+ dynamicIdScope,
168
188
  keyPathFromRoot,
169
189
  parents,
170
190
  processedObjects,
@@ -212,6 +232,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
212
232
  function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
213
233
  $ref: any,
214
234
  path: string,
235
+ scopeBase: string,
236
+ dynamicIdScope: boolean,
215
237
  pathFromRoot: string,
216
238
  parents: Set<any>,
217
239
  processedObjects: any,
@@ -223,7 +245,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
223
245
  ) {
224
246
  const isExternalRef = $Ref.isExternal$Ref($ref);
225
247
  const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
226
- const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
248
+ const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
249
+ const $refPath = url.resolve(resolutionBase, $ref.$ref);
227
250
 
228
251
  const cache = dereferencedCache.get($refPath);
229
252
 
@@ -303,6 +326,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
303
326
  const dereferenced = crawl(
304
327
  dereferencedValue,
305
328
  pointer.path,
329
+ pointer.$ref.path!,
330
+ pointer.$ref.dynamicIdScope,
306
331
  pathFromRoot,
307
332
  parents,
308
333
  processedObjects,
package/lib/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  JSONParserErrorGroup,
20
20
  } from "./util/errors.js";
21
21
  import maybe from "./util/maybe.js";
22
+ import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
22
23
  import type { ParserOptions } from "./options.js";
23
24
  import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
24
25
  import type {
@@ -115,6 +116,8 @@ export class $RefParser<S extends object = JSONSchema, O extends ParserOptions<S
115
116
  const $ref = this.$refs._add(args.path);
116
117
  $ref.value = args.schema;
117
118
  $ref.pathType = pathType;
119
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
120
+ registerSchemaResources(this.$refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
118
121
  promise = Promise.resolve(args.schema);
119
122
  } else {
120
123
  // Parse the schema file/url
package/lib/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
  /**
package/lib/parse.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as url from "./util/url.js";
2
+ import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
2
3
  import { filter, all, sort, run } from "./util/plugins.js";
3
4
  import {
4
5
  ResolverError,
@@ -11,14 +12,24 @@ import type $Refs from "./refs.js";
11
12
  import type { ParserOptions } from "./options.js";
12
13
  import type { FileInfo, JSONSchema } from "./types/index.js";
13
14
 
15
+ interface ParseTarget {
16
+ url: string;
17
+ reference?: string;
18
+ baseUrl?: string;
19
+ }
20
+
14
21
  /**
15
22
  * Reads and parses the specified file path or URL.
16
23
  */
17
24
  async function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
18
- path: string,
25
+ target: string | ParseTarget,
19
26
  $refs: $Refs<S, O>,
20
27
  options: O,
21
28
  ) {
29
+ let path = typeof target === "string" ? target : target.url;
30
+ const baseUrl = typeof target === "string" ? undefined : target.baseUrl;
31
+ let reference = typeof target === "string" ? undefined : target.reference;
32
+
22
33
  // Remove the URL fragment, if any
23
34
  const hashIndex = path.indexOf("#");
24
35
  let hash = "";
@@ -27,6 +38,12 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
27
38
  // Remove the URL fragment, if any
28
39
  path = path.substring(0, hashIndex);
29
40
  }
41
+ if (reference) {
42
+ const referenceHashIndex = reference.indexOf("#");
43
+ if (referenceHashIndex >= 0) {
44
+ reference = reference.substring(0, referenceHashIndex);
45
+ }
46
+ }
30
47
 
31
48
  // Add a new $Ref for this file, even though we don't have the value yet.
32
49
  // This ensures that we don't simultaneously read & parse the same file multiple times
@@ -37,6 +54,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
37
54
  url: path,
38
55
  hash,
39
56
  extension: url.getExtension(path),
57
+ ...(reference !== undefined ? { reference } : {}),
58
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
40
59
  } as FileInfo;
41
60
 
42
61
  // Read the file and then parse the data
@@ -47,6 +66,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
47
66
 
48
67
  const parser = await parseFile<S, O>(file, options, $refs);
49
68
  $ref.value = parser.result;
69
+ $ref.dynamicIdScope = usesDynamicIdScope($ref.value);
70
+ registerSchemaResources($refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
50
71
 
51
72
  return parser.result;
52
73
  } catch (err) {