@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.
Files changed (94) hide show
  1. package/README.md +9 -84
  2. package/dist/index.d.mts +629 -0
  3. package/dist/index.d.mts.map +1 -0
  4. package/dist/index.mjs +1920 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +42 -78
  7. package/{lib/__tests__/spec → src/__tests__/__snapshots__}/circular-ref-with-description.json +1 -1
  8. package/src/__tests__/__snapshots__/main-with-external-siblings.json +78 -0
  9. package/{lib/__tests__/spec → src/__tests__/__snapshots__}/multiple-refs.json +17 -3
  10. package/src/__tests__/__snapshots__/redfish-like.json +87 -0
  11. package/src/__tests__/bundle.test.ts +393 -0
  12. package/src/__tests__/index.test.ts +43 -0
  13. package/src/__tests__/pointer.test.ts +34 -0
  14. package/src/__tests__/utils.ts +3 -0
  15. package/{lib → src}/bundle.ts +191 -231
  16. package/{lib → src}/dereference.ts +20 -43
  17. package/{lib → src}/index.ts +129 -127
  18. package/{lib → src}/options.ts +13 -9
  19. package/{lib → src}/parse.ts +19 -15
  20. package/src/parsers/binary.ts +13 -0
  21. package/{lib → src}/parsers/json.ts +5 -6
  22. package/src/parsers/text.ts +21 -0
  23. package/{lib → src}/parsers/yaml.ts +9 -9
  24. package/{lib → src}/pointer.ts +42 -23
  25. package/{lib → src}/ref.ts +25 -21
  26. package/{lib → src}/refs.ts +23 -26
  27. package/{lib → src}/resolve-external.ts +91 -60
  28. package/{lib → src}/resolvers/file.ts +7 -10
  29. package/{lib → src}/resolvers/url.ts +12 -8
  30. package/{lib → src}/types/index.ts +9 -2
  31. package/src/util/convert-path-to-posix.ts +8 -0
  32. package/{lib → src}/util/errors.ts +38 -36
  33. package/{lib → src}/util/is-windows.ts +1 -1
  34. package/{lib → src}/util/plugins.ts +7 -8
  35. package/{lib → src}/util/url.ts +41 -42
  36. package/dist/lib/__tests__/bundle.test.d.ts +0 -1
  37. package/dist/lib/__tests__/bundle.test.js +0 -50
  38. package/dist/lib/__tests__/index.test.d.ts +0 -1
  39. package/dist/lib/__tests__/index.test.js +0 -43
  40. package/dist/lib/__tests__/pointer.test.d.ts +0 -1
  41. package/dist/lib/__tests__/pointer.test.js +0 -27
  42. package/dist/lib/bundle.d.ts +0 -26
  43. package/dist/lib/bundle.js +0 -600
  44. package/dist/lib/dereference.d.ts +0 -11
  45. package/dist/lib/dereference.js +0 -226
  46. package/dist/lib/index.d.ts +0 -92
  47. package/dist/lib/index.js +0 -525
  48. package/dist/lib/options.d.ts +0 -61
  49. package/dist/lib/options.js +0 -45
  50. package/dist/lib/parse.d.ts +0 -13
  51. package/dist/lib/parse.js +0 -87
  52. package/dist/lib/parsers/binary.d.ts +0 -2
  53. package/dist/lib/parsers/binary.js +0 -12
  54. package/dist/lib/parsers/json.d.ts +0 -2
  55. package/dist/lib/parsers/json.js +0 -38
  56. package/dist/lib/parsers/text.d.ts +0 -2
  57. package/dist/lib/parsers/text.js +0 -18
  58. package/dist/lib/parsers/yaml.d.ts +0 -2
  59. package/dist/lib/parsers/yaml.js +0 -28
  60. package/dist/lib/pointer.d.ts +0 -88
  61. package/dist/lib/pointer.js +0 -297
  62. package/dist/lib/ref.d.ts +0 -180
  63. package/dist/lib/ref.js +0 -226
  64. package/dist/lib/refs.d.ts +0 -127
  65. package/dist/lib/refs.js +0 -232
  66. package/dist/lib/resolve-external.d.ts +0 -13
  67. package/dist/lib/resolve-external.js +0 -151
  68. package/dist/lib/resolvers/file.d.ts +0 -6
  69. package/dist/lib/resolvers/file.js +0 -61
  70. package/dist/lib/resolvers/url.d.ts +0 -17
  71. package/dist/lib/resolvers/url.js +0 -62
  72. package/dist/lib/types/index.d.ts +0 -43
  73. package/dist/lib/types/index.js +0 -2
  74. package/dist/lib/util/convert-path-to-posix.d.ts +0 -1
  75. package/dist/lib/util/convert-path-to-posix.js +0 -14
  76. package/dist/lib/util/errors.d.ts +0 -56
  77. package/dist/lib/util/errors.js +0 -112
  78. package/dist/lib/util/is-windows.d.ts +0 -1
  79. package/dist/lib/util/is-windows.js +0 -6
  80. package/dist/lib/util/plugins.d.ts +0 -16
  81. package/dist/lib/util/plugins.js +0 -45
  82. package/dist/lib/util/url.d.ts +0 -79
  83. package/dist/lib/util/url.js +0 -285
  84. package/dist/vite.config.d.ts +0 -2
  85. package/dist/vite.config.js +0 -19
  86. package/lib/__tests__/bundle.test.ts +0 -52
  87. package/lib/__tests__/index.test.ts +0 -45
  88. package/lib/__tests__/pointer.test.ts +0 -26
  89. package/lib/__tests__/spec/openapi-paths-ref.json +0 -46
  90. package/lib/__tests__/spec/path-parameter.json +0 -16
  91. package/lib/parsers/binary.ts +0 -13
  92. package/lib/parsers/text.ts +0 -21
  93. package/lib/util/convert-path-to-posix.ts +0 -11
  94. /package/{LICENSE → LICENSE.md} +0 -0
@@ -1,21 +1,11 @@
1
- import $Ref from "./ref.js";
2
- import type { ParserOptions } from "./options.js";
3
- import Pointer from "./pointer.js";
4
- import * as url from "./util/url.js";
5
- import type $Refs from "./refs.js";
6
- import type { $RefParser } from "./index";
7
- import type { JSONSchema } from "./types/index.js";
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
- return `${getObjectId($refParent)}_${$refKey}`;
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
- ): "schemas" | "parameters" | "requestBodies" | "responses" | "headers" => {
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("parameters")) {
98
- return "parameters";
75
+ if (has('parameters')) {
76
+ return 'parameters';
99
77
  }
100
- if (has("requestBody")) {
101
- return "requestBodies";
78
+ if (has('requestBody')) {
79
+ return 'requestBodies';
102
80
  }
103
- if (has("headers")) {
104
- return "headers";
81
+ if (has('headers')) {
82
+ return 'headers';
105
83
  }
106
- if (has("responses")) {
107
- return "responses";
84
+ if (has('responses')) {
85
+ return 'responses';
108
86
  }
109
- if (has("schema")) {
110
- return "schemas";
87
+ if (has('schema')) {
88
+ return 'schemas';
111
89
  }
112
90
  // default: treat as schema-like
113
- return "schemas";
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
- perf.mark("resolve-start");
180
- pointer = $refs._resolve($refPath, pathFromRoot, options);
181
- perf.mark("resolve-end");
182
- perf.measure("resolve-time", "resolve-start", "resolve-end");
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, // The key in `parent` that is the $ref pointer
232
- parent: $refParent, // The object that contains this $ref pointer
233
- pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
234
- value: pointer.value, // The resolved value of the $ref pointer
235
- originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined, // The original container type in the external file
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
- perf.log(`Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`);
242
-
243
- // Recursively crawl the resolved value
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
- perf.mark("crawl-recursive-start");
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
- parent: pointer.value,
248
- key: null,
249
- path: pointer.path,
250
- pathFromRoot,
254
+ $refs,
251
255
  indirections: indirections + 1,
252
256
  inventory,
253
257
  inventoryLookup,
254
- $refs,
258
+ key: null,
255
259
  options,
256
- visitedObjects,
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 === "object" && !ArrayBuffer.isView(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
- path,
334
- pathFromRoot,
330
+ $refParent: parent,
331
+ $refs,
335
332
  indirections,
336
333
  inventory,
337
334
  inventoryLookup,
338
- $refs,
339
335
  options,
340
- visitedObjects,
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 === "definitions") {
351
+ if (a === 'definitions') {
354
352
  return -1;
355
- } else if (b === "definitions") {
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 (keyof typeof obj)[];
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
- path,
374
- pathFromRoot: keyPathFromRoot,
370
+ $refParent: obj,
371
+ $refs,
375
372
  indirections,
376
373
  inventory,
377
374
  inventoryLookup,
378
- $refs,
379
375
  options,
380
- visitedObjects,
376
+ path,
377
+ pathFromRoot: keyPathFromRoot,
381
378
  resolvedRefs,
379
+ visitedObjects,
382
380
  });
383
381
  } else {
384
382
  crawl({
385
- parent: obj,
386
- key,
387
- path: keyPath,
388
- pathFromRoot: keyPathFromRoot,
383
+ $refs,
389
384
  indirections,
390
385
  inventory,
391
386
  inventoryLookup,
392
- $refs,
387
+ key,
393
388
  options,
394
- visitedObjects,
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("/definitions");
437
- const bDefinitionsIndex = b.pathFromRoot.lastIndexOf("/definitions");
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 = (type: "schemas" | "parameters" | "requestBodies" | "responses" | "headers") => {
456
- const isOas3 = !!(root && typeof root === "object" && typeof root.openapi === "string");
457
- const isOas2 = !!(root && typeof root === "object" && typeof root.swagger === "string");
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 !== "object") {
452
+ if (!root.components || typeof root.components !== 'object') {
461
453
  root.components = {};
462
454
  }
463
- if (!root.components[type] || typeof root.components[type] !== "object") {
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 === "schemas") {
471
- if (!root.definitions || typeof root.definitions !== "object") {
462
+ if (type === 'schemas') {
463
+ if (!root.definitions || typeof root.definitions !== 'object') {
472
464
  root.definitions = {};
473
465
  }
474
- return { obj: root.definitions, prefix: "#/definitions" } as const;
466
+ return { obj: root.definitions, prefix: '#/definitions' } as const;
475
467
  }
476
- if (type === "parameters") {
477
- if (!root.parameters || typeof root.parameters !== "object") {
468
+ if (type === 'parameters') {
469
+ if (!root.parameters || typeof root.parameters !== 'object') {
478
470
  root.parameters = {};
479
471
  }
480
- return { obj: root.parameters, prefix: "#/parameters" } as const;
472
+ return { obj: root.parameters, prefix: '#/parameters' } as const;
481
473
  }
482
- if (type === "responses") {
483
- if (!root.responses || typeof root.responses !== "object") {
474
+ if (type === 'responses') {
475
+ if (!root.responses || typeof root.responses !== 'object') {
484
476
  root.responses = {};
485
477
  }
486
- return { obj: root.responses, prefix: "#/responses" } as const;
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 !== "object") {
481
+ if (!root.definitions || typeof root.definitions !== 'object') {
490
482
  root.definitions = {};
491
483
  }
492
- return { obj: root.definitions, prefix: "#/definitions" } as const;
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 === "object") {
497
- if (root.components && typeof root.components === "object") {
498
- if (!root.components[type] || typeof root.components[type] !== "object") {
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 === "object") {
504
- return { obj: root.definitions, prefix: "#/definitions" } as const;
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 !== "object") {
499
+ if (!root.components || typeof root.components !== 'object') {
508
500
  root.components = {};
509
501
  }
510
- if (!root.components[type] || typeof root.components[type] !== "object") {
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: "#/definitions" } as const;
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("#")[0];
546
- const parts = withoutHash.split("/");
547
- const filename = parts[parts.length - 1] || "schema";
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 "schema";
543
+ return 'schema';
552
544
  }
553
545
  };
554
546
  const lastToken = (hash: string) => {
555
- if (!hash || hash === "#") {
556
- return "root";
547
+ if (!hash || hash === '#') {
548
+ return 'root';
557
549
  }
558
- const tokens = hash.replace(/^#\//, "").split("/");
559
- return sanitize(tokens[tokens.length - 1] || "root");
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 !== "object") {
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 === "object") {
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 === "object") {
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 (parserAny && parserAny.sourcePathToPrefix && typeof parserAny.sourcePathToPrefix.get === "function") {
617
- const withoutHash = (entry.file || "").split("#")[0];
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 === "string") {
613
+ if (mapped && typeof mapped === 'string') {
620
614
  proposedBase = mapped;
621
615
  }
622
616
  }
623
617
  } catch {
624
618
  // Ignore errors
625
619
  }
626
- const proposed = `${proposedBase}_${lastToken(entry.hash)}`;
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 === "object") {
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[], entry: any) {
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 const bundle = (parser: $RefParser, options: ParserOptions) => {
664
- // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
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>(); // Cache for resolved $ref targets
670
+ const resolvedRefs = new Map<string, any>();
676
671
 
677
672
  crawl<JSONSchema>({
678
- parent: parser,
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
- $refs: parser.$refs,
677
+ key: 'schema',
686
678
  options,
687
- visitedObjects,
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
- perf.mark("remap-phase-end");
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
+ }