@hey-api/json-schema-ref-parser 1.0.7 → 1.1.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.
@@ -17,19 +17,16 @@ const __1 = require("..");
17
17
  const refParser = new __1.$RefParser();
18
18
  const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "multiple-refs.json");
19
19
  const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
20
- // First reference should be fully resolved (no $ref)
21
- (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
22
- (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
23
- (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
24
- (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
25
- // Second reference should be remapped to point to the first reference
26
- (0, vitest_1.expect)(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe("#/paths/~1test1~1%7BpathId%7D/get/parameters/0");
27
- // Both should effectively resolve to the same data
20
+ // Both parameters should now be $ref to the same internal definition
28
21
  const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
29
22
  const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
30
- // The second parameter should resolve to the same data as the first
31
- (0, vitest_1.expect)(secondParam.$ref).toBeDefined();
32
- (0, vitest_1.expect)(firstParam).toEqual({
23
+ // The $ref should match the output structure in file_context_0
24
+ (0, vitest_1.expect)(firstParam.$ref).toBe("#/components/parameters/path-parameter_pathId");
25
+ (0, vitest_1.expect)(secondParam.$ref).toBe("#/components/parameters/path-parameter_pathId");
26
+ // The referenced parameter should exist and match the expected structure
27
+ (0, vitest_1.expect)(schema.components).toBeDefined();
28
+ (0, vitest_1.expect)(schema.components.parameters).toBeDefined();
29
+ (0, vitest_1.expect)(schema.components.parameters["path-parameter_pathId"]).toEqual({
33
30
  name: "pathId",
34
31
  in: "path",
35
32
  required: true,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const __1 = require("..");
8
+ const path_1 = __importDefault(require("path"));
9
+ (0, vitest_1.describe)("pointer", () => {
10
+ (0, vitest_1.it)("inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling", async () => {
11
+ const refParser = new __1.$RefParser();
12
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "openapi-paths-ref.json");
13
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
14
+ console.log(JSON.stringify(schema, null, 2));
15
+ // The GET endpoint should have its schema defined inline
16
+ const getSchema = schema.paths["/foo"].get.responses["200"].content["application/json"].schema;
17
+ (0, vitest_1.expect)(getSchema.$ref).toBeUndefined();
18
+ (0, vitest_1.expect)(getSchema.type).toBe("object");
19
+ (0, vitest_1.expect)(getSchema.properties.bar.type).toBe("string");
20
+ // The POST endpoint should have its schema inlined (copied) instead of a $ref
21
+ const postSchema = schema.paths["/foo"].post.responses["200"].content["application/json"].schema;
22
+ (0, vitest_1.expect)(postSchema.$ref).toBe("#/paths/~1foo/get/responses/200/content/application~1json/schema");
23
+ (0, vitest_1.expect)(postSchema.type).toBeUndefined();
24
+ (0, vitest_1.expect)(postSchema.properties?.bar?.type).toBeUndefined();
25
+ // Both schemas should be identical objects
26
+ (0, vitest_1.expect)(postSchema).not.toBe(getSchema);
27
+ });
28
+ });
@@ -13,6 +13,7 @@ export interface InventoryEntry {
13
13
  parent: any;
14
14
  pathFromRoot: any;
15
15
  value: any;
16
+ originalContainerType?: "schemas" | "parameters" | "requestBodies" | "responses" | "headers";
16
17
  }
17
18
  /**
18
19
  * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that
@@ -37,34 +37,113 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.bundle = void 0;
40
- const isEqual_1 = __importDefault(require("lodash/isEqual"));
41
40
  const ref_js_1 = __importDefault(require("./ref.js"));
42
41
  const pointer_js_1 = __importDefault(require("./pointer.js"));
43
42
  const url = __importStar(require("./util/url.js"));
43
+ const DEBUG_PERFORMANCE = process.env.DEBUG === "true" ||
44
+ (typeof globalThis !== "undefined" && globalThis.DEBUG_BUNDLE_PERFORMANCE === true);
45
+ const perf = {
46
+ mark: (name) => DEBUG_PERFORMANCE && performance.mark(name),
47
+ measure: (name, start, end) => DEBUG_PERFORMANCE && performance.measure(name, start, end),
48
+ log: (message, ...args) => DEBUG_PERFORMANCE && console.log("[PERF] " + message, ...args),
49
+ warn: (message, ...args) => DEBUG_PERFORMANCE && console.warn("[PERF] " + message, ...args),
50
+ };
44
51
  /**
45
- * TODO
52
+ * Fast lookup using Map instead of linear search with deep equality
46
53
  */
47
- const findInInventory = (inventory, $refParent, $refKey) => {
48
- for (const entry of inventory) {
49
- if (entry) {
50
- if ((0, isEqual_1.default)(entry.parent, $refParent)) {
51
- if (entry.key === $refKey) {
52
- return entry;
53
- }
54
- }
54
+ const createInventoryLookup = () => {
55
+ const lookup = new Map();
56
+ const objectIds = new WeakMap(); // Use WeakMap to avoid polluting objects
57
+ let idCounter = 0;
58
+ let lookupCount = 0;
59
+ let addCount = 0;
60
+ const getObjectId = (obj) => {
61
+ if (!objectIds.has(obj)) {
62
+ objectIds.set(obj, `obj_${++idCounter}`);
55
63
  }
64
+ return objectIds.get(obj);
65
+ };
66
+ const createInventoryKey = ($refParent, $refKey) => {
67
+ // Use WeakMap-based lookup to avoid polluting the actual schema objects
68
+ return `${getObjectId($refParent)}_${$refKey}`;
69
+ };
70
+ return {
71
+ add: (entry) => {
72
+ addCount++;
73
+ const key = createInventoryKey(entry.parent, entry.key);
74
+ lookup.set(key, entry);
75
+ if (addCount % 100 === 0) {
76
+ perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`);
77
+ }
78
+ },
79
+ find: ($refParent, $refKey) => {
80
+ lookupCount++;
81
+ const key = createInventoryKey($refParent, $refKey);
82
+ const result = lookup.get(key);
83
+ if (lookupCount % 100 === 0) {
84
+ perf.log(`Inventory lookup: ${lookupCount} lookups performed`);
85
+ }
86
+ return result;
87
+ },
88
+ remove: (entry) => {
89
+ const key = createInventoryKey(entry.parent, entry.key);
90
+ lookup.delete(key);
91
+ },
92
+ getStats: () => ({ lookupCount, addCount, mapSize: lookup.size }),
93
+ };
94
+ };
95
+ /**
96
+ * Determine the container type from a JSON Pointer path.
97
+ * Analyzes the path tokens to identify the appropriate OpenAPI component container.
98
+ *
99
+ * @param path - The JSON Pointer path to analyze
100
+ * @returns The container type: "schemas", "parameters", "requestBodies", "responses", or "headers"
101
+ */
102
+ const getContainerTypeFromPath = (path) => {
103
+ const tokens = pointer_js_1.default.parse(path);
104
+ const has = (t) => tokens.includes(t);
105
+ // Prefer more specific containers first
106
+ if (has("parameters")) {
107
+ return "parameters";
108
+ }
109
+ if (has("requestBody")) {
110
+ return "requestBodies";
111
+ }
112
+ if (has("headers")) {
113
+ return "headers";
114
+ }
115
+ if (has("responses")) {
116
+ return "responses";
117
+ }
118
+ if (has("schema")) {
119
+ return "schemas";
56
120
  }
57
- return undefined;
121
+ // default: treat as schema-like
122
+ return "schemas";
58
123
  };
59
124
  /**
60
125
  * Inventories the given JSON Reference (i.e. records detailed information about it so we can
61
126
  * optimize all $refs in the schema), and then crawls the resolved value.
62
127
  */
63
- const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, options, path, pathFromRoot, }) => {
128
+ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, inventoryLookup, options, path, pathFromRoot, visitedObjects = new WeakSet(), resolvedRefs = new Map(), }) => {
129
+ perf.mark("inventory-ref-start");
64
130
  const $ref = $refKey === null ? $refParent : $refParent[$refKey];
65
131
  const $refPath = url.resolve(path, $ref.$ref);
66
- const pointer = $refs._resolve($refPath, pathFromRoot, options);
132
+ // Check cache first to avoid redundant resolution
133
+ let pointer = resolvedRefs.get($refPath);
134
+ if (!pointer) {
135
+ perf.mark("resolve-start");
136
+ pointer = $refs._resolve($refPath, pathFromRoot, options);
137
+ perf.mark("resolve-end");
138
+ perf.measure("resolve-time", "resolve-start", "resolve-end");
139
+ if (pointer) {
140
+ resolvedRefs.set($refPath, pointer);
141
+ perf.log(`Cached resolved $ref: ${$refPath}`);
142
+ }
143
+ }
67
144
  if (pointer === null) {
145
+ perf.mark("inventory-ref-end");
146
+ perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
68
147
  return;
69
148
  }
70
149
  const parsed = pointer_js_1.default.parse(pathFromRoot);
@@ -75,17 +154,23 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
75
154
  const extended = ref_js_1.default.isExtended$Ref($ref);
76
155
  indirections += pointer.indirections;
77
156
  // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
78
- const existingEntry = findInInventory(inventory, $refParent, $refKey);
157
+ perf.mark("lookup-start");
158
+ const existingEntry = inventoryLookup.find($refParent, $refKey);
159
+ perf.mark("lookup-end");
160
+ perf.measure("lookup-time", "lookup-start", "lookup-end");
79
161
  if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
80
162
  // This exact location has already been inventoried, so we don't need to process it again
81
163
  if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
82
164
  removeFromInventory(inventory, existingEntry);
165
+ inventoryLookup.remove(existingEntry);
83
166
  }
84
167
  else {
168
+ perf.mark("inventory-ref-end");
169
+ perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
85
170
  return;
86
171
  }
87
172
  }
88
- inventory.push({
173
+ const newEntry = {
89
174
  $ref, // The JSON Reference (e.g. {$ref: string})
90
175
  circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
91
176
  depth, // How far from the JSON Schema root is this $ref pointer?
@@ -98,9 +183,14 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
98
183
  parent: $refParent, // The object that contains this $ref pointer
99
184
  pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
100
185
  value: pointer.value, // The resolved value of the $ref pointer
101
- });
186
+ originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined, // The original container type in the external file
187
+ };
188
+ inventory.push(newEntry);
189
+ inventoryLookup.add(newEntry);
190
+ perf.log(`Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`);
102
191
  // Recursively crawl the resolved value
103
192
  if (!existingEntry || external) {
193
+ perf.mark("crawl-recursive-start");
104
194
  crawl({
105
195
  parent: pointer.value,
106
196
  key: null,
@@ -108,18 +198,31 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
108
198
  pathFromRoot,
109
199
  indirections: indirections + 1,
110
200
  inventory,
201
+ inventoryLookup,
111
202
  $refs,
112
203
  options,
204
+ visitedObjects,
205
+ resolvedRefs,
113
206
  });
207
+ perf.mark("crawl-recursive-end");
208
+ perf.measure("crawl-recursive-time", "crawl-recursive-start", "crawl-recursive-end");
114
209
  }
210
+ perf.mark("inventory-ref-end");
211
+ perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
115
212
  };
116
213
  /**
117
214
  * Recursively crawls the given value, and inventories all JSON references.
118
215
  */
119
- const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pathFromRoot, }) => {
216
+ const crawl = ({ $refs, indirections, inventory, inventoryLookup, key, options, parent, path, pathFromRoot, visitedObjects = new WeakSet(), resolvedRefs = new Map(), }) => {
120
217
  const obj = key === null ? parent : parent[key];
121
218
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
219
+ // Early exit if we've already processed this exact object
220
+ if (visitedObjects.has(obj)) {
221
+ perf.log(`Skipping already visited object at ${pathFromRoot}`);
222
+ return;
223
+ }
122
224
  if (ref_js_1.default.isAllowed$Ref(obj)) {
225
+ perf.log(`Found $ref at ${pathFromRoot}: ${obj.$ref}`);
123
226
  inventory$Ref({
124
227
  $refParent: parent,
125
228
  $refKey: key,
@@ -127,11 +230,16 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
127
230
  pathFromRoot,
128
231
  indirections,
129
232
  inventory,
233
+ inventoryLookup,
130
234
  $refs,
131
235
  options,
236
+ visitedObjects,
237
+ resolvedRefs,
132
238
  });
133
239
  }
134
240
  else {
241
+ // Mark this object as visited BEFORE processing its children
242
+ visitedObjects.add(obj);
135
243
  // Crawl the object in a specific order that's optimized for bundling.
136
244
  // This is important because it determines how `pathFromRoot` gets built,
137
245
  // which later determines which keys get dereferenced and which ones get remapped
@@ -162,8 +270,11 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
162
270
  pathFromRoot: keyPathFromRoot,
163
271
  indirections,
164
272
  inventory,
273
+ inventoryLookup,
165
274
  $refs,
166
275
  options,
276
+ visitedObjects,
277
+ resolvedRefs,
167
278
  });
168
279
  }
169
280
  else {
@@ -174,8 +285,11 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
174
285
  pathFromRoot: keyPathFromRoot,
175
286
  indirections,
176
287
  inventory,
288
+ inventoryLookup,
177
289
  $refs,
178
290
  options,
291
+ visitedObjects,
292
+ resolvedRefs,
179
293
  });
180
294
  }
181
295
  }
@@ -183,29 +297,15 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
183
297
  }
184
298
  };
185
299
  /**
186
- * Re-maps every $ref pointer, so that they're all relative to the root of the JSON Schema.
187
- * Each referenced value is dereferenced EXACTLY ONCE. All subsequent references to the same
188
- * value are re-mapped to point to the first reference.
189
- *
190
- * @example: {
191
- * first: { $ref: somefile.json#/some/part },
192
- * second: { $ref: somefile.json#/another/part },
193
- * third: { $ref: somefile.json },
194
- * fourth: { $ref: somefile.json#/some/part/sub/part }
195
- * }
196
- *
197
- * In this example, there are four references to the same file, but since the third reference points
198
- * to the ENTIRE file, that's the only one we need to dereference. The other three can just be
199
- * remapped to point inside the third one.
200
- *
201
- * On the other hand, if the third reference DIDN'T exist, then the first and second would both need
202
- * to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
203
- * need to be dereferenced, because it can be remapped to point inside the first one.
204
- *
205
- * @param inventory
300
+ * Remap external refs by hoisting resolved values into a shared container in the root schema
301
+ * and pointing all occurrences to those internal definitions. Internal refs remain internal.
206
302
  */
207
- function remap(inventory) {
303
+ function remap(parser, inventory) {
304
+ perf.log(`Starting remap with ${inventory.length} inventory entries`);
305
+ perf.mark("remap-start");
306
+ const root = parser.schema;
208
307
  // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
308
+ perf.mark("sort-inventory-start");
209
309
  inventory.sort((a, b) => {
210
310
  if (a.file !== b.file) {
211
311
  // Group all the $refs that point to the same file
@@ -246,54 +346,174 @@ function remap(inventory) {
246
346
  }
247
347
  }
248
348
  });
249
- let file, hash, pathFromRoot;
349
+ perf.mark("sort-inventory-end");
350
+ perf.measure("sort-inventory-time", "sort-inventory-start", "sort-inventory-end");
351
+ perf.log(`Sorted ${inventory.length} inventory entries`);
352
+ // Ensure or return a container by component type. Prefer OpenAPI-aware placement;
353
+ // otherwise use existing root containers; otherwise create components/*.
354
+ const ensureContainer = (type) => {
355
+ const isOas3 = !!(root && typeof root === "object" && typeof root.openapi === "string");
356
+ const isOas2 = !!(root && typeof root === "object" && typeof root.swagger === "string");
357
+ if (isOas3) {
358
+ if (!root.components || typeof root.components !== "object") {
359
+ root.components = {};
360
+ }
361
+ if (!root.components[type] || typeof root.components[type] !== "object") {
362
+ root.components[type] = {};
363
+ }
364
+ return { obj: root.components[type], prefix: `#/components/${type}` };
365
+ }
366
+ if (isOas2) {
367
+ if (type === "schemas") {
368
+ if (!root.definitions || typeof root.definitions !== "object") {
369
+ root.definitions = {};
370
+ }
371
+ return { obj: root.definitions, prefix: "#/definitions" };
372
+ }
373
+ if (type === "parameters") {
374
+ if (!root.parameters || typeof root.parameters !== "object") {
375
+ root.parameters = {};
376
+ }
377
+ return { obj: root.parameters, prefix: "#/parameters" };
378
+ }
379
+ if (type === "responses") {
380
+ if (!root.responses || typeof root.responses !== "object") {
381
+ root.responses = {};
382
+ }
383
+ return { obj: root.responses, prefix: "#/responses" };
384
+ }
385
+ // requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions
386
+ if (!root.definitions || typeof root.definitions !== "object") {
387
+ root.definitions = {};
388
+ }
389
+ return { obj: root.definitions, prefix: "#/definitions" };
390
+ }
391
+ // No explicit version: prefer existing containers
392
+ if (root && typeof root === "object") {
393
+ if (root.components && typeof root.components === "object") {
394
+ if (!root.components[type] || typeof root.components[type] !== "object") {
395
+ root.components[type] = {};
396
+ }
397
+ return { obj: root.components[type], prefix: `#/components/${type}` };
398
+ }
399
+ if (root.definitions && typeof root.definitions === "object") {
400
+ return { obj: root.definitions, prefix: "#/definitions" };
401
+ }
402
+ // Create components/* by default if nothing exists
403
+ if (!root.components || typeof root.components !== "object") {
404
+ root.components = {};
405
+ }
406
+ if (!root.components[type] || typeof root.components[type] !== "object") {
407
+ root.components[type] = {};
408
+ }
409
+ return { obj: root.components[type], prefix: `#/components/${type}` };
410
+ }
411
+ // Fallback
412
+ root.definitions = root.definitions || {};
413
+ return { obj: root.definitions, prefix: "#/definitions" };
414
+ };
415
+ /**
416
+ * Choose the appropriate component container for bundling.
417
+ * Prioritizes the original container type from external files over usage location.
418
+ *
419
+ * @param entry - The inventory entry containing reference information
420
+ * @returns The container type to use for bundling
421
+ */
422
+ const chooseComponent = (entry) => {
423
+ // If we have the original container type from the external file, use it
424
+ if (entry.originalContainerType) {
425
+ return entry.originalContainerType;
426
+ }
427
+ // Fallback to usage path for internal references or when original type is not available
428
+ return getContainerTypeFromPath(entry.pathFromRoot);
429
+ };
430
+ // Track names per (container prefix) and per target
431
+ const targetToNameByPrefix = new Map();
432
+ const usedNamesByObj = new Map();
433
+ const sanitize = (name) => name.replace(/[^A-Za-z0-9_-]/g, "_");
434
+ const baseName = (filePath) => {
435
+ try {
436
+ const withoutHash = filePath.split("#")[0];
437
+ const parts = withoutHash.split("/");
438
+ const filename = parts[parts.length - 1] || "schema";
439
+ const dot = filename.lastIndexOf(".");
440
+ return sanitize(dot > 0 ? filename.substring(0, dot) : filename);
441
+ }
442
+ catch {
443
+ return "schema";
444
+ }
445
+ };
446
+ const lastToken = (hash) => {
447
+ if (!hash || hash === "#") {
448
+ return "root";
449
+ }
450
+ const tokens = hash.replace(/^#\//, "").split("/");
451
+ return sanitize(tokens[tokens.length - 1] || "root");
452
+ };
453
+ const uniqueName = (containerObj, proposed) => {
454
+ if (!usedNamesByObj.has(containerObj)) {
455
+ usedNamesByObj.set(containerObj, new Set(Object.keys(containerObj || {})));
456
+ }
457
+ const used = usedNamesByObj.get(containerObj);
458
+ let name = proposed;
459
+ let i = 2;
460
+ while (used.has(name)) {
461
+ name = `${proposed}_${i++}`;
462
+ }
463
+ used.add(name);
464
+ return name;
465
+ };
466
+ perf.mark("remap-loop-start");
250
467
  for (const entry of inventory) {
251
- // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
468
+ // Safety check: ensure entry and entry.$ref are valid objects
469
+ if (!entry || !entry.$ref || typeof entry.$ref !== "object") {
470
+ perf.warn(`Skipping invalid inventory entry:`, entry);
471
+ continue;
472
+ }
473
+ // Keep internal refs internal
252
474
  if (!entry.external) {
253
- // This $ref already resolves to the main JSON Schema file
254
- entry.$ref.$ref = entry.hash;
475
+ if (entry.$ref && typeof entry.$ref === "object") {
476
+ entry.$ref.$ref = entry.hash;
477
+ }
478
+ continue;
479
+ }
480
+ // Avoid changing direct self-references; keep them internal
481
+ if (entry.circular) {
482
+ if (entry.$ref && typeof entry.$ref === "object") {
483
+ entry.$ref.$ref = entry.pathFromRoot;
484
+ }
485
+ continue;
255
486
  }
256
- else if (entry.file === file && entry.hash === hash) {
257
- // This $ref points to the same value as the prevous $ref, so remap it to the same path
258
- entry.$ref.$ref = pathFromRoot;
487
+ // Choose appropriate container based on original location in external file
488
+ const component = chooseComponent(entry);
489
+ const { obj: container, prefix } = ensureContainer(component);
490
+ const targetKey = `${entry.file}::${entry.hash}`;
491
+ if (!targetToNameByPrefix.has(prefix)) {
492
+ targetToNameByPrefix.set(prefix, new Map());
259
493
  }
260
- else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
261
- // This $ref points to a sub-value of the prevous $ref, so remap it beneath that path
262
- entry.$ref.$ref = pointer_js_1.default.join(pathFromRoot, pointer_js_1.default.parse(entry.hash.replace(hash, "#")));
494
+ const namesForPrefix = targetToNameByPrefix.get(prefix);
495
+ let defName = namesForPrefix.get(targetKey);
496
+ if (!defName) {
497
+ const proposed = `${baseName(entry.file)}_${lastToken(entry.hash)}`;
498
+ defName = uniqueName(container, proposed);
499
+ namesForPrefix.set(targetKey, defName);
500
+ // Store the resolved value under the container
501
+ container[defName] = entry.value;
502
+ }
503
+ // Point the occurrence to the internal definition, preserving extensions
504
+ const refPath = `${prefix}/${defName}`;
505
+ if (entry.extended && entry.$ref && typeof entry.$ref === "object") {
506
+ entry.$ref.$ref = refPath;
263
507
  }
264
508
  else {
265
- // We've moved to a new file or new hash
266
- file = entry.file;
267
- hash = entry.hash;
268
- pathFromRoot = entry.pathFromRoot;
269
- // This is the first $ref to point to this value, so dereference the value.
270
- // Any other $refs that point to the same value will point to this $ref instead
271
- entry.$ref = entry.parent[entry.key] = ref_js_1.default.dereference(entry.$ref, entry.value);
272
- if (entry.circular) {
273
- // This $ref points to itself
274
- entry.$ref.$ref = entry.pathFromRoot;
275
- }
509
+ entry.parent[entry.key] = { $ref: refPath };
276
510
  }
277
511
  }
278
- // we want to ensure that any $refs that point to another $ref are remapped to point to the final value
279
- // let hadChange = true;
280
- // while (hadChange) {
281
- // hadChange = false;
282
- // for (const entry of inventory) {
283
- // if (entry.$ref && typeof entry.$ref === "object" && "$ref" in entry.$ref) {
284
- // const resolved = inventory.find((e: InventoryEntry) => e.pathFromRoot === entry.$ref.$ref);
285
- // if (resolved) {
286
- // const resolvedPointsToAnotherRef =
287
- // resolved.$ref && typeof resolved.$ref === "object" && "$ref" in resolved.$ref;
288
- // if (resolvedPointsToAnotherRef && entry.$ref.$ref !== resolved.$ref.$ref) {
289
- // // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
290
- // entry.$ref.$ref = resolved.$ref.$ref;
291
- // hadChange = true;
292
- // }
293
- // }
294
- // }
295
- // }
296
- // }
512
+ perf.mark("remap-loop-end");
513
+ perf.measure("remap-loop-time", "remap-loop-start", "remap-loop-end");
514
+ perf.mark("remap-end");
515
+ perf.measure("remap-total-time", "remap-start", "remap-end");
516
+ perf.log(`Completed remap of ${inventory.length} entries`);
297
517
  }
298
518
  function removeFromInventory(inventory, entry) {
299
519
  const index = inventory.indexOf(entry);
@@ -309,8 +529,14 @@ function removeFromInventory(inventory, entry) {
309
529
  */
310
530
  const bundle = (parser, options) => {
311
531
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
532
+ perf.mark("bundle-start");
312
533
  // Build an inventory of all $ref pointers in the JSON Schema
313
534
  const inventory = [];
535
+ const inventoryLookup = createInventoryLookup();
536
+ perf.log("Starting crawl phase");
537
+ perf.mark("crawl-phase-start");
538
+ const visitedObjects = new WeakSet();
539
+ const resolvedRefs = new Map(); // Cache for resolved $ref targets
314
540
  crawl({
315
541
  parent: parser,
316
542
  key: "schema",
@@ -318,10 +544,40 @@ const bundle = (parser, options) => {
318
544
  pathFromRoot: "#",
319
545
  indirections: 0,
320
546
  inventory,
547
+ inventoryLookup,
321
548
  $refs: parser.$refs,
322
549
  options,
550
+ visitedObjects,
551
+ resolvedRefs,
323
552
  });
553
+ perf.mark("crawl-phase-end");
554
+ perf.measure("crawl-phase-time", "crawl-phase-start", "crawl-phase-end");
555
+ const stats = inventoryLookup.getStats();
556
+ perf.log(`Crawl phase complete. Found ${inventory.length} $refs. Lookup stats:`, stats);
324
557
  // Remap all $ref pointers
325
- remap(inventory);
558
+ perf.log("Starting remap phase");
559
+ perf.mark("remap-phase-start");
560
+ remap(parser, inventory);
561
+ perf.mark("remap-phase-end");
562
+ perf.measure("remap-phase-time", "remap-phase-start", "remap-phase-end");
563
+ perf.mark("bundle-end");
564
+ perf.measure("bundle-total-time", "bundle-start", "bundle-end");
565
+ perf.log("Bundle complete. Performance summary:");
566
+ // Log final stats
567
+ const finalStats = inventoryLookup.getStats();
568
+ perf.log(`Final inventory stats:`, finalStats);
569
+ perf.log(`Resolved refs cache size: ${resolvedRefs.size}`);
570
+ if (DEBUG_PERFORMANCE) {
571
+ // Log all performance measures
572
+ const measures = performance.getEntriesByType("measure");
573
+ measures.forEach((measure) => {
574
+ if (measure.name.includes("time")) {
575
+ console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
576
+ }
577
+ });
578
+ // Clear performance marks and measures for next run
579
+ performance.clearMarks();
580
+ performance.clearMeasures();
581
+ }
326
582
  };
327
583
  exports.bundle = bundle;
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const ref_js_1 = __importDefault(require("./ref.js"));
40
+ const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
40
41
  const pointer_js_1 = __importDefault(require("./pointer.js"));
41
42
  const ono_1 = require("@jsdevtools/ono");
42
43
  const url = __importStar(require("./util/url.js"));
@@ -160,10 +161,11 @@ function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, de
160
161
  }
161
162
  return {
162
163
  circular: cache.circular,
163
- value: Object.assign({}, cache.value, extraKeys),
164
+ value: Object.assign({}, (0, cloneDeep_1.default)(cache.value), extraKeys),
164
165
  };
165
166
  }
166
- return cache;
167
+ // Return a deep-cloned value so each occurrence is an independent copy
168
+ return { circular: cache.circular, value: (0, cloneDeep_1.default)(cache.value) };
167
169
  }
168
170
  const pointer = $refs._resolve($refPath, path, options);
169
171
  if (pointer === null) {
@@ -3,7 +3,7 @@ import type { JSONSchema } from "./types/index.js";
3
3
  interface ResolvedInput {
4
4
  path: string;
5
5
  schema: string | JSONSchema | Buffer | Awaited<JSONSchema> | undefined;
6
- type: 'file' | 'json' | 'url';
6
+ type: "file" | "json" | "url";
7
7
  }
8
8
  export declare const getResolvedInput: ({ pathOrUrlOrSchema, }: {
9
9
  pathOrUrlOrSchema: JSONSchema | string | unknown;
@@ -73,5 +73,5 @@ export declare class $RefParser {
73
73
  schema: JSONSchema;
74
74
  }>;
75
75
  }
76
- export { sendRequest } from './resolvers/url.js';
76
+ export { sendRequest } from "./resolvers/url.js";
77
77
  export type { JSONSchema } from "./types/index.js";