Skip to content

Commit 072a9ae

Browse files
committed
fix: maximum call stack exceeded
1 parent ebd818d commit 072a9ae

12 files changed

Lines changed: 130 additions & 273 deletions

File tree

.changeset/smooth-maps-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/json-schema-ref-parser": patch
3+
---
4+
5+
**fix**: pass seen references through crawl stack

dev/inputs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export const inputs = {
99
opencode: path.resolve(getSpecsPath(), '3.1.x', 'opencode.yaml'),
1010
petstore:
1111
'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml',
12-
redfish: 'http://redfish.dmtf.org/schemas/v1/Message.v1_2_1.yaml',
12+
redfish:
13+
'https://raw.githubusercontent.com/DMTF/Redfish-Publications/refs/heads/main/openapi/openapi.yaml',
1314
scalar: 'scalar:@scalar/access-service',
1415
transformers: path.resolve(getSpecsPath(), '3.1.x', 'transformers.json'),
1516
validators: path.resolve(getSpecsPath(), '3.1.x', 'validators.yaml'),

packages/json-schema-ref-parser/README.md

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,6 @@ JavaScript objects.
5858
instance
5959
- Compatible with Node LTS and beyond, and all major web browsers on Windows, Mac, and Linux
6060

61-
## Example
62-
63-
```javascript
64-
import { $RefParser } from '@hey-api/json-schema-ref-parser';
65-
66-
try {
67-
const parser = new $RefParser();
68-
await parser.dereference({ pathOrUrlOrSchema: mySchema });
69-
console.log(parser.schema.definitions.person.properties.firstName);
70-
} catch (err) {
71-
console.error(err);
72-
}
73-
```
74-
7561
### New in this fork (@hey-api)
7662

7763
- **Multiple inputs with `bundleMany`**: Merge and bundle several OpenAPI/JSON Schema inputs (files, URLs, or raw objects) into a single schema. Components are prefixed to avoid name collisions, paths are namespaced on conflict, and `$ref`s are rewritten accordingly.

packages/json-schema-ref-parser/src/bundle.ts

Lines changed: 20 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,9 @@ import Pointer from './pointer';
44
import $Ref from './ref';
55
import type $Refs from './refs';
66
import type { JSONSchema } from './types';
7+
import { MissingPointerError } from './util/errors';
78
import * as url from './util/url';
89

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-
log: (message: string, ...args: any[]) =>
15-
DEBUG_PERFORMANCE && console.log('[PERF] ' + message, ...args),
16-
mark: (name: string) => DEBUG_PERFORMANCE && performance.mark(name),
17-
measure: (name: string, start: string, end: string) =>
18-
DEBUG_PERFORMANCE && performance.measure(name, start, end),
19-
warn: (message: string, ...args: any[]) =>
20-
DEBUG_PERFORMANCE && console.warn('[PERF] ' + message, ...args),
21-
};
22-
2310
export interface InventoryEntry {
2411
$ref: any;
2512
circular: any;
@@ -43,8 +30,6 @@ const createInventoryLookup = () => {
4330
const lookup = new Map<string, InventoryEntry>();
4431
const objectIds = new WeakMap<object, string>(); // Use WeakMap to avoid polluting objects
4532
let idCounter = 0;
46-
let lookupCount = 0;
47-
let addCount = 0;
4833

4934
const getObjectId = (obj: any) => {
5035
if (!objectIds.has(obj)) {
@@ -59,23 +44,14 @@ const createInventoryLookup = () => {
5944

6045
return {
6146
add: (entry: InventoryEntry) => {
62-
addCount++;
6347
const key = createInventoryKey(entry.parent, entry.key);
6448
lookup.set(key, entry);
65-
if (addCount % 100 === 0) {
66-
perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`);
67-
}
6849
},
6950
find: ($refParent: any, $refKey: any) => {
70-
lookupCount++;
7151
const key = createInventoryKey($refParent, $refKey);
7252
const result = lookup.get(key);
73-
if (lookupCount % 100 === 0) {
74-
perf.log(`Inventory lookup: ${lookupCount} lookups performed`);
75-
}
7653
return result;
7754
},
78-
getStats: () => ({ addCount, lookupCount, mapSize: lookup.size }),
7955
remove: (entry: InventoryEntry) => {
8056
const key = createInventoryKey(entry.parent, entry.key);
8157
lookup.delete(key);
@@ -171,29 +147,29 @@ const inventory$Ref = <S extends object = JSONSchema>({
171147
*/
172148
visitedObjects?: WeakSet<object>;
173149
}) => {
174-
perf.mark('inventory-ref-start');
175150
const $ref = $refKey === null ? $refParent : $refParent[$refKey];
176151
const $refPath = url.resolve(path, $ref.$ref);
177152

178153
// Check cache first to avoid redundant resolution
179154
let pointer = resolvedRefs.get($refPath);
180155
if (!pointer) {
181-
perf.mark('resolve-start');
182-
pointer = $refs._resolve($refPath, pathFromRoot, options);
183-
perf.mark('resolve-end');
184-
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+
// 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+
}
185166

186167
if (pointer) {
187168
resolvedRefs.set($refPath, pointer);
188-
perf.log(`Cached resolved $ref: ${$refPath}`);
189169
}
190170
}
191171

192-
if (pointer === null) {
193-
perf.mark('inventory-ref-end');
194-
perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
195-
return;
196-
}
172+
if (pointer === null) return;
197173

198174
const parsed = Pointer.parse(pathFromRoot);
199175
const depth = parsed.length;
@@ -204,19 +180,14 @@ const inventory$Ref = <S extends object = JSONSchema>({
204180
indirections += pointer.indirections;
205181

206182
// Check if this exact location (parent + key + pathFromRoot) has already been inventoried
207-
perf.mark('lookup-start');
208183
const existingEntry = inventoryLookup.find($refParent, $refKey);
209-
perf.mark('lookup-end');
210-
perf.measure('lookup-time', 'lookup-start', 'lookup-end');
211184

212185
if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
213186
// This exact location has already been inventoried, so we don't need to process it again
214187
if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
215188
removeFromInventory(inventory, existingEntry);
216189
inventoryLookup.remove(existingEntry);
217190
} else {
218-
perf.mark('inventory-ref-end');
219-
perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
220191
return;
221192
}
222193
}
@@ -246,13 +217,8 @@ const inventory$Ref = <S extends object = JSONSchema>({
246217
inventory.push(newEntry);
247218
inventoryLookup.add(newEntry);
248219

249-
perf.log(
250-
`Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`,
251-
);
252-
253220
// Recursively crawl the resolved value
254221
if (!existingEntry || external) {
255-
perf.mark('crawl-recursive-start');
256222
crawl({
257223
$refs,
258224
indirections: indirections + 1,
@@ -266,12 +232,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
266232
resolvedRefs,
267233
visitedObjects,
268234
});
269-
perf.mark('crawl-recursive-end');
270-
perf.measure('crawl-recursive-time', 'crawl-recursive-start', 'crawl-recursive-end');
271235
}
272-
273-
perf.mark('inventory-ref-end');
274-
perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
275236
};
276237

277238
/**
@@ -330,13 +291,9 @@ const crawl = <S extends object = JSONSchema>({
330291

331292
if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) {
332293
// Early exit if we've already processed this exact object
333-
if (visitedObjects.has(obj)) {
334-
perf.log(`Skipping already visited object at ${pathFromRoot}`);
335-
return;
336-
}
294+
if (visitedObjects.has(obj)) return;
337295

338296
if ($Ref.isAllowed$Ref(obj)) {
339-
perf.log(`Found $ref at ${pathFromRoot}: ${(obj as any).$ref}`);
340297
inventory$Ref({
341298
$refKey: key,
342299
$refParent: parent,
@@ -369,7 +326,7 @@ const crawl = <S extends object = JSONSchema>({
369326
// This produces the shortest possible bundled references
370327
return a.length - b.length;
371328
}
372-
}) as (keyof typeof obj)[];
329+
}) as Array<keyof typeof obj>;
373330

374331
for (const key of keys) {
375332
const keyPath = Pointer.join(path, key);
@@ -414,13 +371,10 @@ const crawl = <S extends object = JSONSchema>({
414371
* Remap external refs by hoisting resolved values into a shared container in the root schema
415372
* and pointing all occurrences to those internal definitions. Internal refs remain internal.
416373
*/
417-
function remap(parser: $RefParser, inventory: InventoryEntry[]) {
418-
perf.log(`Starting remap with ${inventory.length} inventory entries`);
419-
perf.mark('remap-start');
374+
function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
420375
const root = parser.schema as any;
421376

422377
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
423-
perf.mark('sort-inventory-start');
424378
inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
425379
if (a.file !== b.file) {
426380
// Group all the $refs that point to the same file
@@ -455,11 +409,6 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
455409
}
456410
});
457411

458-
perf.mark('sort-inventory-end');
459-
perf.measure('sort-inventory-time', 'sort-inventory-start', 'sort-inventory-end');
460-
461-
perf.log(`Sorted ${inventory.length} inventory entries`);
462-
463412
// Ensure or return a container by component type. Prefer OpenAPI-aware placement;
464413
// otherwise use existing root containers; otherwise create components/*.
465414
const ensureContainer = (
@@ -583,11 +532,9 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
583532
used.add(name);
584533
return name;
585534
};
586-
perf.mark('remap-loop-start');
587535
for (const entry of inventory) {
588536
// Safety check: ensure entry and entry.$ref are valid objects
589537
if (!entry || !entry.$ref || typeof entry.$ref !== 'object') {
590-
perf.warn(`Skipping invalid inventory entry:`, entry);
591538
continue;
592539
}
593540

@@ -654,16 +601,9 @@ function remap(parser: $RefParser, inventory: InventoryEntry[]) {
654601
entry.parent[entry.key] = { $ref: refPath };
655602
}
656603
}
657-
perf.mark('remap-loop-end');
658-
perf.measure('remap-loop-time', 'remap-loop-start', 'remap-loop-end');
659-
660-
perf.mark('remap-end');
661-
perf.measure('remap-total-time', 'remap-start', 'remap-end');
662-
663-
perf.log(`Completed remap of ${inventory.length} entries`);
664604
}
665605

666-
function removeFromInventory(inventory: InventoryEntry[], entry: any) {
606+
function removeFromInventory(inventory: Array<InventoryEntry>, entry: any) {
667607
const index = inventory.indexOf(entry);
668608
inventory.splice(index, 1);
669609
}
@@ -676,19 +616,12 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
676616
* @param parser
677617
* @param options
678618
*/
679-
export const bundle = (parser: $RefParser, options: ParserOptions) => {
680-
// console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
681-
perf.mark('bundle-start');
682-
683-
// Build an inventory of all $ref pointers in the JSON Schema
684-
const inventory: InventoryEntry[] = [];
619+
export function bundle(parser: $RefParser, options: ParserOptions): void {
620+
const inventory: Array<InventoryEntry> = [];
685621
const inventoryLookup = createInventoryLookup();
686622

687-
perf.log('Starting crawl phase');
688-
perf.mark('crawl-phase-start');
689-
690623
const visitedObjects = new WeakSet<object>();
691-
const resolvedRefs = new Map<string, any>(); // Cache for resolved $ref targets
624+
const resolvedRefs = new Map<string, any>();
692625

693626
crawl<JSONSchema>({
694627
$refs: parser.$refs,
@@ -704,40 +637,5 @@ export const bundle = (parser: $RefParser, options: ParserOptions) => {
704637
visitedObjects,
705638
});
706639

707-
perf.mark('crawl-phase-end');
708-
perf.measure('crawl-phase-time', 'crawl-phase-start', 'crawl-phase-end');
709-
710-
const stats = inventoryLookup.getStats();
711-
perf.log(`Crawl phase complete. Found ${inventory.length} $refs. Lookup stats:`, stats);
712-
713-
// Remap all $ref pointers
714-
perf.log('Starting remap phase');
715-
perf.mark('remap-phase-start');
716640
remap(parser, inventory);
717-
perf.mark('remap-phase-end');
718-
perf.measure('remap-phase-time', 'remap-phase-start', 'remap-phase-end');
719-
720-
perf.mark('bundle-end');
721-
perf.measure('bundle-total-time', 'bundle-start', 'bundle-end');
722-
723-
perf.log('Bundle complete. Performance summary:');
724-
725-
// Log final stats
726-
const finalStats = inventoryLookup.getStats();
727-
perf.log(`Final inventory stats:`, finalStats);
728-
perf.log(`Resolved refs cache size: ${resolvedRefs.size}`);
729-
730-
if (DEBUG_PERFORMANCE) {
731-
// Log all performance measures
732-
const measures = performance.getEntriesByType('measure');
733-
measures.forEach((measure) => {
734-
if (measure.name.includes('time')) {
735-
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
736-
}
737-
});
738-
739-
// Clear performance marks and measures for next run
740-
performance.clearMarks();
741-
performance.clearMeasures();
742-
}
743-
};
641+
}

packages/json-schema-ref-parser/src/dereference.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ono } from '@jsdevtools/ono';
22

3-
import type { $RefParser } from '.';
43
import type { DereferenceOptions, ParserOptions } from './options';
54
import Pointer from './pointer';
65
import $Ref from './ref';
@@ -9,33 +8,6 @@ import type { JSONSchema } from './types';
98
import { TimeoutError } from './util/errors';
109
import * as url from './util/url';
1110

12-
export default dereference;
13-
14-
/**
15-
* Crawls the JSON schema, finds all JSON references, and dereferences them.
16-
* This method mutates the JSON schema object, replacing JSON references with their resolved value.
17-
*
18-
* @param parser
19-
* @param options
20-
*/
21-
function dereference(parser: $RefParser, options: ParserOptions) {
22-
const start = Date.now();
23-
// console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
24-
const dereferenced = crawl<JSONSchema>(
25-
parser.schema,
26-
parser.$refs._root$Ref.path!,
27-
'#',
28-
new Set(),
29-
new Set(),
30-
new Map(),
31-
parser.$refs,
32-
options,
33-
start,
34-
);
35-
parser.$refs.circular = dereferenced.circular;
36-
parser.schema = dereferenced.value;
37-
}
38-
3911
/**
4012
* Recursively crawls the given value, and dereferences any JSON references.
4113
*

0 commit comments

Comments
 (0)