@apidevtools/json-schema-ref-parser 15.3.3 → 15.3.5

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.
@@ -12,9 +12,12 @@ import { getSchemaBasePath } from "./util/schema-resources.js";
12
12
  */
13
13
  function bundle(parser, options) {
14
14
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
15
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
16
+ ? getSchemaBasePath(parser.$refs._root$Ref.path, parser.schema)
17
+ : parser.$refs._root$Ref.path;
15
18
  // Build an inventory of all $ref pointers in the JSON Schema
16
19
  const inventory = [];
17
- crawl(parser, "schema", parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, "#", 0, inventory, parser.$refs, options);
20
+ crawl(parser, "schema", parser.$refs._root$Ref.path + "#", rootScopeBase, parser.$refs._root$Ref.dynamicIdScope, "#", 0, inventory, parser.$refs, options);
18
21
  // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
19
22
  const rootId = parser.schema && typeof parser.schema === "object" && "$id" in parser.schema
20
23
  ? parser.schema.$id
@@ -44,8 +47,8 @@ function crawl(parent, key, path, scopeBase, dynamicIdScope, pathFromRoot, indir
44
47
  const bundleOptions = (options.bundle || {});
45
48
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
46
49
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
47
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
48
- if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
50
+ const currentScopeBase = scopeBase;
51
+ if ($Ref.isAllowed$Ref(obj)) {
49
52
  inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
50
53
  }
51
54
  else {
@@ -71,12 +74,14 @@ function crawl(parent, key, path, scopeBase, dynamicIdScope, pathFromRoot, indir
71
74
  const keyPath = Pointer.join(path, key);
72
75
  const keyPathFromRoot = Pointer.join(pathFromRoot, key);
73
76
  const value = obj[key];
74
- if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
75
- const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
76
- inventory$Ref(obj, key, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
77
+ const childScopeBase = dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
78
+ ? getSchemaBasePath(currentScopeBase, value)
79
+ : currentScopeBase;
80
+ if ($Ref.isAllowed$Ref(value)) {
81
+ inventory$Ref(obj, key, keyPath, childScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
77
82
  }
78
83
  else {
79
- crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
84
+ crawl(obj, key, keyPath, childScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
80
85
  }
81
86
  // We need to ensure that we have an object to work with here because we may be crawling
82
87
  // an `examples` schema and `value` may be nullish.
@@ -13,8 +13,11 @@ export default dereference;
13
13
  */
14
14
  function dereference(parser, options) {
15
15
  const start = Date.now();
16
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
17
+ ? getSchemaBasePath(parser.$refs._root$Ref.path, parser.schema)
18
+ : parser.$refs._root$Ref.path;
16
19
  // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
17
- const dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, "#", new Set(), new Set(), new Map(), parser.$refs, options, start, 0);
20
+ const dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, rootScopeBase, parser.$refs._root$Ref.dynamicIdScope, "#", new Set(), new Set(), new Map(), parser.$refs, options, start, 0);
18
21
  parser.$refs.circular = dereferenced.circular;
19
22
  parser.schema = dereferenced.value;
20
23
  }
@@ -52,8 +55,8 @@ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, proc
52
55
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
53
56
  parents.add(obj);
54
57
  processedObjects.add(obj);
55
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
56
- if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
58
+ const currentScopeBase = scopeBase;
59
+ if ($Ref.isAllowed$Ref(obj, options)) {
57
60
  dereferenced = dereference$Ref(obj, path, currentScopeBase, dynamicIdScope, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
58
61
  result.circular = dereferenced.circular;
59
62
  result.value = dereferenced.value;
@@ -67,10 +70,12 @@ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, proc
67
70
  continue;
68
71
  }
69
72
  const value = obj[key];
73
+ const childScopeBase = dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
74
+ ? getSchemaBasePath(currentScopeBase, value)
75
+ : currentScopeBase;
70
76
  let circular;
71
- if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
72
- const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
73
- dereferenced = dereference$Ref(value, keyPath, valueScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
77
+ if ($Ref.isAllowed$Ref(value, options)) {
78
+ dereferenced = dereference$Ref(value, keyPath, childScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth);
74
79
  circular = dereferenced.circular;
75
80
  // Avoid pointless mutations; breaks frozen objects to no profit
76
81
  if (obj[key] !== dereferenced.value) {
@@ -107,7 +112,7 @@ function crawl(obj, path, scopeBase, dynamicIdScope, pathFromRoot, parents, proc
107
112
  }
108
113
  else {
109
114
  if (!parents.has(value)) {
110
- dereferenced = crawl(value, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
115
+ dereferenced = crawl(value, keyPath, childScopeBase, dynamicIdScope, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options, startTime, depth + 1);
111
116
  circular = dereferenced.circular;
112
117
  // Avoid pointless mutations; breaks frozen objects to no profit
113
118
  if (obj[key] !== dereferenced.value) {
@@ -153,10 +158,18 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
153
158
  // If the cached object however is _not_ circular and there are additional keys alongside our
154
159
  // `$ref` pointer here we should merge them back in and return that.
155
160
  if (!cache.circular) {
156
- if (Object.keys($ref).length > 1) {
161
+ const refKeys = Object.keys($ref);
162
+ if (refKeys.length > 1) {
163
+ const extraKeys = {};
164
+ for (const key of refKeys) {
165
+ if (key !== "$ref" && !(key in cache.value)) {
166
+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
167
+ extraKeys[key] = $ref[key];
168
+ }
169
+ }
157
170
  return {
158
171
  circular: cache.circular,
159
- value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
172
+ value: Object.assign({}, cache.value, extraKeys),
160
173
  };
161
174
  }
162
175
  return cache;
@@ -202,7 +215,7 @@ function dereference$Ref($ref, path, scopeBase, dynamicIdScope, pathFromRoot, pa
202
215
  foundCircularReference(path, $refs, options);
203
216
  }
204
217
  // Dereference the JSON reference
205
- let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
218
+ let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
206
219
  // Crawl the dereferenced value (unless it's circular)
207
220
  if (!circular) {
208
221
  // Determine if the dereferenced value is circular
@@ -74,7 +74,7 @@ class Pointer {
74
74
  const found = [];
75
75
  // Crawl the object, one token at a time
76
76
  this.value = unwrapOrThrow(obj);
77
- if (this.$ref.dynamicIdScope) {
77
+ if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
78
78
  this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
79
79
  }
80
80
  for (let i = 0; i < tokens.length; i++) {
@@ -163,7 +163,7 @@ class Pointer {
163
163
  }
164
164
  // Crawl the object, one token at a time
165
165
  this.value = unwrapOrThrow(obj);
166
- if (this.$ref.dynamicIdScope) {
166
+ if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
167
167
  this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
168
168
  }
169
169
  for (let i = 0; i < tokens.length - 1; i++) {
@@ -256,7 +256,7 @@ class Pointer {
256
256
  */
257
257
  function resolveIf$Ref(pointer, options, pathFromRoot) {
258
258
  // Is the value a JSON reference? (and allowed?)
259
- if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
259
+ if ($Ref.isAllowed$Ref(pointer.value, options)) {
260
260
  const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
261
261
  const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
262
262
  if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
@@ -272,10 +272,7 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
272
272
  if ($Ref.isExtended$Ref(pointer.value)) {
273
273
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
274
274
  // So the resolved path does NOT change. Just the value does.
275
- pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
276
- if (pointer.$ref.dynamicIdScope) {
277
- pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
278
- }
275
+ pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
279
276
  return false;
280
277
  }
281
278
  else {
@@ -283,9 +280,10 @@ function resolveIf$Ref(pointer, options, pathFromRoot) {
283
280
  pointer.$ref = resolved.$ref;
284
281
  pointer.path = resolved.path;
285
282
  pointer.value = resolved.value;
286
- pointer.scopeBase = pointer.$ref.dynamicIdScope
287
- ? getSchemaBasePath(pointer.$ref.path, pointer.value)
288
- : pointer.$ref.path;
283
+ // `pointer.$ref.path` is already the canonical location of the resolved resource.
284
+ // Re-applying the resource's own `$id` here would duplicate nested path segments
285
+ // such as `nested/nested/foo.json`.
286
+ pointer.scopeBase = pointer.$ref.path;
289
287
  }
290
288
  return true;
291
289
  }
@@ -327,3 +325,6 @@ function unwrapOrThrow(value) {
327
325
  function isRootPath(pathFromRoot) {
328
326
  return typeof pathFromRoot == "string" && Pointer.parse(pathFromRoot).length == 0;
329
327
  }
328
+ function isAliasedResource($ref) {
329
+ return Boolean($ref.path && $ref.path in $ref.$refs._aliases);
330
+ }
package/dist/lib/ref.d.ts CHANGED
@@ -114,7 +114,7 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
114
114
  * @param options
115
115
  * @returns
116
116
  */
117
- static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>, allowPlainNameFragments?: boolean): true | undefined;
117
+ static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>): true | undefined;
118
118
  /**
119
119
  * Determines whether the given value is a JSON reference that "extends" its resolved value.
120
120
  * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
@@ -182,6 +182,6 @@ declare class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = P
182
182
  * @param options - The options
183
183
  * @returns - Returns the dereferenced value
184
184
  */
185
- static dereference<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($ref: $Ref<S, O>, resolvedValue: S, options?: O, useSpecCompliantRefSiblings?: boolean): S;
185
+ static dereference<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($ref: $Ref<S, O>, resolvedValue: S, options?: O): S;
186
186
  }
187
187
  export default $Ref;
package/dist/lib/ref.js CHANGED
@@ -170,16 +170,12 @@ class $Ref {
170
170
  * @param options
171
171
  * @returns
172
172
  */
173
- static isAllowed$Ref(value, options, allowPlainNameFragments = false) {
173
+ static isAllowed$Ref(value, options) {
174
174
  if (this.is$Ref(value)) {
175
175
  if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
176
176
  // It's a JSON Pointer reference, which is always allowed
177
177
  return true;
178
178
  }
179
- else if (allowPlainNameFragments && value.$ref[0] === "#") {
180
- // JSON Schema 2019-09+ allows plain-name fragments declared via $anchor/$dynamicAnchor
181
- return true;
182
- }
183
179
  else if (value.$ref[0] !== "#" && (!options || options.resolve?.external)) {
184
180
  // It's an external reference, which is allowed by the options
185
181
  return true;
@@ -256,10 +252,7 @@ class $Ref {
256
252
  * @param options - The options
257
253
  * @returns - Returns the dereferenced value
258
254
  */
259
- static dereference($ref, resolvedValue, options, useSpecCompliantRefSiblings = false) {
260
- if ($Ref.isExtended$Ref($ref) && useSpecCompliantRefSiblings) {
261
- return preserveRefSiblings($ref, resolvedValue);
262
- }
255
+ static dereference($ref, resolvedValue, options) {
263
256
  if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
264
257
  const merged = {};
265
258
  for (const key of Object.keys($ref)) {
@@ -294,24 +287,6 @@ class $Ref {
294
287
  }
295
288
  }
296
289
  }
297
- function preserveRefSiblings(refValue, resolvedValue) {
298
- const preserved = {};
299
- const existingAllOf = "allOf" in refValue ? refValue.allOf : undefined;
300
- for (const key of Object.keys(refValue)) {
301
- if (key !== "$ref" && key !== "allOf") {
302
- preserved[key] = refValue[key];
303
- }
304
- }
305
- const allOf = [resolvedValue];
306
- if (Array.isArray(existingAllOf)) {
307
- allOf.push(...existingAllOf);
308
- }
309
- else if (existingAllOf !== undefined) {
310
- allOf.push(existingAllOf);
311
- }
312
- preserved.allOf = allOf;
313
- return preserved;
314
- }
315
290
  function deepMerge(target, source) {
316
291
  //return {...target, ...source};
317
292
  // If either isn't an object, just return source (overwrite)
@@ -80,7 +80,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
80
80
  */
81
81
  _add(path: string): $Ref<S, O>;
82
82
  _addAlias(path: string, value: S, pathType?: string | unknown, dynamicIdScope?: boolean): $Ref<S, O>;
83
- _addExactAlias(path: string, targetPath: string): string;
84
83
  /**
85
84
  * Resolves the given JSON reference.
86
85
  *
@@ -99,7 +98,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
99
98
  */
100
99
  _$refs: $RefsMap<S, O>;
101
100
  _aliases: $RefsMap<S, O>;
102
- _exactAliases: Record<string, string>;
103
101
  /**
104
102
  * The {@link $Ref} object that is the root of the JSON schema.
105
103
  *
package/dist/lib/refs.js CHANGED
@@ -99,7 +99,7 @@ export default class $Refs {
99
99
  */
100
100
  _get$Ref(path) {
101
101
  path = url.resolve(this._root$Ref.path, path);
102
- return this._getRef(this._exactAliases[path] || path);
102
+ return this._getRef(path);
103
103
  }
104
104
  /**
105
105
  * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object.
@@ -127,13 +127,6 @@ export default class $Refs {
127
127
  this._aliases[withoutHash] = $ref;
128
128
  return $ref;
129
129
  }
130
- _addExactAlias(path, targetPath) {
131
- if (!path || this._exactAliases[path]) {
132
- return this._exactAliases[path];
133
- }
134
- this._exactAliases[path] = targetPath;
135
- return targetPath;
136
- }
137
130
  /**
138
131
  * Resolves the given JSON reference.
139
132
  *
@@ -145,12 +138,11 @@ export default class $Refs {
145
138
  */
146
139
  _resolve(path, pathFromRoot, options) {
147
140
  const absPath = url.resolve(this._root$Ref.path, path);
148
- const canonicalPath = this._exactAliases[absPath] || absPath;
149
- const $ref = this._getRef(canonicalPath);
141
+ const $ref = this._getRef(absPath);
150
142
  if (!$ref) {
151
143
  throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
152
144
  }
153
- return $ref.resolve(canonicalPath, options, path, pathFromRoot);
145
+ return $ref.resolve(absPath, options, path, pathFromRoot);
154
146
  }
155
147
  /**
156
148
  * A map of paths/urls to {@link $Ref} objects
@@ -160,7 +152,6 @@ export default class $Refs {
160
152
  */
161
153
  _$refs = {};
162
154
  _aliases = {};
163
- _exactAliases = {};
164
155
  /**
165
156
  * The {@link $Ref} object that is the root of the JSON schema.
166
157
  *
@@ -177,7 +168,6 @@ export default class $Refs {
177
168
  this.circular = false;
178
169
  this._$refs = {};
179
170
  this._aliases = {};
180
- this._exactAliases = {};
181
171
  // @ts-ignore
182
172
  this._root$Ref = null;
183
173
  }
@@ -20,8 +20,11 @@ function resolveExternal(parser, options) {
20
20
  return Promise.resolve();
21
21
  }
22
22
  try {
23
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
24
+ ? getSchemaBasePath(parser.$refs._root$Ref.path, parser.schema)
25
+ : parser.$refs._root$Ref.path;
23
26
  // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
24
- const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, parser.$refs, options);
27
+ const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", rootScopeBase, parser.$refs._root$Ref.dynamicIdScope, parser.$refs, options);
25
28
  return Promise.all(promises);
26
29
  }
27
30
  catch (e) {
@@ -49,7 +52,7 @@ function crawl(obj, path, scopeBase, dynamicIdScope, $refs, options, seen, exter
49
52
  let promises = [];
50
53
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
51
54
  seen.add(obj); // Track previously seen objects to avoid infinite recursion
52
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
55
+ const currentScopeBase = scopeBase;
53
56
  if ($Ref.isExternal$Ref(obj)) {
54
57
  promises.push(resolve$Ref(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
55
58
  }
@@ -57,7 +60,9 @@ function crawl(obj, path, scopeBase, dynamicIdScope, $refs, options, seen, exter
57
60
  for (const key of keys) {
58
61
  const keyPath = Pointer.join(path, key);
59
62
  const value = obj[key];
60
- const childScopeBase = dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
63
+ const childScopeBase = dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
64
+ ? getSchemaBasePath(currentScopeBase, value)
65
+ : currentScopeBase;
61
66
  promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
62
67
  }
63
68
  }
@@ -20,22 +20,20 @@ export function registerSchemaResources($refs, basePath, value, pathType, dynami
20
20
  return;
21
21
  }
22
22
  const seen = new Set();
23
- const visit = (node, scopeBase, pointerTokens) => {
23
+ const visit = (node, scopeBase) => {
24
24
  if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
25
25
  return;
26
26
  }
27
27
  seen.add(node);
28
28
  const nextScopeBase = getSchemaBasePath(scopeBase, node);
29
- const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
30
29
  if (nextScopeBase !== scopeBase) {
31
30
  $refs._addAlias(nextScopeBase, node, pathType, dynamicIdScope);
32
31
  }
33
- registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
34
32
  for (const key of Object.keys(node)) {
35
- visit(node[key], nextScopeBase, [...resourcePointerTokens, key]);
33
+ visit(node[key], nextScopeBase);
36
34
  }
37
35
  };
38
- visit(value, basePath, []);
36
+ visit(value, basePath);
39
37
  }
40
38
  function getSchemaId(value) {
41
39
  if (value &&
@@ -47,26 +45,3 @@ function getSchemaId(value) {
47
45
  }
48
46
  return undefined;
49
47
  }
50
- function registerAnchorAliases($refs, scopeBase, pointerTokens, value) {
51
- if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
52
- return;
53
- }
54
- const resourceBase = url.stripHash(scopeBase);
55
- const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
56
- const anchors = [
57
- value.$anchor,
58
- value.$dynamicAnchor,
59
- ];
60
- for (const anchor of anchors) {
61
- if (typeof anchor === "string" && anchor.length > 0) {
62
- $refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
63
- }
64
- }
65
- }
66
- function joinPointerPath(basePath, tokens) {
67
- let path = `${basePath}#`;
68
- for (const token of tokens) {
69
- path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
70
- }
71
- return path;
72
- }
@@ -4,7 +4,7 @@ const protocolPattern = /^(\w{2,}):\/\//i;
4
4
  const jsonPointerSlash = /~1/g;
5
5
  const jsonPointerTilde = /~0/g;
6
6
  import { isWindows } from "./is-windows.js";
7
- const isAbsoluteWin32Path = /^[a-zA-Z]:\\/;
7
+ const isAbsoluteWin32Path = /^[a-zA-Z]:[\\/]/;
8
8
  // RegExp patterns to URL-encode special characters in local filesystem paths
9
9
  const urlEncodePatterns = [
10
10
  [/\?/g, "%3F"],
@@ -381,6 +381,9 @@ export function fromFileSystemPath(path) {
381
381
  * Converts a URL to a local filesystem path.
382
382
  */
383
383
  export function toFileSystemPath(path, keepFileProtocol) {
384
+ // Bare "%" characters are valid in filesystem paths, but they make `decodeURI` throw.
385
+ // Escape only the non-encoded ones so percent-encoded sequences still decode normally.
386
+ path = path.replace(/%(?![0-9A-Fa-f]{2})/g, "%25");
384
387
  // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
385
388
  path = decodeURI(path);
386
389
  // Step 2: Manually decode characters that are not decoded by `decodeURI`.
package/lib/bundle.ts CHANGED
@@ -35,6 +35,9 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
35
35
  options: O,
36
36
  ) {
37
37
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
38
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
39
+ ? getSchemaBasePath(parser.$refs._root$Ref.path!, parser.schema)
40
+ : parser.$refs._root$Ref.path!;
38
41
 
39
42
  // Build an inventory of all $ref pointers in the JSON Schema
40
43
  const inventory: InventoryEntry[] = [];
@@ -42,7 +45,7 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
42
45
  parser,
43
46
  "schema",
44
47
  parser.$refs._root$Ref.path + "#",
45
- parser.$refs._root$Ref.path!,
48
+ rootScopeBase,
46
49
  parser.$refs._root$Ref.dynamicIdScope,
47
50
  "#",
48
51
  0,
@@ -96,8 +99,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
96
99
  const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
97
100
 
98
101
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
99
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
100
- if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
102
+ const currentScopeBase = scopeBase;
103
+ if ($Ref.isAllowed$Ref(obj)) {
101
104
  inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
102
105
  } else {
103
106
  // Crawl the object in a specific order that's optimized for bundling.
@@ -121,14 +124,17 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
121
124
  const keyPath = Pointer.join(path, key);
122
125
  const keyPathFromRoot = Pointer.join(pathFromRoot, key);
123
126
  const value = obj[key];
127
+ const childScopeBase =
128
+ dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
129
+ ? getSchemaBasePath(currentScopeBase, value)
130
+ : currentScopeBase;
124
131
 
125
- if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
126
- const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
132
+ if ($Ref.isAllowed$Ref(value)) {
127
133
  inventory$Ref(
128
134
  obj,
129
135
  key,
130
136
  keyPath,
131
- valueScopeBase,
137
+ childScopeBase,
132
138
  dynamicIdScope,
133
139
  keyPathFromRoot,
134
140
  indirections,
@@ -137,7 +143,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
137
143
  options,
138
144
  );
139
145
  } else {
140
- crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
146
+ crawl(obj, key, keyPath, childScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
141
147
  }
142
148
 
143
149
  // We need to ensure that we have an object to work with here because we may be crawling
@@ -21,11 +21,14 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
21
21
  options: O,
22
22
  ) {
23
23
  const start = Date.now();
24
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
25
+ ? getSchemaBasePath(parser.$refs._root$Ref.path!, parser.schema)
26
+ : parser.$refs._root$Ref.path!;
24
27
  // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
25
28
  const dereferenced = crawl<S, O>(
26
29
  parser.schema,
27
30
  parser.$refs._root$Ref.path!,
28
- parser.$refs._root$Ref.path!,
31
+ rootScopeBase,
29
32
  parser.$refs._root$Ref.dynamicIdScope,
30
33
  "#",
31
34
  new Set(),
@@ -92,9 +95,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
92
95
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
93
96
  parents.add(obj);
94
97
  processedObjects.add(obj);
95
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
98
+ const currentScopeBase = scopeBase;
96
99
 
97
- if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
100
+ if ($Ref.isAllowed$Ref(obj, options)) {
98
101
  dereferenced = dereference$Ref(
99
102
  obj,
100
103
  path,
@@ -123,14 +126,17 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
123
126
  }
124
127
 
125
128
  const value = obj[key];
129
+ const childScopeBase =
130
+ dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
131
+ ? getSchemaBasePath(currentScopeBase, value)
132
+ : currentScopeBase;
126
133
  let circular;
127
134
 
128
- if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
129
- const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
135
+ if ($Ref.isAllowed$Ref(value, options)) {
130
136
  dereferenced = dereference$Ref(
131
137
  value,
132
138
  keyPath,
133
- valueScopeBase,
139
+ childScopeBase,
134
140
  dynamicIdScope,
135
141
  keyPathFromRoot,
136
142
  parents,
@@ -183,7 +189,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
183
189
  dereferenced = crawl(
184
190
  value,
185
191
  keyPath,
186
- currentScopeBase,
192
+ childScopeBase,
187
193
  dynamicIdScope,
188
194
  keyPathFromRoot,
189
195
  parents,
@@ -257,10 +263,18 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
257
263
  // If the cached object however is _not_ circular and there are additional keys alongside our
258
264
  // `$ref` pointer here we should merge them back in and return that.
259
265
  if (!cache.circular) {
260
- if (Object.keys($ref).length > 1) {
266
+ const refKeys = Object.keys($ref);
267
+ if (refKeys.length > 1) {
268
+ const extraKeys = {};
269
+ for (const key of refKeys) {
270
+ if (key !== "$ref" && !(key in cache.value)) {
271
+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
272
+ extraKeys[key] = $ref[key];
273
+ }
274
+ }
261
275
  return {
262
276
  circular: cache.circular,
263
- value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
277
+ value: Object.assign({}, cache.value, extraKeys),
264
278
  };
265
279
  }
266
280
 
@@ -310,7 +324,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
310
324
  }
311
325
 
312
326
  // Dereference the JSON reference
313
- let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
327
+ let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
314
328
 
315
329
  // Crawl the dereferenced value (unless it's circular)
316
330
  if (!circular) {
package/lib/pointer.ts CHANGED
@@ -95,7 +95,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
95
95
 
96
96
  // Crawl the object, one token at a time
97
97
  this.value = unwrapOrThrow(obj);
98
- if (this.$ref.dynamicIdScope) {
98
+ if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
99
99
  this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
100
100
  }
101
101
 
@@ -196,7 +196,7 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
196
196
 
197
197
  // Crawl the object, one token at a time
198
198
  this.value = unwrapOrThrow(obj);
199
- if (this.$ref.dynamicIdScope) {
199
+ if (this.$ref.dynamicIdScope && !isAliasedResource(this.$ref)) {
200
200
  this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
201
201
  }
202
202
 
@@ -308,7 +308,7 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
308
308
  ) {
309
309
  // Is the value a JSON reference? (and allowed?)
310
310
 
311
- if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
311
+ if ($Ref.isAllowed$Ref(pointer.value, options)) {
312
312
  const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
313
313
  const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
314
314
 
@@ -326,19 +326,17 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
326
326
  if ($Ref.isExtended$Ref(pointer.value)) {
327
327
  // This JSON reference "extends" the resolved value, rather than simply pointing to it.
328
328
  // So the resolved path does NOT change. Just the value does.
329
- pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
330
- if (pointer.$ref.dynamicIdScope) {
331
- pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
332
- }
329
+ pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
333
330
  return false;
334
331
  } else {
335
332
  // Resolve the reference
336
333
  pointer.$ref = resolved.$ref;
337
334
  pointer.path = resolved.path;
338
335
  pointer.value = resolved.value;
339
- pointer.scopeBase = pointer.$ref.dynamicIdScope
340
- ? getSchemaBasePath(pointer.$ref.path!, pointer.value)
341
- : pointer.$ref.path!;
336
+ // `pointer.$ref.path` is already the canonical location of the resolved resource.
337
+ // Re-applying the resource's own `$id` here would duplicate nested path segments
338
+ // such as `nested/nested/foo.json`.
339
+ pointer.scopeBase = pointer.$ref.path!;
342
340
  }
343
341
 
344
342
  return true;
@@ -385,3 +383,7 @@ function unwrapOrThrow(value: unknown) {
385
383
  function isRootPath(pathFromRoot: string | unknown): boolean {
386
384
  return typeof pathFromRoot == "string" && Pointer.parse(pathFromRoot).length == 0;
387
385
  }
386
+
387
+ function isAliasedResource($ref: $Ref<any, any>) {
388
+ return Boolean($ref.path && $ref.path in $ref.$refs._aliases);
389
+ }
package/lib/ref.ts CHANGED
@@ -199,18 +199,11 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
199
199
  * @param options
200
200
  * @returns
201
201
  */
202
- static isAllowed$Ref<S extends object = JSONSchema>(
203
- value: unknown,
204
- options?: ParserOptions<S>,
205
- allowPlainNameFragments = false,
206
- ) {
202
+ static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>) {
207
203
  if (this.is$Ref(value)) {
208
204
  if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
209
205
  // It's a JSON Pointer reference, which is always allowed
210
206
  return true;
211
- } else if (allowPlainNameFragments && value.$ref[0] === "#") {
212
- // JSON Schema 2019-09+ allows plain-name fragments declared via $anchor/$dynamicAnchor
213
- return true;
214
207
  } else if (value.$ref[0] !== "#" && (!options || options.resolve?.external)) {
215
208
  // It's an external reference, which is allowed by the options
216
209
  return true;
@@ -293,12 +286,7 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
293
286
  $ref: $Ref<S, O>,
294
287
  resolvedValue: S,
295
288
  options?: O,
296
- useSpecCompliantRefSiblings = false,
297
289
  ): S {
298
- if ($Ref.isExtended$Ref($ref) && useSpecCompliantRefSiblings) {
299
- return preserveRefSiblings($ref, resolvedValue) as S;
300
- }
301
-
302
290
  if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
303
291
  const merged = {} as Partial<S>;
304
292
  for (const key of Object.keys($ref)) {
@@ -337,27 +325,6 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
337
325
  }
338
326
  }
339
327
 
340
- function preserveRefSiblings<T>(refValue: Record<string, any>, resolvedValue: T): T {
341
- const preserved = {} as Record<string, any>;
342
- const existingAllOf = "allOf" in refValue ? refValue.allOf : undefined;
343
-
344
- for (const key of Object.keys(refValue)) {
345
- if (key !== "$ref" && key !== "allOf") {
346
- preserved[key] = refValue[key];
347
- }
348
- }
349
-
350
- const allOf = [resolvedValue];
351
- if (Array.isArray(existingAllOf)) {
352
- allOf.push(...existingAllOf);
353
- } else if (existingAllOf !== undefined) {
354
- allOf.push(existingAllOf);
355
- }
356
-
357
- preserved.allOf = allOf;
358
- return preserved as T;
359
- }
360
-
361
328
  function deepMerge<T>(target: Partial<T>, source: Partial<T>): T {
362
329
  //return {...target, ...source};
363
330
 
package/lib/refs.ts CHANGED
@@ -112,7 +112,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
112
112
  */
113
113
  _get$Ref(path: string) {
114
114
  path = url.resolve(this._root$Ref.path!, path);
115
- return this._getRef(this._exactAliases[path] || path);
115
+ return this._getRef(path);
116
116
  }
117
117
 
118
118
  /**
@@ -149,15 +149,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
149
149
  return $ref;
150
150
  }
151
151
 
152
- _addExactAlias(path: string, targetPath: string) {
153
- if (!path || this._exactAliases[path]) {
154
- return this._exactAliases[path];
155
- }
156
-
157
- this._exactAliases[path] = targetPath;
158
- return targetPath;
159
- }
160
-
161
152
  /**
162
153
  * Resolves the given JSON reference.
163
154
  *
@@ -169,14 +160,13 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
169
160
  */
170
161
  _resolve(path: string, pathFromRoot: string, options?: O) {
171
162
  const absPath = url.resolve(this._root$Ref.path!, path);
172
- const canonicalPath = this._exactAliases[absPath] || absPath;
173
- const $ref = this._getRef(canonicalPath);
163
+ const $ref = this._getRef(absPath);
174
164
 
175
165
  if (!$ref) {
176
166
  throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
177
167
  }
178
168
 
179
- return $ref.resolve(canonicalPath, options, path, pathFromRoot);
169
+ return $ref.resolve(absPath, options, path, pathFromRoot);
180
170
  }
181
171
 
182
172
  /**
@@ -189,8 +179,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
189
179
 
190
180
  _aliases: $RefsMap<S, O> = {};
191
181
 
192
- _exactAliases: Record<string, string> = {};
193
-
194
182
  /**
195
183
  * The {@link $Ref} object that is the root of the JSON schema.
196
184
  *
@@ -209,7 +197,6 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
209
197
 
210
198
  this._$refs = {};
211
199
  this._aliases = {};
212
- this._exactAliases = {};
213
200
 
214
201
  // @ts-ignore
215
202
  this._root$Ref = null;
@@ -29,11 +29,14 @@ function resolveExternal<S extends object = JSONSchema, O extends ParserOptions<
29
29
  }
30
30
 
31
31
  try {
32
+ const rootScopeBase = parser.$refs._root$Ref.dynamicIdScope
33
+ ? getSchemaBasePath(parser.$refs._root$Ref.path!, parser.schema)
34
+ : parser.$refs._root$Ref.path!;
32
35
  // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
33
36
  const promises = crawl(
34
37
  parser.schema,
35
38
  parser.$refs._root$Ref.path + "#",
36
- parser.$refs._root$Ref.path!,
39
+ rootScopeBase,
37
40
  parser.$refs._root$Ref.dynamicIdScope,
38
41
  parser.$refs,
39
42
  options,
@@ -75,7 +78,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
75
78
 
76
79
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
77
80
  seen.add(obj); // Track previously seen objects to avoid infinite recursion
78
- const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
81
+ const currentScopeBase = scopeBase;
79
82
  if ($Ref.isExternal$Ref(obj)) {
80
83
  promises.push(resolve$Ref<S, O>(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
81
84
  }
@@ -85,7 +88,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
85
88
  const keyPath = Pointer.join(path, key);
86
89
  const value = obj[key as keyof typeof obj] as string | JSONSchema | Buffer | undefined;
87
90
  const childScopeBase =
88
- dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
91
+ dynamicIdScope && value && typeof value === "object" && !ArrayBuffer.isView(value)
92
+ ? getSchemaBasePath(currentScopeBase, value)
93
+ : currentScopeBase;
89
94
  promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
90
95
  }
91
96
  }
@@ -38,7 +38,7 @@ export function registerSchemaResources<S extends object = JSONSchema, O extends
38
38
 
39
39
  const seen = new Set<object>();
40
40
 
41
- const visit = (node: unknown, scopeBase: string, pointerTokens: string[]) => {
41
+ const visit = (node: unknown, scopeBase: string) => {
42
42
  if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
43
43
  return;
44
44
  }
@@ -46,18 +46,16 @@ export function registerSchemaResources<S extends object = JSONSchema, O extends
46
46
  seen.add(node);
47
47
 
48
48
  const nextScopeBase = getSchemaBasePath(scopeBase, node);
49
- const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
50
49
  if (nextScopeBase !== scopeBase) {
51
50
  $refs._addAlias(nextScopeBase, node as S, pathType, dynamicIdScope);
52
51
  }
53
- registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
54
52
 
55
53
  for (const key of Object.keys(node)) {
56
- visit((node as Record<string, unknown>)[key], nextScopeBase, [...resourcePointerTokens, key]);
54
+ visit((node as Record<string, unknown>)[key], nextScopeBase);
57
55
  }
58
56
  };
59
57
 
60
- visit(value, basePath, []);
58
+ visit(value, basePath);
61
59
  }
62
60
 
63
61
  function getSchemaId(value: unknown): string | undefined {
@@ -73,37 +71,3 @@ function getSchemaId(value: unknown): string | undefined {
73
71
 
74
72
  return undefined;
75
73
  }
76
-
77
- function registerAnchorAliases<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
78
- $refs: $Refs<S, O>,
79
- scopeBase: string,
80
- pointerTokens: string[],
81
- value: unknown,
82
- ) {
83
- if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
84
- return;
85
- }
86
-
87
- const resourceBase = url.stripHash(scopeBase);
88
- const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
89
- const anchors = [
90
- (value as { $anchor?: unknown }).$anchor,
91
- (value as { $dynamicAnchor?: unknown }).$dynamicAnchor,
92
- ];
93
-
94
- for (const anchor of anchors) {
95
- if (typeof anchor === "string" && anchor.length > 0) {
96
- $refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
97
- }
98
- }
99
- }
100
-
101
- function joinPointerPath(basePath: string, tokens: string[]) {
102
- let path = `${basePath}#`;
103
-
104
- for (const token of tokens) {
105
- path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
106
- }
107
-
108
- return path;
109
- }
package/lib/util/url.ts CHANGED
@@ -7,7 +7,7 @@ const jsonPointerTilde = /~0/g;
7
7
 
8
8
  import { isWindows } from "./is-windows.js";
9
9
 
10
- const isAbsoluteWin32Path = /^[a-zA-Z]:\\/;
10
+ const isAbsoluteWin32Path = /^[a-zA-Z]:[\\/]/;
11
11
 
12
12
  // RegExp patterns to URL-encode special characters in local filesystem paths
13
13
  const urlEncodePatterns = [
@@ -426,6 +426,10 @@ export function fromFileSystemPath(path: string) {
426
426
  * Converts a URL to a local filesystem path.
427
427
  */
428
428
  export function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string {
429
+ // Bare "%" characters are valid in filesystem paths, but they make `decodeURI` throw.
430
+ // Escape only the non-encoded ones so percent-encoded sequences still decode normally.
431
+ path = path!.replace(/%(?![0-9A-Fa-f]{2})/g, "%25");
432
+
429
433
  // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
430
434
  path = decodeURI(path!);
431
435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apidevtools/json-schema-ref-parser",
3
- "version": "15.3.3",
3
+ "version": "15.3.5",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "type": "module",
6
6
  "types": "dist/lib/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  "test": "vitest --coverage",
16
16
  "test:specific": "vitest invalid",
17
17
  "test:node": "yarn test",
18
- "test:browser": "cross-env BROWSER=\"true\" yarn test",
18
+ "test:browser": "node ./test/fixtures/run-browser-tests.mjs",
19
19
  "test:update": "vitest -u",
20
20
  "test:watch": "vitest -w"
21
21
  },
@@ -83,7 +83,7 @@
83
83
  "@types/node": "^25",
84
84
  "@typescript-eslint/eslint-plugin": "^8.57.2",
85
85
  "@typescript-eslint/parser": "^8.57.2",
86
- "@vitest/coverage-v8": "^4.1.1",
86
+ "@vitest/coverage-v8": "^4.1.2",
87
87
  "cross-env": "^10.1.0",
88
88
  "eslint": "^10.1.0",
89
89
  "eslint-config-prettier": "^10.1.8",
@@ -97,7 +97,7 @@
97
97
  "rimraf": "^6.1.3",
98
98
  "typescript": "^6.0.2",
99
99
  "typescript-eslint": "^8.57.2",
100
- "vitest": "^4.1.1"
100
+ "vitest": "^4.1.2"
101
101
  },
102
102
  "release": {
103
103
  "branches": [