@hey-api/json-schema-ref-parser 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -84
- package/dist/index.d.mts +629 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1920 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -78
- package/{lib/__tests__/spec → src/__tests__/__snapshots__}/circular-ref-with-description.json +1 -1
- package/src/__tests__/__snapshots__/main-with-external-siblings.json +78 -0
- package/{lib/__tests__/spec → src/__tests__/__snapshots__}/multiple-refs.json +17 -3
- package/src/__tests__/__snapshots__/redfish-like.json +87 -0
- package/src/__tests__/bundle.test.ts +393 -0
- package/src/__tests__/index.test.ts +43 -0
- package/src/__tests__/pointer.test.ts +34 -0
- package/src/__tests__/utils.ts +3 -0
- package/{lib → src}/bundle.ts +191 -231
- package/{lib → src}/dereference.ts +20 -43
- package/{lib → src}/index.ts +129 -127
- package/{lib → src}/options.ts +13 -9
- package/{lib → src}/parse.ts +19 -15
- package/src/parsers/binary.ts +13 -0
- package/{lib → src}/parsers/json.ts +5 -6
- package/src/parsers/text.ts +21 -0
- package/{lib → src}/parsers/yaml.ts +9 -9
- package/{lib → src}/pointer.ts +42 -23
- package/{lib → src}/ref.ts +25 -21
- package/{lib → src}/refs.ts +23 -26
- package/{lib → src}/resolve-external.ts +91 -60
- package/{lib → src}/resolvers/file.ts +7 -10
- package/{lib → src}/resolvers/url.ts +12 -8
- package/{lib → src}/types/index.ts +9 -2
- package/src/util/convert-path-to-posix.ts +8 -0
- package/{lib → src}/util/errors.ts +38 -36
- package/{lib → src}/util/is-windows.ts +1 -1
- package/{lib → src}/util/plugins.ts +7 -8
- package/{lib → src}/util/url.ts +41 -42
- package/dist/lib/__tests__/bundle.test.d.ts +0 -1
- package/dist/lib/__tests__/bundle.test.js +0 -50
- package/dist/lib/__tests__/index.test.d.ts +0 -1
- package/dist/lib/__tests__/index.test.js +0 -43
- package/dist/lib/__tests__/pointer.test.d.ts +0 -1
- package/dist/lib/__tests__/pointer.test.js +0 -27
- package/dist/lib/bundle.d.ts +0 -26
- package/dist/lib/bundle.js +0 -600
- package/dist/lib/dereference.d.ts +0 -11
- package/dist/lib/dereference.js +0 -226
- package/dist/lib/index.d.ts +0 -92
- package/dist/lib/index.js +0 -525
- package/dist/lib/options.d.ts +0 -61
- package/dist/lib/options.js +0 -45
- package/dist/lib/parse.d.ts +0 -13
- package/dist/lib/parse.js +0 -87
- package/dist/lib/parsers/binary.d.ts +0 -2
- package/dist/lib/parsers/binary.js +0 -12
- package/dist/lib/parsers/json.d.ts +0 -2
- package/dist/lib/parsers/json.js +0 -38
- package/dist/lib/parsers/text.d.ts +0 -2
- package/dist/lib/parsers/text.js +0 -18
- package/dist/lib/parsers/yaml.d.ts +0 -2
- package/dist/lib/parsers/yaml.js +0 -28
- package/dist/lib/pointer.d.ts +0 -88
- package/dist/lib/pointer.js +0 -297
- package/dist/lib/ref.d.ts +0 -180
- package/dist/lib/ref.js +0 -226
- package/dist/lib/refs.d.ts +0 -127
- package/dist/lib/refs.js +0 -232
- package/dist/lib/resolve-external.d.ts +0 -13
- package/dist/lib/resolve-external.js +0 -151
- package/dist/lib/resolvers/file.d.ts +0 -6
- package/dist/lib/resolvers/file.js +0 -61
- package/dist/lib/resolvers/url.d.ts +0 -17
- package/dist/lib/resolvers/url.js +0 -62
- package/dist/lib/types/index.d.ts +0 -43
- package/dist/lib/types/index.js +0 -2
- package/dist/lib/util/convert-path-to-posix.d.ts +0 -1
- package/dist/lib/util/convert-path-to-posix.js +0 -14
- package/dist/lib/util/errors.d.ts +0 -56
- package/dist/lib/util/errors.js +0 -112
- package/dist/lib/util/is-windows.d.ts +0 -1
- package/dist/lib/util/is-windows.js +0 -6
- package/dist/lib/util/plugins.d.ts +0 -16
- package/dist/lib/util/plugins.js +0 -45
- package/dist/lib/util/url.d.ts +0 -79
- package/dist/lib/util/url.js +0 -285
- package/dist/vite.config.d.ts +0 -2
- package/dist/vite.config.js +0 -19
- package/lib/__tests__/bundle.test.ts +0 -52
- package/lib/__tests__/index.test.ts +0 -45
- package/lib/__tests__/pointer.test.ts +0 -26
- package/lib/__tests__/spec/openapi-paths-ref.json +0 -46
- package/lib/__tests__/spec/path-parameter.json +0 -16
- package/lib/parsers/binary.ts +0 -13
- package/lib/parsers/text.ts +0 -21
- package/lib/util/convert-path-to-posix.ts +0 -11
- /package/{LICENSE → LICENSE.md} +0 -0
package/{lib → src}/bundle.ts
RENAMED
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import $
|
|
2
|
-
import type { ParserOptions } from
|
|
3
|
-
import Pointer from
|
|
4
|
-
import
|
|
5
|
-
import type $Refs from
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
const DEBUG_PERFORMANCE =
|
|
10
|
-
process.env.DEBUG === "true" ||
|
|
11
|
-
(typeof globalThis !== "undefined" && (globalThis as any).DEBUG_BUNDLE_PERFORMANCE === true);
|
|
12
|
-
|
|
13
|
-
const perf = {
|
|
14
|
-
mark: (name: string) => DEBUG_PERFORMANCE && performance.mark(name),
|
|
15
|
-
measure: (name: string, start: string, end: string) => DEBUG_PERFORMANCE && performance.measure(name, start, end),
|
|
16
|
-
log: (message: string, ...args: any[]) => DEBUG_PERFORMANCE && console.log("[PERF] " + message, ...args),
|
|
17
|
-
warn: (message: string, ...args: any[]) => DEBUG_PERFORMANCE && console.warn("[PERF] " + message, ...args),
|
|
18
|
-
};
|
|
1
|
+
import type { $RefParser } from '.';
|
|
2
|
+
import type { ParserOptions } from './options';
|
|
3
|
+
import Pointer from './pointer';
|
|
4
|
+
import $Ref from './ref';
|
|
5
|
+
import type $Refs from './refs';
|
|
6
|
+
import type { JSONSchema } from './types';
|
|
7
|
+
import { MissingPointerError } from './util/errors';
|
|
8
|
+
import * as url from './util/url';
|
|
19
9
|
|
|
20
10
|
export interface InventoryEntry {
|
|
21
11
|
$ref: any;
|
|
@@ -27,10 +17,10 @@ export interface InventoryEntry {
|
|
|
27
17
|
hash: any;
|
|
28
18
|
indirections: any;
|
|
29
19
|
key: any;
|
|
20
|
+
originalContainerType?: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers';
|
|
30
21
|
parent: any;
|
|
31
22
|
pathFromRoot: any;
|
|
32
23
|
value: any;
|
|
33
|
-
originalContainerType?: "schemas" | "parameters" | "requestBodies" | "responses" | "headers";
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
/**
|
|
@@ -40,8 +30,6 @@ const createInventoryLookup = () => {
|
|
|
40
30
|
const lookup = new Map<string, InventoryEntry>();
|
|
41
31
|
const objectIds = new WeakMap<object, string>(); // Use WeakMap to avoid polluting objects
|
|
42
32
|
let idCounter = 0;
|
|
43
|
-
let lookupCount = 0;
|
|
44
|
-
let addCount = 0;
|
|
45
33
|
|
|
46
34
|
const getObjectId = (obj: any) => {
|
|
47
35
|
if (!objectIds.has(obj)) {
|
|
@@ -50,34 +38,24 @@ const createInventoryLookup = () => {
|
|
|
50
38
|
return objectIds.get(obj)!;
|
|
51
39
|
};
|
|
52
40
|
|
|
53
|
-
const createInventoryKey = ($refParent: any, $refKey: any) =>
|
|
41
|
+
const createInventoryKey = ($refParent: any, $refKey: any) =>
|
|
54
42
|
// Use WeakMap-based lookup to avoid polluting the actual schema objects
|
|
55
|
-
|
|
56
|
-
};
|
|
43
|
+
`${getObjectId($refParent)}_${$refKey}`;
|
|
57
44
|
|
|
58
45
|
return {
|
|
59
46
|
add: (entry: InventoryEntry) => {
|
|
60
|
-
addCount++;
|
|
61
47
|
const key = createInventoryKey(entry.parent, entry.key);
|
|
62
48
|
lookup.set(key, entry);
|
|
63
|
-
if (addCount % 100 === 0) {
|
|
64
|
-
perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`);
|
|
65
|
-
}
|
|
66
49
|
},
|
|
67
50
|
find: ($refParent: any, $refKey: any) => {
|
|
68
|
-
lookupCount++;
|
|
69
51
|
const key = createInventoryKey($refParent, $refKey);
|
|
70
52
|
const result = lookup.get(key);
|
|
71
|
-
if (lookupCount % 100 === 0) {
|
|
72
|
-
perf.log(`Inventory lookup: ${lookupCount} lookups performed`);
|
|
73
|
-
}
|
|
74
53
|
return result;
|
|
75
54
|
},
|
|
76
55
|
remove: (entry: InventoryEntry) => {
|
|
77
56
|
const key = createInventoryKey(entry.parent, entry.key);
|
|
78
57
|
lookup.delete(key);
|
|
79
58
|
},
|
|
80
|
-
getStats: () => ({ lookupCount, addCount, mapSize: lookup.size }),
|
|
81
59
|
};
|
|
82
60
|
};
|
|
83
61
|
|
|
@@ -90,27 +68,27 @@ const createInventoryLookup = () => {
|
|
|
90
68
|
*/
|
|
91
69
|
const getContainerTypeFromPath = (
|
|
92
70
|
path: string,
|
|
93
|
-
):
|
|
71
|
+
): 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers' => {
|
|
94
72
|
const tokens = Pointer.parse(path);
|
|
95
73
|
const has = (t: string) => tokens.includes(t);
|
|
96
74
|
// Prefer more specific containers first
|
|
97
|
-
if (has(
|
|
98
|
-
return
|
|
75
|
+
if (has('parameters')) {
|
|
76
|
+
return 'parameters';
|
|
99
77
|
}
|
|
100
|
-
if (has(
|
|
101
|
-
return
|
|
78
|
+
if (has('requestBody')) {
|
|
79
|
+
return 'requestBodies';
|
|
102
80
|
}
|
|
103
|
-
if (has(
|
|
104
|
-
return
|
|
81
|
+
if (has('headers')) {
|
|
82
|
+
return 'headers';
|
|
105
83
|
}
|
|
106
|
-
if (has(
|
|
107
|
-
return
|
|
84
|
+
if (has('responses')) {
|
|
85
|
+
return 'responses';
|
|
108
86
|
}
|
|
109
|
-
if (has(
|
|
110
|
-
return
|
|
87
|
+
if (has('schema')) {
|
|
88
|
+
return 'schemas';
|
|
111
89
|
}
|
|
112
90
|
// default: treat as schema-like
|
|
113
|
-
return
|
|
91
|
+
return 'schemas';
|
|
114
92
|
};
|
|
115
93
|
|
|
116
94
|
/**
|
|
@@ -127,8 +105,8 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
127
105
|
options,
|
|
128
106
|
path,
|
|
129
107
|
pathFromRoot,
|
|
130
|
-
visitedObjects = new WeakSet(),
|
|
131
108
|
resolvedRefs = new Map(),
|
|
109
|
+
visitedObjects = new WeakSet(),
|
|
132
110
|
}: {
|
|
133
111
|
/**
|
|
134
112
|
* The key in `$refParent` that is a JSON Reference
|
|
@@ -160,38 +138,58 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
160
138
|
* The path of the JSON Reference at `$refKey`, from the schema root
|
|
161
139
|
*/
|
|
162
140
|
pathFromRoot: string;
|
|
163
|
-
/**
|
|
164
|
-
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
165
|
-
*/
|
|
166
|
-
visitedObjects?: WeakSet<object>;
|
|
167
141
|
/**
|
|
168
142
|
* Cache for resolved $ref targets to avoid redundant resolution
|
|
169
143
|
*/
|
|
170
144
|
resolvedRefs?: Map<string, any>;
|
|
145
|
+
/**
|
|
146
|
+
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
147
|
+
*/
|
|
148
|
+
visitedObjects?: WeakSet<object>;
|
|
171
149
|
}) => {
|
|
172
|
-
perf.mark("inventory-ref-start");
|
|
173
150
|
const $ref = $refKey === null ? $refParent : $refParent[$refKey];
|
|
174
151
|
const $refPath = url.resolve(path, $ref.$ref);
|
|
175
152
|
|
|
176
153
|
// Check cache first to avoid redundant resolution
|
|
177
154
|
let pointer = resolvedRefs.get($refPath);
|
|
178
155
|
if (!pointer) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
try {
|
|
157
|
+
pointer = $refs._resolve($refPath, pathFromRoot, options);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof MissingPointerError) {
|
|
160
|
+
// The ref couldn't be resolved in the target file. This commonly
|
|
161
|
+
// happens when a wrapper file redirects via $ref to a versioned
|
|
162
|
+
// file, and the bundler's crawl path retains the wrapper URL.
|
|
163
|
+
// Try resolving the hash fragment against other files in $refs
|
|
164
|
+
// that might contain the target schema.
|
|
165
|
+
const hash = url.getHash($refPath);
|
|
166
|
+
if (hash) {
|
|
167
|
+
const baseFile = url.stripHash($refPath);
|
|
168
|
+
for (const filePath of Object.keys($refs._$refs)) {
|
|
169
|
+
if (filePath === baseFile) continue;
|
|
170
|
+
try {
|
|
171
|
+
pointer = $refs._resolve(filePath + hash, pathFromRoot, options);
|
|
172
|
+
if (pointer) break;
|
|
173
|
+
} catch {
|
|
174
|
+
// try next file
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!pointer) {
|
|
179
|
+
console.warn(`Skipping unresolvable $ref: ${$refPath}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
183
186
|
|
|
184
187
|
if (pointer) {
|
|
185
188
|
resolvedRefs.set($refPath, pointer);
|
|
186
|
-
perf.log(`Cached resolved $ref: ${$refPath}`);
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
if (pointer === null)
|
|
191
|
-
perf.mark("inventory-ref-end");
|
|
192
|
-
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
192
|
+
if (pointer === null) return;
|
|
195
193
|
|
|
196
194
|
const parsed = Pointer.parse(pathFromRoot);
|
|
197
195
|
const depth = parsed.length;
|
|
@@ -202,10 +200,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
202
200
|
indirections += pointer.indirections;
|
|
203
201
|
|
|
204
202
|
// Check if this exact location (parent + key + pathFromRoot) has already been inventoried
|
|
205
|
-
perf.mark("lookup-start");
|
|
206
203
|
const existingEntry = inventoryLookup.find($refParent, $refKey);
|
|
207
|
-
perf.mark("lookup-end");
|
|
208
|
-
perf.measure("lookup-time", "lookup-start", "lookup-end");
|
|
209
204
|
|
|
210
205
|
if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
|
|
211
206
|
// This exact location has already been inventoried, so we don't need to process it again
|
|
@@ -213,8 +208,6 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
213
208
|
removeFromInventory(inventory, existingEntry);
|
|
214
209
|
inventoryLookup.remove(existingEntry);
|
|
215
210
|
} else {
|
|
216
|
-
perf.mark("inventory-ref-end");
|
|
217
|
-
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
218
211
|
return;
|
|
219
212
|
}
|
|
220
213
|
}
|
|
@@ -228,40 +221,49 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
228
221
|
file, // The file that the $ref pointer resolves to
|
|
229
222
|
hash, // The hash within `file` that the $ref pointer resolves to
|
|
230
223
|
indirections, // The number of indirect references that were traversed to resolve the value
|
|
231
|
-
key: $refKey,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
224
|
+
key: $refKey,
|
|
225
|
+
// The resolved value of the $ref pointer
|
|
226
|
+
originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined,
|
|
227
|
+
|
|
228
|
+
// The key in `parent` that is the $ref pointer
|
|
229
|
+
parent: $refParent,
|
|
230
|
+
|
|
231
|
+
// The object that contains this $ref pointer
|
|
232
|
+
pathFromRoot,
|
|
233
|
+
// The path to the $ref pointer, from the JSON Schema root
|
|
234
|
+
value: pointer.value, // The original container type in the external file
|
|
236
235
|
};
|
|
237
236
|
|
|
238
237
|
inventory.push(newEntry);
|
|
239
238
|
inventoryLookup.add(newEntry);
|
|
240
239
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
240
|
+
// Recursively crawl the resolved value.
|
|
241
|
+
// When the resolution followed a $ref chain to a different file,
|
|
242
|
+
// use the resolved file as the base path so that local $ref values
|
|
243
|
+
// (e.g. #/components/schemas/SiblingSchema) inside the resolved
|
|
244
|
+
// value resolve against the correct file.
|
|
244
245
|
if (!existingEntry || external) {
|
|
245
|
-
|
|
246
|
+
let crawlPath = pointer.path;
|
|
247
|
+
|
|
248
|
+
const originalFile = url.stripHash($refPath);
|
|
249
|
+
if (file !== originalFile) {
|
|
250
|
+
crawlPath = file + url.getHash(pointer.path);
|
|
251
|
+
}
|
|
252
|
+
|
|
246
253
|
crawl({
|
|
247
|
-
|
|
248
|
-
key: null,
|
|
249
|
-
path: pointer.path,
|
|
250
|
-
pathFromRoot,
|
|
254
|
+
$refs,
|
|
251
255
|
indirections: indirections + 1,
|
|
252
256
|
inventory,
|
|
253
257
|
inventoryLookup,
|
|
254
|
-
|
|
258
|
+
key: null,
|
|
255
259
|
options,
|
|
256
|
-
|
|
260
|
+
parent: pointer.value,
|
|
261
|
+
path: crawlPath,
|
|
262
|
+
pathFromRoot,
|
|
257
263
|
resolvedRefs,
|
|
264
|
+
visitedObjects,
|
|
258
265
|
});
|
|
259
|
-
perf.mark("crawl-recursive-end");
|
|
260
|
-
perf.measure("crawl-recursive-time", "crawl-recursive-start", "crawl-recursive-end");
|
|
261
266
|
}
|
|
262
|
-
|
|
263
|
-
perf.mark("inventory-ref-end");
|
|
264
|
-
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
265
267
|
};
|
|
266
268
|
|
|
267
269
|
/**
|
|
@@ -277,8 +279,8 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
277
279
|
parent,
|
|
278
280
|
path,
|
|
279
281
|
pathFromRoot,
|
|
280
|
-
visitedObjects = new WeakSet(),
|
|
281
282
|
resolvedRefs = new Map(),
|
|
283
|
+
visitedObjects = new WeakSet(),
|
|
282
284
|
}: {
|
|
283
285
|
$refs: $Refs<S>;
|
|
284
286
|
indirections: number;
|
|
@@ -307,38 +309,34 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
307
309
|
* The path of the property being crawled, from the schema root
|
|
308
310
|
*/
|
|
309
311
|
pathFromRoot: string;
|
|
310
|
-
/**
|
|
311
|
-
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
312
|
-
*/
|
|
313
|
-
visitedObjects?: WeakSet<object>;
|
|
314
312
|
/**
|
|
315
313
|
* Cache for resolved $ref targets to avoid redundant resolution
|
|
316
314
|
*/
|
|
317
315
|
resolvedRefs?: Map<string, any>;
|
|
316
|
+
/**
|
|
317
|
+
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
318
|
+
*/
|
|
319
|
+
visitedObjects?: WeakSet<object>;
|
|
318
320
|
}) => {
|
|
319
321
|
const obj = key === null ? parent : parent[key as keyof typeof parent];
|
|
320
322
|
|
|
321
|
-
if (obj && typeof obj ===
|
|
323
|
+
if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) {
|
|
322
324
|
// Early exit if we've already processed this exact object
|
|
323
|
-
if (visitedObjects.has(obj))
|
|
324
|
-
perf.log(`Skipping already visited object at ${pathFromRoot}`);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
325
|
+
if (visitedObjects.has(obj)) return;
|
|
327
326
|
|
|
328
327
|
if ($Ref.isAllowed$Ref(obj)) {
|
|
329
|
-
perf.log(`Found $ref at ${pathFromRoot}: ${(obj as any).$ref}`);
|
|
330
328
|
inventory$Ref({
|
|
331
|
-
$refParent: parent,
|
|
332
329
|
$refKey: key,
|
|
333
|
-
|
|
334
|
-
|
|
330
|
+
$refParent: parent,
|
|
331
|
+
$refs,
|
|
335
332
|
indirections,
|
|
336
333
|
inventory,
|
|
337
334
|
inventoryLookup,
|
|
338
|
-
$refs,
|
|
339
335
|
options,
|
|
340
|
-
|
|
336
|
+
path,
|
|
337
|
+
pathFromRoot,
|
|
341
338
|
resolvedRefs,
|
|
339
|
+
visitedObjects,
|
|
342
340
|
});
|
|
343
341
|
} else {
|
|
344
342
|
// Mark this object as visited BEFORE processing its children
|
|
@@ -350,16 +348,16 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
350
348
|
const keys = Object.keys(obj).sort((a, b) => {
|
|
351
349
|
// Most people will expect references to be bundled into the "definitions" property,
|
|
352
350
|
// so we always crawl that property first, if it exists.
|
|
353
|
-
if (a ===
|
|
351
|
+
if (a === 'definitions') {
|
|
354
352
|
return -1;
|
|
355
|
-
} else if (b ===
|
|
353
|
+
} else if (b === 'definitions') {
|
|
356
354
|
return 1;
|
|
357
355
|
} else {
|
|
358
356
|
// Otherwise, crawl the keys based on their length.
|
|
359
357
|
// This produces the shortest possible bundled references
|
|
360
358
|
return a.length - b.length;
|
|
361
359
|
}
|
|
362
|
-
}) as
|
|
360
|
+
}) as Array<keyof typeof obj>;
|
|
363
361
|
|
|
364
362
|
for (const key of keys) {
|
|
365
363
|
const keyPath = Pointer.join(path, key);
|
|
@@ -368,31 +366,31 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
368
366
|
|
|
369
367
|
if ($Ref.isAllowed$Ref(value)) {
|
|
370
368
|
inventory$Ref({
|
|
371
|
-
$refParent: obj,
|
|
372
369
|
$refKey: key,
|
|
373
|
-
|
|
374
|
-
|
|
370
|
+
$refParent: obj,
|
|
371
|
+
$refs,
|
|
375
372
|
indirections,
|
|
376
373
|
inventory,
|
|
377
374
|
inventoryLookup,
|
|
378
|
-
$refs,
|
|
379
375
|
options,
|
|
380
|
-
|
|
376
|
+
path,
|
|
377
|
+
pathFromRoot: keyPathFromRoot,
|
|
381
378
|
resolvedRefs,
|
|
379
|
+
visitedObjects,
|
|
382
380
|
});
|
|
383
381
|
} else {
|
|
384
382
|
crawl({
|
|
385
|
-
|
|
386
|
-
key,
|
|
387
|
-
path: keyPath,
|
|
388
|
-
pathFromRoot: keyPathFromRoot,
|
|
383
|
+
$refs,
|
|
389
384
|
indirections,
|
|
390
385
|
inventory,
|
|
391
386
|
inventoryLookup,
|
|
392
|
-
|
|
387
|
+
key,
|
|
393
388
|
options,
|
|
394
|
-
|
|
389
|
+
parent: obj,
|
|
390
|
+
path: keyPath,
|
|
391
|
+
pathFromRoot: keyPathFromRoot,
|
|
395
392
|
resolvedRefs,
|
|
393
|
+
visitedObjects,
|
|
396
394
|
});
|
|
397
395
|
}
|
|
398
396
|
}
|
|
@@ -404,13 +402,10 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
404
402
|
* Remap external refs by hoisting resolved values into a shared container in the root schema
|
|
405
403
|
* and pointing all occurrences to those internal definitions. Internal refs remain internal.
|
|
406
404
|
*/
|
|
407
|
-
function remap(parser: $RefParser, inventory: InventoryEntry
|
|
408
|
-
perf.log(`Starting remap with ${inventory.length} inventory entries`);
|
|
409
|
-
perf.mark("remap-start");
|
|
405
|
+
function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
|
|
410
406
|
const root = parser.schema as any;
|
|
411
407
|
|
|
412
408
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
413
|
-
perf.mark("sort-inventory-start");
|
|
414
409
|
inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
|
|
415
410
|
if (a.file !== b.file) {
|
|
416
411
|
// Group all the $refs that point to the same file
|
|
@@ -433,8 +428,8 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
433
428
|
} else {
|
|
434
429
|
// Determine how far each $ref is from the "definitions" property.
|
|
435
430
|
// Most people will expect references to be bundled into the the "definitions" property if possible.
|
|
436
|
-
const aDefinitionsIndex = a.pathFromRoot.lastIndexOf(
|
|
437
|
-
const bDefinitionsIndex = b.pathFromRoot.lastIndexOf(
|
|
431
|
+
const aDefinitionsIndex = a.pathFromRoot.lastIndexOf('/definitions');
|
|
432
|
+
const bDefinitionsIndex = b.pathFromRoot.lastIndexOf('/definitions');
|
|
438
433
|
if (aDefinitionsIndex !== bDefinitionsIndex) {
|
|
439
434
|
// Give higher priority to the $ref that's closer to the "definitions" property
|
|
440
435
|
return bDefinitionsIndex - aDefinitionsIndex;
|
|
@@ -445,69 +440,66 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
445
440
|
}
|
|
446
441
|
});
|
|
447
442
|
|
|
448
|
-
perf.mark("sort-inventory-end");
|
|
449
|
-
perf.measure("sort-inventory-time", "sort-inventory-start", "sort-inventory-end");
|
|
450
|
-
|
|
451
|
-
perf.log(`Sorted ${inventory.length} inventory entries`);
|
|
452
|
-
|
|
453
443
|
// Ensure or return a container by component type. Prefer OpenAPI-aware placement;
|
|
454
444
|
// otherwise use existing root containers; otherwise create components/*.
|
|
455
|
-
const ensureContainer = (
|
|
456
|
-
|
|
457
|
-
|
|
445
|
+
const ensureContainer = (
|
|
446
|
+
type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',
|
|
447
|
+
) => {
|
|
448
|
+
const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string');
|
|
449
|
+
const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string');
|
|
458
450
|
|
|
459
451
|
if (isOas3) {
|
|
460
|
-
if (!root.components || typeof root.components !==
|
|
452
|
+
if (!root.components || typeof root.components !== 'object') {
|
|
461
453
|
root.components = {};
|
|
462
454
|
}
|
|
463
|
-
if (!root.components[type] || typeof root.components[type] !==
|
|
455
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
464
456
|
root.components[type] = {};
|
|
465
457
|
}
|
|
466
458
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
467
459
|
}
|
|
468
460
|
|
|
469
461
|
if (isOas2) {
|
|
470
|
-
if (type ===
|
|
471
|
-
if (!root.definitions || typeof root.definitions !==
|
|
462
|
+
if (type === 'schemas') {
|
|
463
|
+
if (!root.definitions || typeof root.definitions !== 'object') {
|
|
472
464
|
root.definitions = {};
|
|
473
465
|
}
|
|
474
|
-
return { obj: root.definitions, prefix:
|
|
466
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
475
467
|
}
|
|
476
|
-
if (type ===
|
|
477
|
-
if (!root.parameters || typeof root.parameters !==
|
|
468
|
+
if (type === 'parameters') {
|
|
469
|
+
if (!root.parameters || typeof root.parameters !== 'object') {
|
|
478
470
|
root.parameters = {};
|
|
479
471
|
}
|
|
480
|
-
return { obj: root.parameters, prefix:
|
|
472
|
+
return { obj: root.parameters, prefix: '#/parameters' } as const;
|
|
481
473
|
}
|
|
482
|
-
if (type ===
|
|
483
|
-
if (!root.responses || typeof root.responses !==
|
|
474
|
+
if (type === 'responses') {
|
|
475
|
+
if (!root.responses || typeof root.responses !== 'object') {
|
|
484
476
|
root.responses = {};
|
|
485
477
|
}
|
|
486
|
-
return { obj: root.responses, prefix:
|
|
478
|
+
return { obj: root.responses, prefix: '#/responses' } as const;
|
|
487
479
|
}
|
|
488
480
|
// requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions
|
|
489
|
-
if (!root.definitions || typeof root.definitions !==
|
|
481
|
+
if (!root.definitions || typeof root.definitions !== 'object') {
|
|
490
482
|
root.definitions = {};
|
|
491
483
|
}
|
|
492
|
-
return { obj: root.definitions, prefix:
|
|
484
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
493
485
|
}
|
|
494
486
|
|
|
495
487
|
// No explicit version: prefer existing containers
|
|
496
|
-
if (root && typeof root ===
|
|
497
|
-
if (root.components && typeof root.components ===
|
|
498
|
-
if (!root.components[type] || typeof root.components[type] !==
|
|
488
|
+
if (root && typeof root === 'object') {
|
|
489
|
+
if (root.components && typeof root.components === 'object') {
|
|
490
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
499
491
|
root.components[type] = {};
|
|
500
492
|
}
|
|
501
493
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
502
494
|
}
|
|
503
|
-
if (root.definitions && typeof root.definitions ===
|
|
504
|
-
return { obj: root.definitions, prefix:
|
|
495
|
+
if (root.definitions && typeof root.definitions === 'object') {
|
|
496
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
505
497
|
}
|
|
506
498
|
// Create components/* by default if nothing exists
|
|
507
|
-
if (!root.components || typeof root.components !==
|
|
499
|
+
if (!root.components || typeof root.components !== 'object') {
|
|
508
500
|
root.components = {};
|
|
509
501
|
}
|
|
510
|
-
if (!root.components[type] || typeof root.components[type] !==
|
|
502
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
511
503
|
root.components[type] = {};
|
|
512
504
|
}
|
|
513
505
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
@@ -515,7 +507,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
515
507
|
|
|
516
508
|
// Fallback
|
|
517
509
|
root.definitions = root.definitions || {};
|
|
518
|
-
return { obj: root.definitions, prefix:
|
|
510
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
519
511
|
};
|
|
520
512
|
|
|
521
513
|
/**
|
|
@@ -539,24 +531,24 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
539
531
|
const targetToNameByPrefix = new Map<string, Map<string, string>>();
|
|
540
532
|
const usedNamesByObj = new Map<any, Set<string>>();
|
|
541
533
|
|
|
542
|
-
const sanitize = (name: string) => name.replace(/[^A-Za-z0-9_-]/g,
|
|
534
|
+
const sanitize = (name: string) => name.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
543
535
|
const baseName = (filePath: string) => {
|
|
544
536
|
try {
|
|
545
|
-
const withoutHash = filePath.split(
|
|
546
|
-
const parts = withoutHash.split(
|
|
547
|
-
const filename = parts[parts.length - 1] ||
|
|
548
|
-
const dot = filename.lastIndexOf(
|
|
537
|
+
const withoutHash = filePath.split('#')[0]!;
|
|
538
|
+
const parts = withoutHash.split('/');
|
|
539
|
+
const filename = parts[parts.length - 1] || 'schema';
|
|
540
|
+
const dot = filename.lastIndexOf('.');
|
|
549
541
|
return sanitize(dot > 0 ? filename.substring(0, dot) : filename);
|
|
550
542
|
} catch {
|
|
551
|
-
return
|
|
543
|
+
return 'schema';
|
|
552
544
|
}
|
|
553
545
|
};
|
|
554
546
|
const lastToken = (hash: string) => {
|
|
555
|
-
if (!hash || hash ===
|
|
556
|
-
return
|
|
547
|
+
if (!hash || hash === '#') {
|
|
548
|
+
return 'root';
|
|
557
549
|
}
|
|
558
|
-
const tokens = hash.replace(/^#\//,
|
|
559
|
-
return sanitize(tokens[tokens.length - 1] ||
|
|
550
|
+
const tokens = hash.replace(/^#\//, '').split('/');
|
|
551
|
+
return sanitize(tokens[tokens.length - 1] || 'root');
|
|
560
552
|
};
|
|
561
553
|
const uniqueName = (containerObj: any, proposed: string) => {
|
|
562
554
|
if (!usedNamesByObj.has(containerObj)) {
|
|
@@ -571,11 +563,9 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
571
563
|
used.add(name);
|
|
572
564
|
return name;
|
|
573
565
|
};
|
|
574
|
-
perf.mark("remap-loop-start");
|
|
575
566
|
for (const entry of inventory) {
|
|
576
567
|
// Safety check: ensure entry and entry.$ref are valid objects
|
|
577
|
-
if (!entry || !entry.$ref || typeof entry.$ref !==
|
|
578
|
-
perf.warn(`Skipping invalid inventory entry:`, entry);
|
|
568
|
+
if (!entry || !entry.$ref || typeof entry.$ref !== 'object') {
|
|
579
569
|
continue;
|
|
580
570
|
}
|
|
581
571
|
|
|
@@ -583,7 +573,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
583
573
|
// (i.e. it has additional properties in addition to "$ref"), then we must
|
|
584
574
|
// preserve the original $ref rather than rewriting it to the resolved hash.
|
|
585
575
|
if (!entry.external) {
|
|
586
|
-
if (!entry.extended && entry.$ref && typeof entry.$ref ===
|
|
576
|
+
if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
|
|
587
577
|
entry.$ref.$ref = entry.hash;
|
|
588
578
|
}
|
|
589
579
|
continue;
|
|
@@ -591,7 +581,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
591
581
|
|
|
592
582
|
// Avoid changing direct self-references; keep them internal
|
|
593
583
|
if (entry.circular) {
|
|
594
|
-
if (entry.$ref && typeof entry.$ref ===
|
|
584
|
+
if (entry.$ref && typeof entry.$ref === 'object') {
|
|
595
585
|
entry.$ref.$ref = entry.pathFromRoot;
|
|
596
586
|
}
|
|
597
587
|
continue;
|
|
@@ -613,17 +603,36 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
613
603
|
let proposedBase = baseName(entry.file);
|
|
614
604
|
try {
|
|
615
605
|
const parserAny: any = parser as any;
|
|
616
|
-
if (
|
|
617
|
-
|
|
606
|
+
if (
|
|
607
|
+
parserAny &&
|
|
608
|
+
parserAny.sourcePathToPrefix &&
|
|
609
|
+
typeof parserAny.sourcePathToPrefix.get === 'function'
|
|
610
|
+
) {
|
|
611
|
+
const withoutHash = (entry.file || '').split('#')[0];
|
|
618
612
|
const mapped = parserAny.sourcePathToPrefix.get(withoutHash);
|
|
619
|
-
if (mapped && typeof mapped ===
|
|
613
|
+
if (mapped && typeof mapped === 'string') {
|
|
620
614
|
proposedBase = mapped;
|
|
621
615
|
}
|
|
622
616
|
}
|
|
623
617
|
} catch {
|
|
624
618
|
// Ignore errors
|
|
625
619
|
}
|
|
626
|
-
|
|
620
|
+
|
|
621
|
+
// Try without prefix first (cleaner names)
|
|
622
|
+
const schemaName = lastToken(entry.hash);
|
|
623
|
+
let proposed = schemaName;
|
|
624
|
+
|
|
625
|
+
// Check if this name would conflict with existing schemas from other files
|
|
626
|
+
if (!usedNamesByObj.has(container)) {
|
|
627
|
+
usedNamesByObj.set(container, new Set<string>(Object.keys(container || {})));
|
|
628
|
+
}
|
|
629
|
+
const used = usedNamesByObj.get(container)!;
|
|
630
|
+
|
|
631
|
+
// If the name is already used, add the file prefix
|
|
632
|
+
if (used.has(proposed)) {
|
|
633
|
+
proposed = `${proposedBase}_${schemaName}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
627
636
|
defName = uniqueName(container, proposed);
|
|
628
637
|
namesForPrefix.set(targetKey, defName);
|
|
629
638
|
// Store the resolved value under the container
|
|
@@ -632,22 +641,15 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
632
641
|
|
|
633
642
|
// Point the occurrence to the internal definition, preserving extensions
|
|
634
643
|
const refPath = `${prefix}/${defName}`;
|
|
635
|
-
if (entry.extended && entry.$ref && typeof entry.$ref ===
|
|
644
|
+
if (entry.extended && entry.$ref && typeof entry.$ref === 'object') {
|
|
636
645
|
entry.$ref.$ref = refPath;
|
|
637
646
|
} else {
|
|
638
647
|
entry.parent[entry.key] = { $ref: refPath };
|
|
639
648
|
}
|
|
640
649
|
}
|
|
641
|
-
perf.mark("remap-loop-end");
|
|
642
|
-
perf.measure("remap-loop-time", "remap-loop-start", "remap-loop-end");
|
|
643
|
-
|
|
644
|
-
perf.mark("remap-end");
|
|
645
|
-
perf.measure("remap-total-time", "remap-start", "remap-end");
|
|
646
|
-
|
|
647
|
-
perf.log(`Completed remap of ${inventory.length} entries`);
|
|
648
650
|
}
|
|
649
651
|
|
|
650
|
-
function removeFromInventory(inventory: InventoryEntry
|
|
652
|
+
function removeFromInventory(inventory: Array<InventoryEntry>, entry: any) {
|
|
651
653
|
const index = inventory.indexOf(entry);
|
|
652
654
|
inventory.splice(index, 1);
|
|
653
655
|
}
|
|
@@ -660,68 +662,26 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
|
|
|
660
662
|
* @param parser
|
|
661
663
|
* @param options
|
|
662
664
|
*/
|
|
663
|
-
export
|
|
664
|
-
|
|
665
|
-
perf.mark("bundle-start");
|
|
666
|
-
|
|
667
|
-
// Build an inventory of all $ref pointers in the JSON Schema
|
|
668
|
-
const inventory: InventoryEntry[] = [];
|
|
665
|
+
export function bundle(parser: $RefParser, options: ParserOptions): void {
|
|
666
|
+
const inventory: Array<InventoryEntry> = [];
|
|
669
667
|
const inventoryLookup = createInventoryLookup();
|
|
670
668
|
|
|
671
|
-
perf.log("Starting crawl phase");
|
|
672
|
-
perf.mark("crawl-phase-start");
|
|
673
|
-
|
|
674
669
|
const visitedObjects = new WeakSet<object>();
|
|
675
|
-
const resolvedRefs = new Map<string, any>();
|
|
670
|
+
const resolvedRefs = new Map<string, any>();
|
|
676
671
|
|
|
677
672
|
crawl<JSONSchema>({
|
|
678
|
-
|
|
679
|
-
key: "schema",
|
|
680
|
-
path: parser.$refs._root$Ref.path + "#",
|
|
681
|
-
pathFromRoot: "#",
|
|
673
|
+
$refs: parser.$refs,
|
|
682
674
|
indirections: 0,
|
|
683
675
|
inventory,
|
|
684
676
|
inventoryLookup,
|
|
685
|
-
|
|
677
|
+
key: 'schema',
|
|
686
678
|
options,
|
|
687
|
-
|
|
679
|
+
parent: parser,
|
|
680
|
+
path: parser.$refs._root$Ref.path + '#',
|
|
681
|
+
pathFromRoot: '#',
|
|
688
682
|
resolvedRefs,
|
|
683
|
+
visitedObjects,
|
|
689
684
|
});
|
|
690
685
|
|
|
691
|
-
perf.mark("crawl-phase-end");
|
|
692
|
-
perf.measure("crawl-phase-time", "crawl-phase-start", "crawl-phase-end");
|
|
693
|
-
|
|
694
|
-
const stats = inventoryLookup.getStats();
|
|
695
|
-
perf.log(`Crawl phase complete. Found ${inventory.length} $refs. Lookup stats:`, stats);
|
|
696
|
-
|
|
697
|
-
// Remap all $ref pointers
|
|
698
|
-
perf.log("Starting remap phase");
|
|
699
|
-
perf.mark("remap-phase-start");
|
|
700
686
|
remap(parser, inventory);
|
|
701
|
-
|
|
702
|
-
perf.measure("remap-phase-time", "remap-phase-start", "remap-phase-end");
|
|
703
|
-
|
|
704
|
-
perf.mark("bundle-end");
|
|
705
|
-
perf.measure("bundle-total-time", "bundle-start", "bundle-end");
|
|
706
|
-
|
|
707
|
-
perf.log("Bundle complete. Performance summary:");
|
|
708
|
-
|
|
709
|
-
// Log final stats
|
|
710
|
-
const finalStats = inventoryLookup.getStats();
|
|
711
|
-
perf.log(`Final inventory stats:`, finalStats);
|
|
712
|
-
perf.log(`Resolved refs cache size: ${resolvedRefs.size}`);
|
|
713
|
-
|
|
714
|
-
if (DEBUG_PERFORMANCE) {
|
|
715
|
-
// Log all performance measures
|
|
716
|
-
const measures = performance.getEntriesByType("measure");
|
|
717
|
-
measures.forEach((measure) => {
|
|
718
|
-
if (measure.name.includes("time")) {
|
|
719
|
-
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// Clear performance marks and measures for next run
|
|
724
|
-
performance.clearMarks();
|
|
725
|
-
performance.clearMeasures();
|
|
726
|
-
}
|
|
727
|
-
};
|
|
687
|
+
}
|