@hey-api/json-schema-ref-parser 1.2.3 → 1.3.0
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 +1887 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -78
- package/src/__tests__/bundle.test.ts +59 -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 +143 -229
- package/{lib → src}/dereference.ts +20 -43
- package/{lib → src}/index.ts +103 -125
- 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/circular-ref-with-description.json +0 -11
- package/lib/__tests__/spec/multiple-refs.json +0 -34
- 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,38 @@ 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
|
+
// Log warning but continue - common in complex schema ecosystems
|
|
161
|
+
console.warn(`Skipping unresolvable $ref: ${$refPath}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
throw error; // Re-throw unexpected errors
|
|
165
|
+
}
|
|
183
166
|
|
|
184
167
|
if (pointer) {
|
|
185
168
|
resolvedRefs.set($refPath, pointer);
|
|
186
|
-
perf.log(`Cached resolved $ref: ${$refPath}`);
|
|
187
169
|
}
|
|
188
170
|
}
|
|
189
171
|
|
|
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
|
-
}
|
|
172
|
+
if (pointer === null) return;
|
|
195
173
|
|
|
196
174
|
const parsed = Pointer.parse(pathFromRoot);
|
|
197
175
|
const depth = parsed.length;
|
|
@@ -202,10 +180,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
202
180
|
indirections += pointer.indirections;
|
|
203
181
|
|
|
204
182
|
// Check if this exact location (parent + key + pathFromRoot) has already been inventoried
|
|
205
|
-
perf.mark("lookup-start");
|
|
206
183
|
const existingEntry = inventoryLookup.find($refParent, $refKey);
|
|
207
|
-
perf.mark("lookup-end");
|
|
208
|
-
perf.measure("lookup-time", "lookup-start", "lookup-end");
|
|
209
184
|
|
|
210
185
|
if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
|
|
211
186
|
// This exact location has already been inventoried, so we don't need to process it again
|
|
@@ -213,8 +188,6 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
213
188
|
removeFromInventory(inventory, existingEntry);
|
|
214
189
|
inventoryLookup.remove(existingEntry);
|
|
215
190
|
} else {
|
|
216
|
-
perf.mark("inventory-ref-end");
|
|
217
|
-
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
218
191
|
return;
|
|
219
192
|
}
|
|
220
193
|
}
|
|
@@ -228,40 +201,38 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
228
201
|
file, // The file that the $ref pointer resolves to
|
|
229
202
|
hash, // The hash within `file` that the $ref pointer resolves to
|
|
230
203
|
indirections, // The number of indirect references that were traversed to resolve the value
|
|
231
|
-
key: $refKey,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
204
|
+
key: $refKey,
|
|
205
|
+
// The resolved value of the $ref pointer
|
|
206
|
+
originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined,
|
|
207
|
+
|
|
208
|
+
// The key in `parent` that is the $ref pointer
|
|
209
|
+
parent: $refParent,
|
|
210
|
+
|
|
211
|
+
// The object that contains this $ref pointer
|
|
212
|
+
pathFromRoot,
|
|
213
|
+
// The path to the $ref pointer, from the JSON Schema root
|
|
214
|
+
value: pointer.value, // The original container type in the external file
|
|
236
215
|
};
|
|
237
216
|
|
|
238
217
|
inventory.push(newEntry);
|
|
239
218
|
inventoryLookup.add(newEntry);
|
|
240
219
|
|
|
241
|
-
perf.log(`Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`);
|
|
242
|
-
|
|
243
220
|
// Recursively crawl the resolved value
|
|
244
221
|
if (!existingEntry || external) {
|
|
245
|
-
perf.mark("crawl-recursive-start");
|
|
246
222
|
crawl({
|
|
247
|
-
|
|
248
|
-
key: null,
|
|
249
|
-
path: pointer.path,
|
|
250
|
-
pathFromRoot,
|
|
223
|
+
$refs,
|
|
251
224
|
indirections: indirections + 1,
|
|
252
225
|
inventory,
|
|
253
226
|
inventoryLookup,
|
|
254
|
-
|
|
227
|
+
key: null,
|
|
255
228
|
options,
|
|
256
|
-
|
|
229
|
+
parent: pointer.value,
|
|
230
|
+
path: pointer.path,
|
|
231
|
+
pathFromRoot,
|
|
257
232
|
resolvedRefs,
|
|
233
|
+
visitedObjects,
|
|
258
234
|
});
|
|
259
|
-
perf.mark("crawl-recursive-end");
|
|
260
|
-
perf.measure("crawl-recursive-time", "crawl-recursive-start", "crawl-recursive-end");
|
|
261
235
|
}
|
|
262
|
-
|
|
263
|
-
perf.mark("inventory-ref-end");
|
|
264
|
-
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
265
236
|
};
|
|
266
237
|
|
|
267
238
|
/**
|
|
@@ -277,8 +248,8 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
277
248
|
parent,
|
|
278
249
|
path,
|
|
279
250
|
pathFromRoot,
|
|
280
|
-
visitedObjects = new WeakSet(),
|
|
281
251
|
resolvedRefs = new Map(),
|
|
252
|
+
visitedObjects = new WeakSet(),
|
|
282
253
|
}: {
|
|
283
254
|
$refs: $Refs<S>;
|
|
284
255
|
indirections: number;
|
|
@@ -307,38 +278,34 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
307
278
|
* The path of the property being crawled, from the schema root
|
|
308
279
|
*/
|
|
309
280
|
pathFromRoot: string;
|
|
310
|
-
/**
|
|
311
|
-
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
312
|
-
*/
|
|
313
|
-
visitedObjects?: WeakSet<object>;
|
|
314
281
|
/**
|
|
315
282
|
* Cache for resolved $ref targets to avoid redundant resolution
|
|
316
283
|
*/
|
|
317
284
|
resolvedRefs?: Map<string, any>;
|
|
285
|
+
/**
|
|
286
|
+
* Set of already visited objects to avoid infinite loops and redundant processing
|
|
287
|
+
*/
|
|
288
|
+
visitedObjects?: WeakSet<object>;
|
|
318
289
|
}) => {
|
|
319
290
|
const obj = key === null ? parent : parent[key as keyof typeof parent];
|
|
320
291
|
|
|
321
|
-
if (obj && typeof obj ===
|
|
292
|
+
if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) {
|
|
322
293
|
// 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
|
-
}
|
|
294
|
+
if (visitedObjects.has(obj)) return;
|
|
327
295
|
|
|
328
296
|
if ($Ref.isAllowed$Ref(obj)) {
|
|
329
|
-
perf.log(`Found $ref at ${pathFromRoot}: ${(obj as any).$ref}`);
|
|
330
297
|
inventory$Ref({
|
|
331
|
-
$refParent: parent,
|
|
332
298
|
$refKey: key,
|
|
333
|
-
|
|
334
|
-
|
|
299
|
+
$refParent: parent,
|
|
300
|
+
$refs,
|
|
335
301
|
indirections,
|
|
336
302
|
inventory,
|
|
337
303
|
inventoryLookup,
|
|
338
|
-
$refs,
|
|
339
304
|
options,
|
|
340
|
-
|
|
305
|
+
path,
|
|
306
|
+
pathFromRoot,
|
|
341
307
|
resolvedRefs,
|
|
308
|
+
visitedObjects,
|
|
342
309
|
});
|
|
343
310
|
} else {
|
|
344
311
|
// Mark this object as visited BEFORE processing its children
|
|
@@ -350,16 +317,16 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
350
317
|
const keys = Object.keys(obj).sort((a, b) => {
|
|
351
318
|
// Most people will expect references to be bundled into the "definitions" property,
|
|
352
319
|
// so we always crawl that property first, if it exists.
|
|
353
|
-
if (a ===
|
|
320
|
+
if (a === 'definitions') {
|
|
354
321
|
return -1;
|
|
355
|
-
} else if (b ===
|
|
322
|
+
} else if (b === 'definitions') {
|
|
356
323
|
return 1;
|
|
357
324
|
} else {
|
|
358
325
|
// Otherwise, crawl the keys based on their length.
|
|
359
326
|
// This produces the shortest possible bundled references
|
|
360
327
|
return a.length - b.length;
|
|
361
328
|
}
|
|
362
|
-
}) as
|
|
329
|
+
}) as Array<keyof typeof obj>;
|
|
363
330
|
|
|
364
331
|
for (const key of keys) {
|
|
365
332
|
const keyPath = Pointer.join(path, key);
|
|
@@ -368,31 +335,31 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
368
335
|
|
|
369
336
|
if ($Ref.isAllowed$Ref(value)) {
|
|
370
337
|
inventory$Ref({
|
|
371
|
-
$refParent: obj,
|
|
372
338
|
$refKey: key,
|
|
373
|
-
|
|
374
|
-
|
|
339
|
+
$refParent: obj,
|
|
340
|
+
$refs,
|
|
375
341
|
indirections,
|
|
376
342
|
inventory,
|
|
377
343
|
inventoryLookup,
|
|
378
|
-
$refs,
|
|
379
344
|
options,
|
|
380
|
-
|
|
345
|
+
path,
|
|
346
|
+
pathFromRoot: keyPathFromRoot,
|
|
381
347
|
resolvedRefs,
|
|
348
|
+
visitedObjects,
|
|
382
349
|
});
|
|
383
350
|
} else {
|
|
384
351
|
crawl({
|
|
385
|
-
|
|
386
|
-
key,
|
|
387
|
-
path: keyPath,
|
|
388
|
-
pathFromRoot: keyPathFromRoot,
|
|
352
|
+
$refs,
|
|
389
353
|
indirections,
|
|
390
354
|
inventory,
|
|
391
355
|
inventoryLookup,
|
|
392
|
-
|
|
356
|
+
key,
|
|
393
357
|
options,
|
|
394
|
-
|
|
358
|
+
parent: obj,
|
|
359
|
+
path: keyPath,
|
|
360
|
+
pathFromRoot: keyPathFromRoot,
|
|
395
361
|
resolvedRefs,
|
|
362
|
+
visitedObjects,
|
|
396
363
|
});
|
|
397
364
|
}
|
|
398
365
|
}
|
|
@@ -404,13 +371,10 @@ const crawl = <S extends object = JSONSchema>({
|
|
|
404
371
|
* Remap external refs by hoisting resolved values into a shared container in the root schema
|
|
405
372
|
* and pointing all occurrences to those internal definitions. Internal refs remain internal.
|
|
406
373
|
*/
|
|
407
|
-
function remap(parser: $RefParser, inventory: InventoryEntry
|
|
408
|
-
perf.log(`Starting remap with ${inventory.length} inventory entries`);
|
|
409
|
-
perf.mark("remap-start");
|
|
374
|
+
function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
|
|
410
375
|
const root = parser.schema as any;
|
|
411
376
|
|
|
412
377
|
// 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
378
|
inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
|
|
415
379
|
if (a.file !== b.file) {
|
|
416
380
|
// Group all the $refs that point to the same file
|
|
@@ -433,8 +397,8 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
433
397
|
} else {
|
|
434
398
|
// Determine how far each $ref is from the "definitions" property.
|
|
435
399
|
// 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(
|
|
400
|
+
const aDefinitionsIndex = a.pathFromRoot.lastIndexOf('/definitions');
|
|
401
|
+
const bDefinitionsIndex = b.pathFromRoot.lastIndexOf('/definitions');
|
|
438
402
|
if (aDefinitionsIndex !== bDefinitionsIndex) {
|
|
439
403
|
// Give higher priority to the $ref that's closer to the "definitions" property
|
|
440
404
|
return bDefinitionsIndex - aDefinitionsIndex;
|
|
@@ -445,69 +409,66 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
445
409
|
}
|
|
446
410
|
});
|
|
447
411
|
|
|
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
412
|
// Ensure or return a container by component type. Prefer OpenAPI-aware placement;
|
|
454
413
|
// otherwise use existing root containers; otherwise create components/*.
|
|
455
|
-
const ensureContainer = (
|
|
456
|
-
|
|
457
|
-
|
|
414
|
+
const ensureContainer = (
|
|
415
|
+
type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',
|
|
416
|
+
) => {
|
|
417
|
+
const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string');
|
|
418
|
+
const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string');
|
|
458
419
|
|
|
459
420
|
if (isOas3) {
|
|
460
|
-
if (!root.components || typeof root.components !==
|
|
421
|
+
if (!root.components || typeof root.components !== 'object') {
|
|
461
422
|
root.components = {};
|
|
462
423
|
}
|
|
463
|
-
if (!root.components[type] || typeof root.components[type] !==
|
|
424
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
464
425
|
root.components[type] = {};
|
|
465
426
|
}
|
|
466
427
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
467
428
|
}
|
|
468
429
|
|
|
469
430
|
if (isOas2) {
|
|
470
|
-
if (type ===
|
|
471
|
-
if (!root.definitions || typeof root.definitions !==
|
|
431
|
+
if (type === 'schemas') {
|
|
432
|
+
if (!root.definitions || typeof root.definitions !== 'object') {
|
|
472
433
|
root.definitions = {};
|
|
473
434
|
}
|
|
474
|
-
return { obj: root.definitions, prefix:
|
|
435
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
475
436
|
}
|
|
476
|
-
if (type ===
|
|
477
|
-
if (!root.parameters || typeof root.parameters !==
|
|
437
|
+
if (type === 'parameters') {
|
|
438
|
+
if (!root.parameters || typeof root.parameters !== 'object') {
|
|
478
439
|
root.parameters = {};
|
|
479
440
|
}
|
|
480
|
-
return { obj: root.parameters, prefix:
|
|
441
|
+
return { obj: root.parameters, prefix: '#/parameters' } as const;
|
|
481
442
|
}
|
|
482
|
-
if (type ===
|
|
483
|
-
if (!root.responses || typeof root.responses !==
|
|
443
|
+
if (type === 'responses') {
|
|
444
|
+
if (!root.responses || typeof root.responses !== 'object') {
|
|
484
445
|
root.responses = {};
|
|
485
446
|
}
|
|
486
|
-
return { obj: root.responses, prefix:
|
|
447
|
+
return { obj: root.responses, prefix: '#/responses' } as const;
|
|
487
448
|
}
|
|
488
449
|
// requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions
|
|
489
|
-
if (!root.definitions || typeof root.definitions !==
|
|
450
|
+
if (!root.definitions || typeof root.definitions !== 'object') {
|
|
490
451
|
root.definitions = {};
|
|
491
452
|
}
|
|
492
|
-
return { obj: root.definitions, prefix:
|
|
453
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
493
454
|
}
|
|
494
455
|
|
|
495
456
|
// 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] !==
|
|
457
|
+
if (root && typeof root === 'object') {
|
|
458
|
+
if (root.components && typeof root.components === 'object') {
|
|
459
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
499
460
|
root.components[type] = {};
|
|
500
461
|
}
|
|
501
462
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
502
463
|
}
|
|
503
|
-
if (root.definitions && typeof root.definitions ===
|
|
504
|
-
return { obj: root.definitions, prefix:
|
|
464
|
+
if (root.definitions && typeof root.definitions === 'object') {
|
|
465
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
505
466
|
}
|
|
506
467
|
// Create components/* by default if nothing exists
|
|
507
|
-
if (!root.components || typeof root.components !==
|
|
468
|
+
if (!root.components || typeof root.components !== 'object') {
|
|
508
469
|
root.components = {};
|
|
509
470
|
}
|
|
510
|
-
if (!root.components[type] || typeof root.components[type] !==
|
|
471
|
+
if (!root.components[type] || typeof root.components[type] !== 'object') {
|
|
511
472
|
root.components[type] = {};
|
|
512
473
|
}
|
|
513
474
|
return { obj: root.components[type], prefix: `#/components/${type}` } as const;
|
|
@@ -515,7 +476,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
515
476
|
|
|
516
477
|
// Fallback
|
|
517
478
|
root.definitions = root.definitions || {};
|
|
518
|
-
return { obj: root.definitions, prefix:
|
|
479
|
+
return { obj: root.definitions, prefix: '#/definitions' } as const;
|
|
519
480
|
};
|
|
520
481
|
|
|
521
482
|
/**
|
|
@@ -539,24 +500,24 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
539
500
|
const targetToNameByPrefix = new Map<string, Map<string, string>>();
|
|
540
501
|
const usedNamesByObj = new Map<any, Set<string>>();
|
|
541
502
|
|
|
542
|
-
const sanitize = (name: string) => name.replace(/[^A-Za-z0-9_-]/g,
|
|
503
|
+
const sanitize = (name: string) => name.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
543
504
|
const baseName = (filePath: string) => {
|
|
544
505
|
try {
|
|
545
|
-
const withoutHash = filePath.split(
|
|
546
|
-
const parts = withoutHash.split(
|
|
547
|
-
const filename = parts[parts.length - 1] ||
|
|
548
|
-
const dot = filename.lastIndexOf(
|
|
506
|
+
const withoutHash = filePath.split('#')[0]!;
|
|
507
|
+
const parts = withoutHash.split('/');
|
|
508
|
+
const filename = parts[parts.length - 1] || 'schema';
|
|
509
|
+
const dot = filename.lastIndexOf('.');
|
|
549
510
|
return sanitize(dot > 0 ? filename.substring(0, dot) : filename);
|
|
550
511
|
} catch {
|
|
551
|
-
return
|
|
512
|
+
return 'schema';
|
|
552
513
|
}
|
|
553
514
|
};
|
|
554
515
|
const lastToken = (hash: string) => {
|
|
555
|
-
if (!hash || hash ===
|
|
556
|
-
return
|
|
516
|
+
if (!hash || hash === '#') {
|
|
517
|
+
return 'root';
|
|
557
518
|
}
|
|
558
|
-
const tokens = hash.replace(/^#\//,
|
|
559
|
-
return sanitize(tokens[tokens.length - 1] ||
|
|
519
|
+
const tokens = hash.replace(/^#\//, '').split('/');
|
|
520
|
+
return sanitize(tokens[tokens.length - 1] || 'root');
|
|
560
521
|
};
|
|
561
522
|
const uniqueName = (containerObj: any, proposed: string) => {
|
|
562
523
|
if (!usedNamesByObj.has(containerObj)) {
|
|
@@ -571,11 +532,9 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
571
532
|
used.add(name);
|
|
572
533
|
return name;
|
|
573
534
|
};
|
|
574
|
-
perf.mark("remap-loop-start");
|
|
575
535
|
for (const entry of inventory) {
|
|
576
536
|
// 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);
|
|
537
|
+
if (!entry || !entry.$ref || typeof entry.$ref !== 'object') {
|
|
579
538
|
continue;
|
|
580
539
|
}
|
|
581
540
|
|
|
@@ -583,7 +542,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
583
542
|
// (i.e. it has additional properties in addition to "$ref"), then we must
|
|
584
543
|
// preserve the original $ref rather than rewriting it to the resolved hash.
|
|
585
544
|
if (!entry.external) {
|
|
586
|
-
if (!entry.extended && entry.$ref && typeof entry.$ref ===
|
|
545
|
+
if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
|
|
587
546
|
entry.$ref.$ref = entry.hash;
|
|
588
547
|
}
|
|
589
548
|
continue;
|
|
@@ -591,7 +550,7 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
591
550
|
|
|
592
551
|
// Avoid changing direct self-references; keep them internal
|
|
593
552
|
if (entry.circular) {
|
|
594
|
-
if (entry.$ref && typeof entry.$ref ===
|
|
553
|
+
if (entry.$ref && typeof entry.$ref === 'object') {
|
|
595
554
|
entry.$ref.$ref = entry.pathFromRoot;
|
|
596
555
|
}
|
|
597
556
|
continue;
|
|
@@ -613,10 +572,14 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
613
572
|
let proposedBase = baseName(entry.file);
|
|
614
573
|
try {
|
|
615
574
|
const parserAny: any = parser as any;
|
|
616
|
-
if (
|
|
617
|
-
|
|
575
|
+
if (
|
|
576
|
+
parserAny &&
|
|
577
|
+
parserAny.sourcePathToPrefix &&
|
|
578
|
+
typeof parserAny.sourcePathToPrefix.get === 'function'
|
|
579
|
+
) {
|
|
580
|
+
const withoutHash = (entry.file || '').split('#')[0];
|
|
618
581
|
const mapped = parserAny.sourcePathToPrefix.get(withoutHash);
|
|
619
|
-
if (mapped && typeof mapped ===
|
|
582
|
+
if (mapped && typeof mapped === 'string') {
|
|
620
583
|
proposedBase = mapped;
|
|
621
584
|
}
|
|
622
585
|
}
|
|
@@ -632,22 +595,15 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
|
|
|
632
595
|
|
|
633
596
|
// Point the occurrence to the internal definition, preserving extensions
|
|
634
597
|
const refPath = `${prefix}/${defName}`;
|
|
635
|
-
if (entry.extended && entry.$ref && typeof entry.$ref ===
|
|
598
|
+
if (entry.extended && entry.$ref && typeof entry.$ref === 'object') {
|
|
636
599
|
entry.$ref.$ref = refPath;
|
|
637
600
|
} else {
|
|
638
601
|
entry.parent[entry.key] = { $ref: refPath };
|
|
639
602
|
}
|
|
640
603
|
}
|
|
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
604
|
}
|
|
649
605
|
|
|
650
|
-
function removeFromInventory(inventory: InventoryEntry
|
|
606
|
+
function removeFromInventory(inventory: Array<InventoryEntry>, entry: any) {
|
|
651
607
|
const index = inventory.indexOf(entry);
|
|
652
608
|
inventory.splice(index, 1);
|
|
653
609
|
}
|
|
@@ -660,68 +616,26 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
|
|
|
660
616
|
* @param parser
|
|
661
617
|
* @param options
|
|
662
618
|
*/
|
|
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[] = [];
|
|
619
|
+
export function bundle(parser: $RefParser, options: ParserOptions): void {
|
|
620
|
+
const inventory: Array<InventoryEntry> = [];
|
|
669
621
|
const inventoryLookup = createInventoryLookup();
|
|
670
622
|
|
|
671
|
-
perf.log("Starting crawl phase");
|
|
672
|
-
perf.mark("crawl-phase-start");
|
|
673
|
-
|
|
674
623
|
const visitedObjects = new WeakSet<object>();
|
|
675
|
-
const resolvedRefs = new Map<string, any>();
|
|
624
|
+
const resolvedRefs = new Map<string, any>();
|
|
676
625
|
|
|
677
626
|
crawl<JSONSchema>({
|
|
678
|
-
|
|
679
|
-
key: "schema",
|
|
680
|
-
path: parser.$refs._root$Ref.path + "#",
|
|
681
|
-
pathFromRoot: "#",
|
|
627
|
+
$refs: parser.$refs,
|
|
682
628
|
indirections: 0,
|
|
683
629
|
inventory,
|
|
684
630
|
inventoryLookup,
|
|
685
|
-
|
|
631
|
+
key: 'schema',
|
|
686
632
|
options,
|
|
687
|
-
|
|
633
|
+
parent: parser,
|
|
634
|
+
path: parser.$refs._root$Ref.path + '#',
|
|
635
|
+
pathFromRoot: '#',
|
|
688
636
|
resolvedRefs,
|
|
637
|
+
visitedObjects,
|
|
689
638
|
});
|
|
690
639
|
|
|
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
640
|
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
|
-
};
|
|
641
|
+
}
|