Skip to content

Commit cda4466

Browse files
committed
Update bundle crawl scope handling
1 parent 49d77f6 commit cda4466

File tree

13 files changed

+312
-24
lines changed

13 files changed

+312
-24
lines changed

lib/bundle.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import $Ref from "./ref.js";
22
import Pointer from "./pointer.js";
33
import * as url from "./util/url.js";
4+
import { getSchemaBasePath } from "./util/schema-resources.js";
45
import type $Refs from "./refs.js";
56
import type $RefParser from "./index.js";
67
import type { ParserOptions } from "./index.js";
@@ -37,7 +38,18 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
3738

3839
// Build an inventory of all $ref pointers in the JSON Schema
3940
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+
);
4153

4254
// Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
4355
const rootId =
@@ -71,6 +83,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
7183
parent: object | $RefParser<S, O>,
7284
key: string | null,
7385
path: string,
86+
scopeBase: string,
87+
dynamicIdScope: boolean,
7488
pathFromRoot: string,
7589
indirections: number,
7690
inventory: InventoryEntry[],
@@ -82,8 +96,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
8296
const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
8397

8498
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
99+
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
85100
if ($Ref.isAllowed$Ref(obj)) {
86-
inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
101+
inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
87102
} else {
88103
// Crawl the object in a specific order that's optimized for bundling.
89104
// This is important because it determines how `pathFromRoot` gets built,
@@ -108,9 +123,21 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
108123
const value = obj[key];
109124

110125
if ($Ref.isAllowed$Ref(value)) {
111-
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+
);
112139
} else {
113-
crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
140+
crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
114141
}
115142

116143
// We need to ensure that we have an object to work with here because we may be crawling
@@ -142,14 +169,16 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
142169
$refParent: any,
143170
$refKey: string | null,
144171
path: string,
172+
scopeBase: string,
173+
dynamicIdScope: boolean,
145174
pathFromRoot: string,
146175
indirections: number,
147176
inventory: InventoryEntry[],
148177
$refs: $Refs<S, O>,
149178
options: O,
150179
) {
151180
const $ref = $refKey === null ? $refParent : $refParent[$refKey];
152-
const $refPath = url.resolve(path, $ref.$ref);
181+
const $refPath = url.resolve(dynamicIdScope ? scopeBase : path, $ref.$ref);
153182
const pointer = $refs._resolve($refPath, pathFromRoot, options);
154183
if (pointer === null) {
155184
return;
@@ -158,7 +187,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
158187
const depth = parsed.length;
159188
const file = url.stripHash(pointer.path);
160189
const hash = url.getHash(pointer.path);
161-
const external = file !== $refs._root$Ref.path;
190+
const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
162191
const extended = $Ref.isExtended$Ref($ref);
163192
indirections += pointer.indirections;
164193

@@ -189,7 +218,18 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
189218

190219
// Recursively crawl the resolved value
191220
if (!existingEntry || external) {
192-
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+
);
193233
}
194234
}
195235

lib/dereference.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import $Ref from "./ref.js";
22
import Pointer from "./pointer.js";
33
import * as url from "./util/url.js";
4+
import { getSchemaBasePath } from "./util/schema-resources.js";
45
import type $Refs from "./refs.js";
56
import type { DereferenceOptions, ParserOptions } from "./options.js";
67
import { type $RefParser, type JSONSchema } from "./index.js";
@@ -24,6 +25,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
2425
const dereferenced = crawl<S, O>(
2526
parser.schema,
2627
parser.$refs._root$Ref.path!,
28+
parser.$refs._root$Ref.path!,
29+
parser.$refs._root$Ref.dynamicIdScope,
2730
"#",
2831
new Set(),
2932
new Set(),
@@ -55,6 +58,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
5558
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
5659
obj: any,
5760
path: string,
61+
scopeBase: string,
62+
dynamicIdScope: boolean,
5863
pathFromRoot: string,
5964
parents: Set<any>,
6065
processedObjects: Set<any>,
@@ -87,11 +92,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
8792
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
8893
parents.add(obj);
8994
processedObjects.add(obj);
95+
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
9096

9197
if ($Ref.isAllowed$Ref(obj, options)) {
9298
dereferenced = dereference$Ref(
9399
obj,
94100
path,
101+
currentScopeBase,
102+
dynamicIdScope,
95103
pathFromRoot,
96104
parents,
97105
processedObjects,
@@ -118,9 +126,12 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
118126
let circular;
119127

120128
if ($Ref.isAllowed$Ref(value, options)) {
129+
const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
121130
dereferenced = dereference$Ref(
122131
value,
123132
keyPath,
133+
valueScopeBase,
134+
dynamicIdScope,
124135
keyPathFromRoot,
125136
parents,
126137
processedObjects,
@@ -172,6 +183,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
172183
dereferenced = crawl(
173184
value,
174185
keyPath,
186+
currentScopeBase,
187+
dynamicIdScope,
175188
keyPathFromRoot,
176189
parents,
177190
processedObjects,
@@ -219,6 +232,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
219232
function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
220233
$ref: any,
221234
path: string,
235+
scopeBase: string,
236+
dynamicIdScope: boolean,
222237
pathFromRoot: string,
223238
parents: Set<any>,
224239
processedObjects: any,
@@ -230,7 +245,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
230245
) {
231246
const isExternalRef = $Ref.isExternal$Ref($ref);
232247
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
233-
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);
234250

235251
const cache = dereferencedCache.get($refPath);
236252

@@ -310,6 +326,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
310326
const dereferenced = crawl(
311327
dereferencedValue,
312328
pointer.path,
329+
pointer.$ref.path!,
330+
pointer.$ref.dynamicIdScope,
313331
pathFromRoot,
314332
parents,
315333
processedObjects,

lib/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
JSONParserErrorGroup,
2020
} from "./util/errors.js";
2121
import maybe from "./util/maybe.js";
22+
import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
2223
import type { ParserOptions } from "./options.js";
2324
import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
2425
import type {
@@ -115,6 +116,8 @@ export class $RefParser<S extends object = JSONSchema, O extends ParserOptions<S
115116
const $ref = this.$refs._add(args.path);
116117
$ref.value = args.schema;
117118
$ref.pathType = pathType;
119+
$ref.dynamicIdScope = usesDynamicIdScope($ref.value);
120+
registerSchemaResources(this.$refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
118121
promise = Promise.resolve(args.schema);
119122
} else {
120123
// Parse the schema file/url

lib/parse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as url from "./util/url.js";
2+
import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
23
import { filter, all, sort, run } from "./util/plugins.js";
34
import {
45
ResolverError,
@@ -47,6 +48,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
4748

4849
const parser = await parseFile<S, O>(file, options, $refs);
4950
$ref.value = parser.result;
51+
$ref.dynamicIdScope = usesDynamicIdScope($ref.value);
52+
registerSchemaResources($refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
5053

5154
return parser.result;
5255
} catch (err) {

lib/pointer.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ParserOptions } from "./options.js";
33
import $Ref from "./ref.js";
44
import * as url from "./util/url.js";
55
import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js";
6+
import { getSchemaBasePath } from "./util/schema-resources.js";
67
import type { JSONSchema } from "./index.js";
78
import type { JSONSchema4Type, JSONSchema6Type, JSONSchema7Type } from "json-schema";
89

@@ -38,6 +39,11 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
3839
*/
3940
originalPath: string;
4041

42+
/**
43+
* The current base URI used to resolve nested $ref pointers while walking this pointer.
44+
*/
45+
scopeBase: string;
46+
4147
/**
4248
* The value of the JSON pointer.
4349
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
@@ -61,6 +67,8 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
6167

6268
this.originalPath = friendlyPath || path;
6369

70+
this.scopeBase = $ref.path || url.stripHash(path);
71+
6472
this.value = undefined;
6573

6674
this.circular = false;
@@ -87,6 +95,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
8795

8896
// Crawl the object, one token at a time
8997
this.value = unwrapOrThrow(obj);
98+
if (this.$ref.dynamicIdScope) {
99+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
100+
}
90101

91102
for (let i = 0; i < tokens.length; i++) {
92103
// During token walking, if the current value is an extended $ref (has sibling keys
@@ -149,10 +160,14 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
149160
}
150161

151162
found.push(token);
163+
if (this.$ref.dynamicIdScope) {
164+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
165+
}
152166
}
153167

154168
// Resolve the final value
155-
if (!this.value || (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)) {
169+
const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
170+
if (!this.value || (this.value.$ref && url.resolve(finalResolutionBase, this.value.$ref) !== pathFromRoot)) {
156171
resolveIf$Ref(this, options, pathFromRoot);
157172
}
158173

@@ -181,6 +196,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
181196

182197
// Crawl the object, one token at a time
183198
this.value = unwrapOrThrow(obj);
199+
if (this.$ref.dynamicIdScope) {
200+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
201+
}
184202

185203
for (let i = 0; i < tokens.length - 1; i++) {
186204
resolveIf$Ref(this, options);
@@ -193,6 +211,10 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
193211
// The token doesn't exist, so create it
194212
this.value = setValue(this, token, {});
195213
}
214+
215+
if (this.$ref.dynamicIdScope) {
216+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
217+
}
196218
}
197219

198220
// Set the value of the final token
@@ -287,7 +309,8 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
287309
// Is the value a JSON reference? (and allowed?)
288310

289311
if ($Ref.isAllowed$Ref(pointer.value, options)) {
290-
const $refPath = url.resolve(pointer.path, pointer.value.$ref);
312+
const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
313+
const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
291314

292315
if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
293316
// The value is a reference to itself, so there's nothing to do.
@@ -304,12 +327,18 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
304327
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
305328
// So the resolved path does NOT change. Just the value does.
306329
pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
330+
if (pointer.$ref.dynamicIdScope) {
331+
pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
332+
}
307333
return false;
308334
} else {
309335
// Resolve the reference
310336
pointer.$ref = resolved.$ref;
311337
pointer.path = resolved.path;
312338
pointer.value = resolved.value;
339+
pointer.scopeBase = pointer.$ref.dynamicIdScope
340+
? getSchemaBasePath(pointer.$ref.path!, pointer.value)
341+
: pointer.$ref.path!;
313342
}
314343

315344
return true;

lib/ref.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
4747
*/
4848
pathType: string | unknown;
4949

50+
/**
51+
* Whether this document/resource should use JSON Schema 2019-09+ nested $id scope semantics.
52+
*/
53+
dynamicIdScope = false;
54+
5055
/**
5156
* List of all errors. Undefined if no errors.
5257
*/

0 commit comments

Comments
 (0)