@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.
- package/dist/lib/bundle.js +65 -16
- package/dist/lib/dereference.js +19 -9
- package/dist/lib/index.js +3 -0
- package/dist/lib/options.d.ts +22 -0
- package/dist/lib/parse.d.ts +6 -1
- package/dist/lib/parse.js +15 -1
- package/dist/lib/pointer.d.ts +4 -0
- package/dist/lib/pointer.js +28 -2
- package/dist/lib/ref.d.ts +4 -0
- package/dist/lib/ref.js +4 -0
- package/dist/lib/refs.d.ts +3 -0
- package/dist/lib/refs.js +24 -8
- package/dist/lib/resolve-external.js +22 -9
- package/dist/lib/types/index.d.ts +11 -0
- package/dist/lib/util/schema-resources.d.ts +6 -0
- package/dist/lib/util/schema-resources.js +47 -0
- package/lib/bundle.ts +101 -13
- package/lib/dereference.ts +27 -2
- package/lib/index.ts +3 -0
- package/lib/options.ts +24 -0
- package/lib/parse.ts +22 -1
- package/lib/pointer.ts +31 -2
- package/lib/ref.ts +5 -0
- package/lib/refs.ts +30 -8
- package/lib/resolve-external.ts +45 -7
- package/lib/types/index.ts +13 -0
- package/lib/util/schema-resources.ts +73 -0
- package/package.json +12 -12
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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>(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/lib/dereference.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 { 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
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|