fork of hey-api/openapi-ts because I need some additional things

Remove fixDanglingRefs() and cross-file reference test

The fixDanglingRefs() function was handling invalid OpenAPI/JSON Schema usage where external files use local-looking refs (e.g., #/components/schemas/SchemaB) to reference schemas in other external files.

Per JSON Schema and OpenAPI specifications, a ref starting with # is a local reference to the current document only. Cross-file references must use proper relative or absolute URIs.

Removed:
- fixDanglingRefs() function from bundle.ts
- Cross-file reference test case
- Test spec files: cross-file-ref-*.json
- Associated snapshot file

Invalid specs should fail with clear errors rather than being silently "fixed". Users with such specs should be asked to use correct syntax like "file2.json#/components/schemas/SchemaB".

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

-259
-64
packages/json-schema-ref-parser/src/__tests__/__snapshots__/cross-file-ref-main.json
··· 1 - { 2 - "openapi": "3.0.0", 3 - "info": { 4 - "title": "Cross-file Reference Test", 5 - "version": "1.0.0" 6 - }, 7 - "paths": { 8 - "/resource-a": { 9 - "get": { 10 - "responses": { 11 - "200": { 12 - "description": "Returns SchemaA", 13 - "content": { 14 - "application/json": { 15 - "schema": { 16 - "$ref": "#/components/schemas/SchemaA" 17 - } 18 - } 19 - } 20 - } 21 - } 22 - } 23 - }, 24 - "/resource-b": { 25 - "get": { 26 - "responses": { 27 - "200": { 28 - "description": "Returns SchemaB", 29 - "content": { 30 - "application/json": { 31 - "schema": { 32 - "$ref": "#/components/schemas/SchemaB" 33 - } 34 - } 35 - } 36 - } 37 - } 38 - } 39 - } 40 - }, 41 - "components": { 42 - "schemas": { 43 - "SchemaA": { 44 - "type": "object", 45 - "properties": { 46 - "typeField": { 47 - "$ref": "#/components/schemas/SchemaB" 48 - }, 49 - "name": { 50 - "type": "string" 51 - } 52 - } 53 - }, 54 - "SchemaB": { 55 - "type": "string", 56 - "enum": [ 57 - "TypeA", 58 - "TypeB", 59 - "TypeC" 60 - ] 61 - } 62 - } 63 - } 64 - }
-12
packages/json-schema-ref-parser/src/__tests__/bundle.test.ts
··· 81 81 82 82 await expectBundledSchemaToMatchSnapshot(schema, 'redfish-like.json'); 83 83 }); 84 - 85 - it('fixes cross-file references (schemas in different external files)', async () => { 86 - const refParser = new $RefParser(); 87 - const pathOrUrlOrSchema = path.join( 88 - getSpecsPath(), 89 - 'json-schema-ref-parser', 90 - 'cross-file-ref-main.json', 91 - ); 92 - const schema = await refParser.bundle({ pathOrUrlOrSchema }); 93 - 94 - await expectBundledSchemaToMatchSnapshot(schema, 'cross-file-ref-main.json'); 95 - }); 96 84 });
-115
packages/json-schema-ref-parser/src/bundle.ts
··· 624 624 } 625 625 626 626 /** 627 - * Fix dangling $refs that point to schemas in the root spec but don't exist. 628 - * This can happen when an external file references another external file's schema 629 - * using a local-looking ref like #/components/schemas/SchemaName. 630 - * 631 - * @param parser 632 - */ 633 - function fixDanglingRefs(parser: $RefParser): void { 634 - const root = parser.schema as any; 635 - if (!root || typeof root !== 'object') { 636 - return; 637 - } 638 - 639 - // Get all hoisted schemas from components 640 - const containers = [ 641 - { obj: root.components?.schemas, prefix: '#/components/schemas/' }, 642 - { obj: root.components?.parameters, prefix: '#/components/parameters/' }, 643 - { obj: root.components?.requestBodies, prefix: '#/components/requestBodies/' }, 644 - { obj: root.components?.responses, prefix: '#/components/responses/' }, 645 - { obj: root.components?.headers, prefix: '#/components/headers/' }, 646 - { obj: root.definitions, prefix: '#/definitions/' }, 647 - { obj: root.parameters, prefix: '#/parameters/' }, 648 - { obj: root.responses, prefix: '#/responses/' }, 649 - ].filter((c) => c.obj && typeof c.obj === 'object'); 650 - 651 - // Build a map of simple schema names to their hoisted full names 652 - // Since we now use unprefixed names when there's no conflict, we need to handle both cases 653 - const schemaNameMap = new Map<string, Array<{ fullName: string; prefix: string }>>(); 654 - 655 - for (const container of containers) { 656 - for (const fullName of Object.keys(container.obj)) { 657 - // First, add the full name itself as a candidate (for unprefixed schemas) 658 - if (!schemaNameMap.has(fullName)) { 659 - schemaNameMap.set(fullName, []); 660 - } 661 - schemaNameMap.get(fullName)!.push({ 662 - fullName, 663 - prefix: container.prefix, 664 - }); 665 - 666 - // Extract the original schema name from the hoisted name if it has a prefix 667 - // Hoisted names with conflicts are "filename_SchemaName" 668 - // Try to match the pattern and extract SchemaName 669 - const parts = fullName.split('_'); 670 - if (parts.length >= 2) { 671 - // The last part(s) might be the original schema name 672 - // Try progressively longer suffixes 673 - for (let i = 1; i < parts.length; i++) { 674 - const schemaName = parts.slice(i).join('_'); 675 - if (!schemaNameMap.has(schemaName)) { 676 - schemaNameMap.set(schemaName, []); 677 - } 678 - schemaNameMap.get(schemaName)!.push({ 679 - fullName, 680 - prefix: container.prefix, 681 - }); 682 - } 683 - } 684 - } 685 - } 686 - 687 - // Find and fix all dangling $refs 688 - const fixRefs = (obj: any, visited = new WeakSet<object>()): void => { 689 - if (!obj || typeof obj !== 'object' || ArrayBuffer.isView(obj)) { 690 - return; 691 - } 692 - 693 - if (visited.has(obj)) { 694 - return; 695 - } 696 - visited.add(obj); 697 - 698 - if ($Ref.is$Ref(obj)) { 699 - const ref = obj.$ref; 700 - if (typeof ref === 'string') { 701 - // Check if this is a dangling internal ref 702 - for (const container of containers) { 703 - if (ref.startsWith(container.prefix)) { 704 - const schemaName = ref.substring(container.prefix.length); 705 - 706 - // Check if the exact name exists 707 - if (container.obj[schemaName]) { 708 - continue; // Not dangling 709 - } 710 - 711 - // Try to find a hoisted schema that matches this name 712 - const candidates = schemaNameMap.get(schemaName) || []; 713 - 714 - if (candidates.length === 1) { 715 - // Unambiguous match - fix the ref 716 - const candidate = candidates[0]!; 717 - obj.$ref = `${candidate.prefix}${candidate.fullName}`; 718 - console.warn(`Fixed dangling $ref: ${ref} -> ${obj.$ref}`); 719 - } else if (candidates.length > 1) { 720 - // Multiple matches - log warning but don't change 721 - console.warn( 722 - `Ambiguous dangling $ref: ${ref} could refer to: ${candidates.map((c) => `${c.prefix}${c.fullName}`).join(', ')}`, 723 - ); 724 - } 725 - // If no candidates, leave as-is (will remain dangling) 726 - } 727 - } 728 - } 729 - } 730 - 731 - // Recursively fix refs in nested objects 732 - for (const value of Object.values(obj)) { 733 - fixRefs(value, visited); 734 - } 735 - }; 736 - 737 - fixRefs(root); 738 - } 739 - 740 - /** 741 627 * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that 742 628 * only has *internal* references, not any *external* references. 743 629 * This method mutates the JSON schema object, adding new references and re-mapping existing ones. ··· 767 653 }); 768 654 769 655 remap(parser, inventory); 770 - fixDanglingRefs(parser); 771 656 }
-17
specs/json-schema-ref-parser/cross-file-ref-file1.json
··· 1 - { 2 - "components": { 3 - "schemas": { 4 - "SchemaA": { 5 - "type": "object", 6 - "properties": { 7 - "typeField": { 8 - "$ref": "#/components/schemas/SchemaB" 9 - }, 10 - "name": { 11 - "type": "string" 12 - } 13 - } 14 - } 15 - } 16 - } 17 - }
-10
specs/json-schema-ref-parser/cross-file-ref-file2.json
··· 1 - { 2 - "components": { 3 - "schemas": { 4 - "SchemaB": { 5 - "type": "string", 6 - "enum": ["TypeA", "TypeB", "TypeC"] 7 - } 8 - } 9 - } 10 - }
-41
specs/json-schema-ref-parser/cross-file-ref-main.json
··· 1 - { 2 - "openapi": "3.0.0", 3 - "info": { 4 - "title": "Cross-file Reference Test", 5 - "version": "1.0.0" 6 - }, 7 - "paths": { 8 - "/resource-a": { 9 - "get": { 10 - "responses": { 11 - "200": { 12 - "description": "Returns SchemaA", 13 - "content": { 14 - "application/json": { 15 - "schema": { 16 - "$ref": "cross-file-ref-file1.json#/components/schemas/SchemaA" 17 - } 18 - } 19 - } 20 - } 21 - } 22 - } 23 - }, 24 - "/resource-b": { 25 - "get": { 26 - "responses": { 27 - "200": { 28 - "description": "Returns SchemaB", 29 - "content": { 30 - "application/json": { 31 - "schema": { 32 - "$ref": "cross-file-ref-file2.json#/components/schemas/SchemaB" 33 - } 34 - } 35 - } 36 - } 37 - } 38 - } 39 - } 40 - } 41 - }