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

Configure Feed

Select the types of activity you want to include in your feed.

feat: bring json-schema-ref-parser in-house

Lubos e979b789 778a00d0

+4095 -29
+5
.changeset/slick-queens-buy.md
··· 1 + --- 2 + "@hey-api/json-schema-ref-parser": minor 3 + --- 4 + 5 + **feat**: clean up dependencies
+1
dev/inputs.ts
··· 9 9 opencode: path.resolve(getSpecsPath(), '3.1.x', 'opencode.yaml'), 10 10 petstore: 11 11 '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 13 scalar: 'scalar:@scalar/access-service', 13 14 transformers: path.resolve(getSpecsPath(), '3.1.x', 'transformers.json'), 14 15 validators: path.resolve(getSpecsPath(), '3.1.x', 'validators.yaml'),
+117
packages/json-schema-ref-parser/README.md
··· 1 + # JSON Schema $Ref Parser 2 + 3 + #### Parse, Resolve, and Dereference JSON Schema $ref pointers 4 + 5 + ## Installation 6 + 7 + Install using [npm](https://docs.npmjs.com/about-npm/): 8 + 9 + ```bash 10 + npm install @hey-api/json-schema-ref-parser 11 + yarn add @hey-api/json-schema-ref-parser 12 + bun add @hey-api/json-schema-ref-parser 13 + ``` 14 + 15 + ## The Problem: 16 + 17 + You've got a JSON Schema with `$ref` pointers to other files and/or URLs. Maybe you know all the referenced files ahead 18 + of time. Maybe you don't. Maybe some are local files, and others are remote URLs. Maybe they are a mix of JSON and YAML 19 + format. Maybe some of the files contain cross-references to each other. 20 + 21 + ```json 22 + { 23 + "definitions": { 24 + "person": { 25 + // references an external file 26 + "$ref": "schemas/people/Bruce-Wayne.json" 27 + }, 28 + "place": { 29 + // references a sub-schema in an external file 30 + "$ref": "schemas/places.yaml#/definitions/Gotham-City" 31 + }, 32 + "thing": { 33 + // references a URL 34 + "$ref": "http://wayne-enterprises.com/things/batmobile" 35 + }, 36 + "color": { 37 + // references a value in an external file via an internal reference 38 + "$ref": "#/definitions/thing/properties/colors/black-as-the-night" 39 + } 40 + } 41 + } 42 + ``` 43 + 44 + ## The Solution: 45 + 46 + JSON Schema $Ref Parser is a full [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03) 47 + and [JSON Pointer](https://tools.ietf.org/html/rfc6901) implementation that crawls even the most 48 + complex [JSON Schemas](http://json-schema.org/latest/json-schema-core.html) and gives you simple, straightforward 49 + JavaScript objects. 50 + 51 + - Use **JSON** or **YAML** schemas &mdash; or even a mix of both! 52 + - Supports `$ref` pointers to external files and URLs, as well as custom sources such as databases 53 + - Can bundle multiple files into a single schema that only has _internal_ `$ref` pointers 54 + - Can dereference your schema, producing a plain-old JavaScript object that's easy to work with 55 + - Supports circular references, nested references, 56 + back-references, and cross-references between files 57 + - Maintains object reference equality &mdash; `$ref` pointers to the same value always resolve to the same object 58 + instance 59 + - Compatible with Node LTS and beyond, and all major web browsers on Windows, Mac, and Linux 60 + 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 + 75 + ### New in this fork (@hey-api) 76 + 77 + - **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. 78 + 79 + ```javascript 80 + import { $RefParser } from '@hey-api/json-schema-ref-parser'; 81 + 82 + const parser = new $RefParser(); 83 + const merged = await parser.bundleMany({ 84 + pathOrUrlOrSchemas: [ 85 + './specs/a.yaml', 86 + 'https://example.com/b.yaml', 87 + { openapi: '3.1.0', info: { title: 'Inline' }, paths: {} }, 88 + ], 89 + }); 90 + 91 + // merged.components.* will contain prefixed names like a_<name>, b_<name>, etc. 92 + ``` 93 + 94 + - **Dereference hooks**: Fine-tune dereferencing with `excludedPathMatcher(path) => boolean` to skip subpaths and `onDereference(path, value, parent, parentPropName)` to observe replacements. 95 + 96 + ```javascript 97 + const parser = new $RefParser(); 98 + parser.options.dereference.excludedPathMatcher = (p) => p.includes('/example/'); 99 + parser.options.dereference.onDereference = (p, v) => { 100 + // inspect p / v as needed 101 + }; 102 + await parser.dereference({ pathOrUrlOrSchema: './openapi.yaml' }); 103 + ``` 104 + 105 + - **Smart input resolution**: You can pass a file path, URL, or raw schema object. If a raw schema includes `$id`, it is used as the base URL for resolving relative `$ref`s. 106 + 107 + ```javascript 108 + await new $RefParser().bundle({ 109 + pathOrUrlOrSchema: { 110 + $id: 'https://api.example.com/openapi.json', 111 + openapi: '3.1.0', 112 + paths: { 113 + '/ping': { get: { responses: { 200: { description: 'ok' } } } }, 114 + }, 115 + }, 116 + }); 117 + ```
+64
packages/json-schema-ref-parser/package.json
··· 1 + { 2 + "name": "@hey-api/json-schema-ref-parser", 3 + "version": "1.2.4", 4 + "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers", 5 + "keywords": [ 6 + "$ref", 7 + "dereference", 8 + "json", 9 + "json-pointer", 10 + "json-schema", 11 + "jsonschema", 12 + "resolve", 13 + "schema" 14 + ], 15 + "homepage": "https://heyapi.dev/", 16 + "bugs": { 17 + "url": "https://github.com/hey-api/openapi-ts/issues" 18 + }, 19 + "license": "MIT", 20 + "author": { 21 + "name": "Hey API", 22 + "email": "lubos@heyapi.dev", 23 + "url": "https://heyapi.dev" 24 + }, 25 + "repository": { 26 + "type": "git", 27 + "url": "git+https://github.com/hey-api/openapi-ts.git" 28 + }, 29 + "funding": "https://github.com/sponsors/hey-api", 30 + "files": [ 31 + "src", 32 + "dist", 33 + "cjs" 34 + ], 35 + "type": "module", 36 + "main": "./dist/index.mjs", 37 + "types": "./dist/index.d.mts", 38 + "exports": { 39 + ".": { 40 + "types": "./dist/index.d.mts", 41 + "import": "./dist/index.mjs" 42 + }, 43 + "./package.json": "./package.json" 44 + }, 45 + "scripts": { 46 + "build": "tsdown && pnpm check-exports", 47 + "check-exports": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm", 48 + "dev": "tsdown --watch", 49 + "prepublishOnly": "pnpm build", 50 + "typecheck": "tsc --noEmit" 51 + }, 52 + "dependencies": { 53 + "@jsdevtools/ono": "7.1.3", 54 + "@types/json-schema": "7.0.15", 55 + "js-yaml": "4.1.1" 56 + }, 57 + "devDependencies": { 58 + "@types/js-yaml": "4.0.9", 59 + "typescript": "5.9.3" 60 + }, 61 + "engines": { 62 + "node": ">=20.19.0" 63 + } 64 + }
+59
packages/json-schema-ref-parser/src/__tests__/bundle.test.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { $RefParser } from '..'; 4 + import { getSpecsPath } from './utils'; 5 + 6 + describe('bundle', () => { 7 + it('handles circular reference with description', async () => { 8 + const refParser = new $RefParser(); 9 + const pathOrUrlOrSchema = path.join( 10 + getSpecsPath(), 11 + 'json-schema-ref-parser', 12 + 'circular-ref-with-description.json', 13 + ); 14 + const schema = await refParser.bundle({ pathOrUrlOrSchema }); 15 + expect(schema).toEqual({ 16 + schemas: { 17 + Bar: { 18 + $ref: '#/schemas/Foo', 19 + description: 'ok', 20 + }, 21 + Foo: { 22 + $ref: '#/schemas/Bar', 23 + }, 24 + }, 25 + }); 26 + }); 27 + 28 + it('bundles multiple references to the same file correctly', async () => { 29 + const refParser = new $RefParser(); 30 + const pathOrUrlOrSchema = path.join( 31 + getSpecsPath(), 32 + 'json-schema-ref-parser', 33 + 'multiple-refs.json', 34 + ); 35 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 36 + 37 + // Both parameters should now be $ref to the same internal definition 38 + const firstParam = schema.paths['/test1/{pathId}'].get.parameters[0]; 39 + const secondParam = schema.paths['/test2/{pathId}'].get.parameters[0]; 40 + 41 + // The $ref should match the output structure in file_context_0 42 + expect(firstParam.$ref).toBe('#/components/parameters/path-parameter_pathId'); 43 + expect(secondParam.$ref).toBe('#/components/parameters/path-parameter_pathId'); 44 + 45 + // The referenced parameter should exist and match the expected structure 46 + expect(schema.components).toBeDefined(); 47 + expect(schema.components.parameters).toBeDefined(); 48 + expect(schema.components.parameters['path-parameter_pathId']).toEqual({ 49 + in: 'path', 50 + name: 'pathId', 51 + required: true, 52 + schema: { 53 + description: 'Unique identifier for the path', 54 + format: 'uuid', 55 + type: 'string', 56 + }, 57 + }); 58 + }); 59 + });
+43
packages/json-schema-ref-parser/src/__tests__/index.test.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { getResolvedInput } from '../index'; 4 + 5 + describe('getResolvedInput', () => { 6 + it('handles url', async () => { 7 + const pathOrUrlOrSchema = 'https://foo.com'; 8 + const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema }); 9 + expect(resolvedInput.type).toBe('url'); 10 + expect(resolvedInput.schema).toBeUndefined(); 11 + expect(resolvedInput.path).toBe('https://foo.com/'); 12 + }); 13 + 14 + it('handles file', async () => { 15 + const pathOrUrlOrSchema = './path/to/openapi.json'; 16 + const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema }); 17 + expect(resolvedInput.type).toBe('file'); 18 + expect(resolvedInput.schema).toBeUndefined(); 19 + expect(path.normalize(resolvedInput.path).toLowerCase()).toBe( 20 + path.normalize(path.resolve('./path/to/openapi.json')).toLowerCase(), 21 + ); 22 + }); 23 + 24 + it('handles raw spec', async () => { 25 + const pathOrUrlOrSchema = { 26 + info: { 27 + version: '1.0.0', 28 + }, 29 + openapi: '3.1.0', 30 + paths: {}, 31 + }; 32 + const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema }); 33 + expect(resolvedInput.type).toBe('json'); 34 + expect(resolvedInput.schema).toEqual({ 35 + info: { 36 + version: '1.0.0', 37 + }, 38 + openapi: '3.1.0', 39 + paths: {}, 40 + }); 41 + expect(resolvedInput.path).toBe(''); 42 + }); 43 + });
+34
packages/json-schema-ref-parser/src/__tests__/pointer.test.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { $RefParser } from '..'; 4 + import { getSpecsPath } from './utils'; 5 + 6 + describe('pointer', () => { 7 + it('inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling', async () => { 8 + const refParser = new $RefParser(); 9 + const pathOrUrlOrSchema = path.join( 10 + getSpecsPath(), 11 + 'json-schema-ref-parser', 12 + 'openapi-paths-ref.json', 13 + ); 14 + const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any; 15 + 16 + // The GET endpoint should have its schema defined inline 17 + const getSchema = schema.paths['/foo'].get.responses['200'].content['application/json'].schema; 18 + expect(getSchema.$ref).toBeUndefined(); 19 + expect(getSchema.type).toBe('object'); 20 + expect(getSchema.properties.bar.type).toBe('string'); 21 + 22 + // The POST endpoint should have its schema inlined (copied) instead of a $ref 23 + const postSchema = 24 + schema.paths['/foo'].post.responses['200'].content['application/json'].schema; 25 + expect(postSchema.$ref).toBe( 26 + '#/paths/~1foo/get/responses/200/content/application~1json/schema', 27 + ); 28 + expect(postSchema.type).toBeUndefined(); 29 + expect(postSchema.properties?.bar?.type).toBeUndefined(); 30 + 31 + // Both schemas should be identical objects 32 + expect(postSchema).not.toBe(getSchema); 33 + }); 34 + });
+3
packages/json-schema-ref-parser/src/__tests__/utils.ts
··· 1 + import path from 'node:path'; 2 + 3 + export const getSpecsPath = (): string => path.join(__dirname, '..', '..', '..', '..', 'specs');
+743
packages/json-schema-ref-parser/src/bundle.ts
··· 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 * as url from './util/url'; 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 + 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 + 23 + export interface InventoryEntry { 24 + $ref: any; 25 + circular: any; 26 + depth: any; 27 + extended: any; 28 + external: any; 29 + file: any; 30 + hash: any; 31 + indirections: any; 32 + key: any; 33 + originalContainerType?: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers'; 34 + parent: any; 35 + pathFromRoot: any; 36 + value: any; 37 + } 38 + 39 + /** 40 + * Fast lookup using Map instead of linear search with deep equality 41 + */ 42 + const createInventoryLookup = () => { 43 + const lookup = new Map<string, InventoryEntry>(); 44 + const objectIds = new WeakMap<object, string>(); // Use WeakMap to avoid polluting objects 45 + let idCounter = 0; 46 + let lookupCount = 0; 47 + let addCount = 0; 48 + 49 + const getObjectId = (obj: any) => { 50 + if (!objectIds.has(obj)) { 51 + objectIds.set(obj, `obj_${++idCounter}`); 52 + } 53 + return objectIds.get(obj)!; 54 + }; 55 + 56 + const createInventoryKey = ($refParent: any, $refKey: any) => 57 + // Use WeakMap-based lookup to avoid polluting the actual schema objects 58 + `${getObjectId($refParent)}_${$refKey}`; 59 + 60 + return { 61 + add: (entry: InventoryEntry) => { 62 + addCount++; 63 + const key = createInventoryKey(entry.parent, entry.key); 64 + lookup.set(key, entry); 65 + if (addCount % 100 === 0) { 66 + perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`); 67 + } 68 + }, 69 + find: ($refParent: any, $refKey: any) => { 70 + lookupCount++; 71 + const key = createInventoryKey($refParent, $refKey); 72 + const result = lookup.get(key); 73 + if (lookupCount % 100 === 0) { 74 + perf.log(`Inventory lookup: ${lookupCount} lookups performed`); 75 + } 76 + return result; 77 + }, 78 + getStats: () => ({ addCount, lookupCount, mapSize: lookup.size }), 79 + remove: (entry: InventoryEntry) => { 80 + const key = createInventoryKey(entry.parent, entry.key); 81 + lookup.delete(key); 82 + }, 83 + }; 84 + }; 85 + 86 + /** 87 + * Determine the container type from a JSON Pointer path. 88 + * Analyzes the path tokens to identify the appropriate OpenAPI component container. 89 + * 90 + * @param path - The JSON Pointer path to analyze 91 + * @returns The container type: "schemas", "parameters", "requestBodies", "responses", or "headers" 92 + */ 93 + const getContainerTypeFromPath = ( 94 + path: string, 95 + ): 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers' => { 96 + const tokens = Pointer.parse(path); 97 + const has = (t: string) => tokens.includes(t); 98 + // Prefer more specific containers first 99 + if (has('parameters')) { 100 + return 'parameters'; 101 + } 102 + if (has('requestBody')) { 103 + return 'requestBodies'; 104 + } 105 + if (has('headers')) { 106 + return 'headers'; 107 + } 108 + if (has('responses')) { 109 + return 'responses'; 110 + } 111 + if (has('schema')) { 112 + return 'schemas'; 113 + } 114 + // default: treat as schema-like 115 + return 'schemas'; 116 + }; 117 + 118 + /** 119 + * Inventories the given JSON Reference (i.e. records detailed information about it so we can 120 + * optimize all $refs in the schema), and then crawls the resolved value. 121 + */ 122 + const inventory$Ref = <S extends object = JSONSchema>({ 123 + $refKey, 124 + $refParent, 125 + $refs, 126 + indirections, 127 + inventory, 128 + inventoryLookup, 129 + options, 130 + path, 131 + pathFromRoot, 132 + resolvedRefs = new Map(), 133 + visitedObjects = new WeakSet(), 134 + }: { 135 + /** 136 + * The key in `$refParent` that is a JSON Reference 137 + */ 138 + $refKey: string | null; 139 + /** 140 + * The object that contains a JSON Reference as one of its keys 141 + */ 142 + $refParent: any; 143 + $refs: $Refs<S>; 144 + /** 145 + * unknown 146 + */ 147 + indirections: number; 148 + /** 149 + * An array of already-inventoried $ref pointers 150 + */ 151 + inventory: Array<InventoryEntry>; 152 + /** 153 + * Fast lookup for inventory entries 154 + */ 155 + inventoryLookup: ReturnType<typeof createInventoryLookup>; 156 + options: ParserOptions; 157 + /** 158 + * The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash 159 + */ 160 + path: string; 161 + /** 162 + * The path of the JSON Reference at `$refKey`, from the schema root 163 + */ 164 + pathFromRoot: string; 165 + /** 166 + * Cache for resolved $ref targets to avoid redundant resolution 167 + */ 168 + resolvedRefs?: Map<string, any>; 169 + /** 170 + * Set of already visited objects to avoid infinite loops and redundant processing 171 + */ 172 + visitedObjects?: WeakSet<object>; 173 + }) => { 174 + perf.mark('inventory-ref-start'); 175 + const $ref = $refKey === null ? $refParent : $refParent[$refKey]; 176 + const $refPath = url.resolve(path, $ref.$ref); 177 + 178 + // Check cache first to avoid redundant resolution 179 + let pointer = resolvedRefs.get($refPath); 180 + 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'); 185 + 186 + if (pointer) { 187 + resolvedRefs.set($refPath, pointer); 188 + perf.log(`Cached resolved $ref: ${$refPath}`); 189 + } 190 + } 191 + 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 + } 197 + 198 + const parsed = Pointer.parse(pathFromRoot); 199 + const depth = parsed.length; 200 + const file = url.stripHash(pointer.path); 201 + const hash = url.getHash(pointer.path); 202 + const external = file !== $refs._root$Ref.path; 203 + const extended = $Ref.isExtended$Ref($ref); 204 + indirections += pointer.indirections; 205 + 206 + // Check if this exact location (parent + key + pathFromRoot) has already been inventoried 207 + perf.mark('lookup-start'); 208 + const existingEntry = inventoryLookup.find($refParent, $refKey); 209 + perf.mark('lookup-end'); 210 + perf.measure('lookup-time', 'lookup-start', 'lookup-end'); 211 + 212 + if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) { 213 + // This exact location has already been inventoried, so we don't need to process it again 214 + if (depth < existingEntry.depth || indirections < existingEntry.indirections) { 215 + removeFromInventory(inventory, existingEntry); 216 + inventoryLookup.remove(existingEntry); 217 + } else { 218 + perf.mark('inventory-ref-end'); 219 + perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end'); 220 + return; 221 + } 222 + } 223 + 224 + const newEntry: InventoryEntry = { 225 + $ref, // The JSON Reference (e.g. {$ref: string}) 226 + circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself) 227 + depth, // How far from the JSON Schema root is this $ref pointer? 228 + extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref") 229 + external, // Does this $ref pointer point to a file other than the main JSON Schema file? 230 + file, // The file that the $ref pointer resolves to 231 + hash, // The hash within `file` that the $ref pointer resolves to 232 + indirections, // The number of indirect references that were traversed to resolve the value 233 + key: $refKey, 234 + // The resolved value of the $ref pointer 235 + originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined, 236 + 237 + // The key in `parent` that is the $ref pointer 238 + parent: $refParent, 239 + 240 + // The object that contains this $ref pointer 241 + pathFromRoot, 242 + // The path to the $ref pointer, from the JSON Schema root 243 + value: pointer.value, // The original container type in the external file 244 + }; 245 + 246 + inventory.push(newEntry); 247 + inventoryLookup.add(newEntry); 248 + 249 + perf.log( 250 + `Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`, 251 + ); 252 + 253 + // Recursively crawl the resolved value 254 + if (!existingEntry || external) { 255 + perf.mark('crawl-recursive-start'); 256 + crawl({ 257 + $refs, 258 + indirections: indirections + 1, 259 + inventory, 260 + inventoryLookup, 261 + key: null, 262 + options, 263 + parent: pointer.value, 264 + path: pointer.path, 265 + pathFromRoot, 266 + resolvedRefs, 267 + visitedObjects, 268 + }); 269 + perf.mark('crawl-recursive-end'); 270 + perf.measure('crawl-recursive-time', 'crawl-recursive-start', 'crawl-recursive-end'); 271 + } 272 + 273 + perf.mark('inventory-ref-end'); 274 + perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end'); 275 + }; 276 + 277 + /** 278 + * Recursively crawls the given value, and inventories all JSON references. 279 + */ 280 + const crawl = <S extends object = JSONSchema>({ 281 + $refs, 282 + indirections, 283 + inventory, 284 + inventoryLookup, 285 + key, 286 + options, 287 + parent, 288 + path, 289 + pathFromRoot, 290 + resolvedRefs = new Map(), 291 + visitedObjects = new WeakSet(), 292 + }: { 293 + $refs: $Refs<S>; 294 + indirections: number; 295 + /** 296 + * An array of already-inventoried $ref pointers 297 + */ 298 + inventory: Array<InventoryEntry>; 299 + /** 300 + * Fast lookup for inventory entries 301 + */ 302 + inventoryLookup: ReturnType<typeof createInventoryLookup>; 303 + /** 304 + * The property key of `parent` to be crawled 305 + */ 306 + key: string | null; 307 + options: ParserOptions; 308 + /** 309 + * The object containing the value to crawl. If the value is not an object or array, it will be ignored. 310 + */ 311 + parent: object | $RefParser; 312 + /** 313 + * The full path of the property being crawled, possibly with a JSON Pointer in the hash 314 + */ 315 + path: string; 316 + /** 317 + * The path of the property being crawled, from the schema root 318 + */ 319 + pathFromRoot: string; 320 + /** 321 + * Cache for resolved $ref targets to avoid redundant resolution 322 + */ 323 + resolvedRefs?: Map<string, any>; 324 + /** 325 + * Set of already visited objects to avoid infinite loops and redundant processing 326 + */ 327 + visitedObjects?: WeakSet<object>; 328 + }) => { 329 + const obj = key === null ? parent : parent[key as keyof typeof parent]; 330 + 331 + if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) { 332 + // 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 + } 337 + 338 + if ($Ref.isAllowed$Ref(obj)) { 339 + perf.log(`Found $ref at ${pathFromRoot}: ${(obj as any).$ref}`); 340 + inventory$Ref({ 341 + $refKey: key, 342 + $refParent: parent, 343 + $refs, 344 + indirections, 345 + inventory, 346 + inventoryLookup, 347 + options, 348 + path, 349 + pathFromRoot, 350 + resolvedRefs, 351 + visitedObjects, 352 + }); 353 + } else { 354 + // Mark this object as visited BEFORE processing its children 355 + visitedObjects.add(obj); 356 + 357 + // Crawl the object in a specific order that's optimized for bundling. 358 + // This is important because it determines how `pathFromRoot` gets built, 359 + // which later determines which keys get dereferenced and which ones get remapped 360 + const keys = Object.keys(obj).sort((a, b) => { 361 + // Most people will expect references to be bundled into the "definitions" property, 362 + // so we always crawl that property first, if it exists. 363 + if (a === 'definitions') { 364 + return -1; 365 + } else if (b === 'definitions') { 366 + return 1; 367 + } else { 368 + // Otherwise, crawl the keys based on their length. 369 + // This produces the shortest possible bundled references 370 + return a.length - b.length; 371 + } 372 + }) as (keyof typeof obj)[]; 373 + 374 + for (const key of keys) { 375 + const keyPath = Pointer.join(path, key); 376 + const keyPathFromRoot = Pointer.join(pathFromRoot, key); 377 + const value = obj[key]; 378 + 379 + if ($Ref.isAllowed$Ref(value)) { 380 + inventory$Ref({ 381 + $refKey: key, 382 + $refParent: obj, 383 + $refs, 384 + indirections, 385 + inventory, 386 + inventoryLookup, 387 + options, 388 + path, 389 + pathFromRoot: keyPathFromRoot, 390 + resolvedRefs, 391 + visitedObjects, 392 + }); 393 + } else { 394 + crawl({ 395 + $refs, 396 + indirections, 397 + inventory, 398 + inventoryLookup, 399 + key, 400 + options, 401 + parent: obj, 402 + path: keyPath, 403 + pathFromRoot: keyPathFromRoot, 404 + resolvedRefs, 405 + visitedObjects, 406 + }); 407 + } 408 + } 409 + } 410 + } 411 + }; 412 + 413 + /** 414 + * Remap external refs by hoisting resolved values into a shared container in the root schema 415 + * and pointing all occurrences to those internal definitions. Internal refs remain internal. 416 + */ 417 + function remap(parser: $RefParser, inventory: InventoryEntry[]) { 418 + perf.log(`Starting remap with ${inventory.length} inventory entries`); 419 + perf.mark('remap-start'); 420 + const root = parser.schema as any; 421 + 422 + // 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'); 424 + inventory.sort((a: InventoryEntry, b: InventoryEntry) => { 425 + if (a.file !== b.file) { 426 + // Group all the $refs that point to the same file 427 + return a.file < b.file ? -1 : +1; 428 + } else if (a.hash !== b.hash) { 429 + // Group all the $refs that point to the same part of the file 430 + return a.hash < b.hash ? -1 : +1; 431 + } else if (a.circular !== b.circular) { 432 + // If the $ref points to itself, then sort it higher than other $refs that point to this $ref 433 + return a.circular ? -1 : +1; 434 + } else if (a.extended !== b.extended) { 435 + // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value 436 + return a.extended ? +1 : -1; 437 + } else if (a.indirections !== b.indirections) { 438 + // Sort direct references higher than indirect references 439 + return a.indirections - b.indirections; 440 + } else if (a.depth !== b.depth) { 441 + // Sort $refs by how close they are to the JSON Schema root 442 + return a.depth - b.depth; 443 + } else { 444 + // Determine how far each $ref is from the "definitions" property. 445 + // Most people will expect references to be bundled into the the "definitions" property if possible. 446 + const aDefinitionsIndex = a.pathFromRoot.lastIndexOf('/definitions'); 447 + const bDefinitionsIndex = b.pathFromRoot.lastIndexOf('/definitions'); 448 + if (aDefinitionsIndex !== bDefinitionsIndex) { 449 + // Give higher priority to the $ref that's closer to the "definitions" property 450 + return bDefinitionsIndex - aDefinitionsIndex; 451 + } else { 452 + // All else is equal, so use the shorter path, which will produce the shortest possible reference 453 + return a.pathFromRoot.length - b.pathFromRoot.length; 454 + } 455 + } 456 + }); 457 + 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 + 463 + // Ensure or return a container by component type. Prefer OpenAPI-aware placement; 464 + // otherwise use existing root containers; otherwise create components/*. 465 + const ensureContainer = ( 466 + type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers', 467 + ) => { 468 + const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string'); 469 + const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string'); 470 + 471 + if (isOas3) { 472 + if (!root.components || typeof root.components !== 'object') { 473 + root.components = {}; 474 + } 475 + if (!root.components[type] || typeof root.components[type] !== 'object') { 476 + root.components[type] = {}; 477 + } 478 + return { obj: root.components[type], prefix: `#/components/${type}` } as const; 479 + } 480 + 481 + if (isOas2) { 482 + if (type === 'schemas') { 483 + if (!root.definitions || typeof root.definitions !== 'object') { 484 + root.definitions = {}; 485 + } 486 + return { obj: root.definitions, prefix: '#/definitions' } as const; 487 + } 488 + if (type === 'parameters') { 489 + if (!root.parameters || typeof root.parameters !== 'object') { 490 + root.parameters = {}; 491 + } 492 + return { obj: root.parameters, prefix: '#/parameters' } as const; 493 + } 494 + if (type === 'responses') { 495 + if (!root.responses || typeof root.responses !== 'object') { 496 + root.responses = {}; 497 + } 498 + return { obj: root.responses, prefix: '#/responses' } as const; 499 + } 500 + // requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions 501 + if (!root.definitions || typeof root.definitions !== 'object') { 502 + root.definitions = {}; 503 + } 504 + return { obj: root.definitions, prefix: '#/definitions' } as const; 505 + } 506 + 507 + // No explicit version: prefer existing containers 508 + if (root && typeof root === 'object') { 509 + if (root.components && typeof root.components === 'object') { 510 + if (!root.components[type] || typeof root.components[type] !== 'object') { 511 + root.components[type] = {}; 512 + } 513 + return { obj: root.components[type], prefix: `#/components/${type}` } as const; 514 + } 515 + if (root.definitions && typeof root.definitions === 'object') { 516 + return { obj: root.definitions, prefix: '#/definitions' } as const; 517 + } 518 + // Create components/* by default if nothing exists 519 + if (!root.components || typeof root.components !== 'object') { 520 + root.components = {}; 521 + } 522 + if (!root.components[type] || typeof root.components[type] !== 'object') { 523 + root.components[type] = {}; 524 + } 525 + return { obj: root.components[type], prefix: `#/components/${type}` } as const; 526 + } 527 + 528 + // Fallback 529 + root.definitions = root.definitions || {}; 530 + return { obj: root.definitions, prefix: '#/definitions' } as const; 531 + }; 532 + 533 + /** 534 + * Choose the appropriate component container for bundling. 535 + * Prioritizes the original container type from external files over usage location. 536 + * 537 + * @param entry - The inventory entry containing reference information 538 + * @returns The container type to use for bundling 539 + */ 540 + const chooseComponent = (entry: InventoryEntry) => { 541 + // If we have the original container type from the external file, use it 542 + if (entry.originalContainerType) { 543 + return entry.originalContainerType; 544 + } 545 + 546 + // Fallback to usage path for internal references or when original type is not available 547 + return getContainerTypeFromPath(entry.pathFromRoot); 548 + }; 549 + 550 + // Track names per (container prefix) and per target 551 + const targetToNameByPrefix = new Map<string, Map<string, string>>(); 552 + const usedNamesByObj = new Map<any, Set<string>>(); 553 + 554 + const sanitize = (name: string) => name.replace(/[^A-Za-z0-9_-]/g, '_'); 555 + const baseName = (filePath: string) => { 556 + try { 557 + const withoutHash = filePath.split('#')[0]!; 558 + const parts = withoutHash.split('/'); 559 + const filename = parts[parts.length - 1] || 'schema'; 560 + const dot = filename.lastIndexOf('.'); 561 + return sanitize(dot > 0 ? filename.substring(0, dot) : filename); 562 + } catch { 563 + return 'schema'; 564 + } 565 + }; 566 + const lastToken = (hash: string) => { 567 + if (!hash || hash === '#') { 568 + return 'root'; 569 + } 570 + const tokens = hash.replace(/^#\//, '').split('/'); 571 + return sanitize(tokens[tokens.length - 1] || 'root'); 572 + }; 573 + const uniqueName = (containerObj: any, proposed: string) => { 574 + if (!usedNamesByObj.has(containerObj)) { 575 + usedNamesByObj.set(containerObj, new Set<string>(Object.keys(containerObj || {}))); 576 + } 577 + const used = usedNamesByObj.get(containerObj)!; 578 + let name = proposed; 579 + let i = 2; 580 + while (used.has(name)) { 581 + name = `${proposed}_${i++}`; 582 + } 583 + used.add(name); 584 + return name; 585 + }; 586 + perf.mark('remap-loop-start'); 587 + for (const entry of inventory) { 588 + // Safety check: ensure entry and entry.$ref are valid objects 589 + if (!entry || !entry.$ref || typeof entry.$ref !== 'object') { 590 + perf.warn(`Skipping invalid inventory entry:`, entry); 591 + continue; 592 + } 593 + 594 + // Keep internal refs internal. However, if the $ref extends the resolved value 595 + // (i.e. it has additional properties in addition to "$ref"), then we must 596 + // preserve the original $ref rather than rewriting it to the resolved hash. 597 + if (!entry.external) { 598 + if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') { 599 + entry.$ref.$ref = entry.hash; 600 + } 601 + continue; 602 + } 603 + 604 + // Avoid changing direct self-references; keep them internal 605 + if (entry.circular) { 606 + if (entry.$ref && typeof entry.$ref === 'object') { 607 + entry.$ref.$ref = entry.pathFromRoot; 608 + } 609 + continue; 610 + } 611 + 612 + // Choose appropriate container based on original location in external file 613 + const component = chooseComponent(entry); 614 + const { obj: container, prefix } = ensureContainer(component); 615 + 616 + const targetKey = `${entry.file}::${entry.hash}`; 617 + if (!targetToNameByPrefix.has(prefix)) { 618 + targetToNameByPrefix.set(prefix, new Map<string, string>()); 619 + } 620 + const namesForPrefix = targetToNameByPrefix.get(prefix)!; 621 + 622 + let defName = namesForPrefix.get(targetKey); 623 + if (!defName) { 624 + // If the external file is one of the original input sources, prefer its assigned prefix 625 + let proposedBase = baseName(entry.file); 626 + try { 627 + const parserAny: any = parser as any; 628 + if ( 629 + parserAny && 630 + parserAny.sourcePathToPrefix && 631 + typeof parserAny.sourcePathToPrefix.get === 'function' 632 + ) { 633 + const withoutHash = (entry.file || '').split('#')[0]; 634 + const mapped = parserAny.sourcePathToPrefix.get(withoutHash); 635 + if (mapped && typeof mapped === 'string') { 636 + proposedBase = mapped; 637 + } 638 + } 639 + } catch { 640 + // Ignore errors 641 + } 642 + const proposed = `${proposedBase}_${lastToken(entry.hash)}`; 643 + defName = uniqueName(container, proposed); 644 + namesForPrefix.set(targetKey, defName); 645 + // Store the resolved value under the container 646 + container[defName] = entry.value; 647 + } 648 + 649 + // Point the occurrence to the internal definition, preserving extensions 650 + const refPath = `${prefix}/${defName}`; 651 + if (entry.extended && entry.$ref && typeof entry.$ref === 'object') { 652 + entry.$ref.$ref = refPath; 653 + } else { 654 + entry.parent[entry.key] = { $ref: refPath }; 655 + } 656 + } 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`); 664 + } 665 + 666 + function removeFromInventory(inventory: InventoryEntry[], entry: any) { 667 + const index = inventory.indexOf(entry); 668 + inventory.splice(index, 1); 669 + } 670 + 671 + /** 672 + * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that 673 + * only has *internal* references, not any *external* references. 674 + * This method mutates the JSON schema object, adding new references and re-mapping existing ones. 675 + * 676 + * @param parser 677 + * @param options 678 + */ 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[] = []; 685 + const inventoryLookup = createInventoryLookup(); 686 + 687 + perf.log('Starting crawl phase'); 688 + perf.mark('crawl-phase-start'); 689 + 690 + const visitedObjects = new WeakSet<object>(); 691 + const resolvedRefs = new Map<string, any>(); // Cache for resolved $ref targets 692 + 693 + crawl<JSONSchema>({ 694 + $refs: parser.$refs, 695 + indirections: 0, 696 + inventory, 697 + inventoryLookup, 698 + key: 'schema', 699 + options, 700 + parent: parser, 701 + path: parser.$refs._root$Ref.path + '#', 702 + pathFromRoot: '#', 703 + resolvedRefs, 704 + visitedObjects, 705 + }); 706 + 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'); 716 + 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 + };
+290
packages/json-schema-ref-parser/src/dereference.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + 3 + import type { $RefParser } from '.'; 4 + import type { DereferenceOptions, ParserOptions } from './options'; 5 + import Pointer from './pointer'; 6 + import $Ref from './ref'; 7 + import type $Refs from './refs'; 8 + import type { JSONSchema } from './types'; 9 + import { TimeoutError } from './util/errors'; 10 + import * as url from './util/url'; 11 + 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 + 39 + /** 40 + * Recursively crawls the given value, and dereferences any JSON references. 41 + * 42 + * @param obj - The value to crawl. If it's not an object or array, it will be ignored. 43 + * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash 44 + * @param pathFromRoot - The path of `obj` from the schema root 45 + * @param parents - An array of the parent objects that have already been dereferenced 46 + * @param processedObjects - An array of all the objects that have already been processed 47 + * @param dereferencedCache - An map of all the dereferenced objects 48 + * @param $refs 49 + * @param options 50 + * @param startTime - The time when the dereferencing started 51 + * @returns 52 + */ 53 + function crawl<S extends object = JSONSchema>( 54 + obj: any, 55 + path: string, 56 + pathFromRoot: string, 57 + parents: Set<any>, 58 + processedObjects: Set<any>, 59 + dereferencedCache: any, 60 + $refs: $Refs<S>, 61 + options: ParserOptions, 62 + startTime: number, 63 + ) { 64 + let dereferenced; 65 + const result = { 66 + circular: false, 67 + value: obj, 68 + }; 69 + 70 + if (options && options.timeoutMs) { 71 + if (Date.now() - startTime > options.timeoutMs) { 72 + throw new TimeoutError(options.timeoutMs); 73 + } 74 + } 75 + const derefOptions = (options.dereference || {}) as DereferenceOptions; 76 + const isExcludedPath = derefOptions.excludedPathMatcher || (() => false); 77 + 78 + if (derefOptions?.circular === 'ignore' || !processedObjects.has(obj)) { 79 + if ( 80 + obj && 81 + typeof obj === 'object' && 82 + !ArrayBuffer.isView(obj) && 83 + !isExcludedPath(pathFromRoot) 84 + ) { 85 + parents.add(obj); 86 + processedObjects.add(obj); 87 + 88 + if ($Ref.isAllowed$Ref(obj)) { 89 + dereferenced = dereference$Ref( 90 + obj, 91 + path, 92 + pathFromRoot, 93 + parents, 94 + processedObjects, 95 + dereferencedCache, 96 + $refs, 97 + options, 98 + startTime, 99 + ); 100 + result.circular = dereferenced.circular; 101 + result.value = dereferenced.value; 102 + } else { 103 + for (const key of Object.keys(obj)) { 104 + const keyPath = Pointer.join(path, key); 105 + const keyPathFromRoot = Pointer.join(pathFromRoot, key); 106 + 107 + if (isExcludedPath(keyPathFromRoot)) { 108 + continue; 109 + } 110 + 111 + const value = obj[key]; 112 + let circular = false; 113 + 114 + if ($Ref.isAllowed$Ref(value)) { 115 + dereferenced = dereference$Ref( 116 + value, 117 + keyPath, 118 + keyPathFromRoot, 119 + parents, 120 + processedObjects, 121 + dereferencedCache, 122 + $refs, 123 + options, 124 + startTime, 125 + ); 126 + circular = dereferenced.circular; 127 + // Avoid pointless mutations; breaks frozen objects to no profit 128 + if (obj[key] !== dereferenced.value) { 129 + obj[key] = dereferenced.value; 130 + derefOptions?.onDereference?.(value.$ref, obj[key], obj, key); 131 + } 132 + } else { 133 + if (!parents.has(value)) { 134 + dereferenced = crawl( 135 + value, 136 + keyPath, 137 + keyPathFromRoot, 138 + parents, 139 + processedObjects, 140 + dereferencedCache, 141 + $refs, 142 + options, 143 + startTime, 144 + ); 145 + circular = dereferenced.circular; 146 + // Avoid pointless mutations; breaks frozen objects to no profit 147 + if (obj[key] !== dereferenced.value) { 148 + obj[key] = dereferenced.value; 149 + } 150 + } else { 151 + circular = foundCircularReference(keyPath, $refs, options); 152 + } 153 + } 154 + 155 + // Set the "isCircular" flag if this or any other property is circular 156 + result.circular = result.circular || circular; 157 + } 158 + } 159 + 160 + parents.delete(obj); 161 + } 162 + } 163 + 164 + return result; 165 + } 166 + 167 + /** 168 + * Dereferences the given JSON Reference, and then crawls the resulting value. 169 + * 170 + * @param $ref - The JSON Reference to resolve 171 + * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash 172 + * @param pathFromRoot - The path of `$ref` from the schema root 173 + * @param parents - An array of the parent objects that have already been dereferenced 174 + * @param processedObjects - An array of all the objects that have already been dereferenced 175 + * @param dereferencedCache - An map of all the dereferenced objects 176 + * @param $refs 177 + * @param options 178 + * @returns 179 + */ 180 + function dereference$Ref<S extends object = JSONSchema>( 181 + $ref: any, 182 + path: string, 183 + pathFromRoot: string, 184 + parents: Set<any>, 185 + processedObjects: any, 186 + dereferencedCache: any, 187 + $refs: $Refs<S>, 188 + options: ParserOptions, 189 + startTime: number, 190 + ) { 191 + const $refPath = url.resolve(path, $ref.$ref); 192 + 193 + const cache = dereferencedCache.get($refPath); 194 + if (cache && !cache.circular) { 195 + const refKeys = Object.keys($ref); 196 + if (refKeys.length > 1) { 197 + const extraKeys = {}; 198 + for (const key of refKeys) { 199 + if (key !== '$ref' && !(key in cache.value)) { 200 + // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message 201 + extraKeys[key] = $ref[key]; 202 + } 203 + } 204 + return { 205 + circular: cache.circular, 206 + value: Object.assign({}, structuredClone(cache.value), extraKeys), 207 + }; 208 + } 209 + 210 + // Return a deep-cloned value so each occurrence is an independent copy 211 + return { circular: cache.circular, value: structuredClone(cache.value) }; 212 + } 213 + 214 + const pointer = $refs._resolve($refPath, path, options); 215 + 216 + if (pointer === null) { 217 + return { 218 + circular: false, 219 + value: null, 220 + }; 221 + } 222 + 223 + // Check for circular references 224 + const directCircular = pointer.circular; 225 + let circular = directCircular || parents.has(pointer.value); 226 + if (circular) { 227 + foundCircularReference(path, $refs, options); 228 + } 229 + 230 + // Dereference the JSON reference 231 + let dereferencedValue = $Ref.dereference($ref, pointer.value); 232 + 233 + // Crawl the dereferenced value (unless it's circular) 234 + if (!circular) { 235 + // Determine if the dereferenced value is circular 236 + const dereferenced = crawl( 237 + dereferencedValue, 238 + pointer.path, 239 + pathFromRoot, 240 + parents, 241 + processedObjects, 242 + dereferencedCache, 243 + $refs, 244 + options, 245 + startTime, 246 + ); 247 + circular = dereferenced.circular; 248 + dereferencedValue = dereferenced.value; 249 + } 250 + 251 + if (circular && !directCircular && options.dereference?.circular === 'ignore') { 252 + // The user has chosen to "ignore" circular references, so don't change the value 253 + dereferencedValue = $ref; 254 + } 255 + 256 + if (directCircular) { 257 + // The pointer is a DIRECT circular reference (i.e. it references itself). 258 + // So replace the $ref path with the absolute path from the JSON Schema root 259 + dereferencedValue.$ref = pathFromRoot; 260 + } 261 + 262 + const dereferencedObject = { 263 + circular, 264 + value: dereferencedValue, 265 + }; 266 + 267 + // only cache if no extra properties than $ref 268 + if (Object.keys($ref).length === 1) { 269 + dereferencedCache.set($refPath, dereferencedObject); 270 + } 271 + 272 + return dereferencedObject; 273 + } 274 + 275 + /** 276 + * Called when a circular reference is found. 277 + * It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false. 278 + * 279 + * @param keyPath - The JSON Reference path of the circular reference 280 + * @param $refs 281 + * @param options 282 + * @returns - always returns true, to indicate that a circular reference was found 283 + */ 284 + function foundCircularReference(keyPath: any, $refs: any, options: any) { 285 + $refs.circular = true; 286 + if (!options.dereference.circular) { 287 + throw ono.reference(`Circular $ref pointer found at ${keyPath}`); 288 + } 289 + return true; 290 + }
+599
packages/json-schema-ref-parser/src/index.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + 3 + import { bundle as _bundle } from './bundle'; 4 + import _dereference from './dereference'; 5 + import { getJsonSchemaRefParserDefaultOptions } from './options'; 6 + import { newFile, parseFile } from './parse'; 7 + import $Refs from './refs'; 8 + import { resolveExternal } from './resolve-external'; 9 + import { fileResolver } from './resolvers/file'; 10 + import { urlResolver } from './resolvers/url'; 11 + import type { JSONSchema } from './types'; 12 + import { isHandledError, JSONParserErrorGroup } from './util/errors'; 13 + import * as url from './util/url'; 14 + 15 + interface ResolvedInput { 16 + path: string; 17 + schema: string | JSONSchema | Buffer | Awaited<JSONSchema> | undefined; 18 + type: 'file' | 'json' | 'url'; 19 + } 20 + 21 + export const getResolvedInput = ({ 22 + pathOrUrlOrSchema, 23 + }: { 24 + pathOrUrlOrSchema: JSONSchema | string | unknown; 25 + }): ResolvedInput => { 26 + if (!pathOrUrlOrSchema) { 27 + throw ono(`Expected a file path, URL, or object. Got ${pathOrUrlOrSchema}`); 28 + } 29 + 30 + const resolvedInput: ResolvedInput = { 31 + path: typeof pathOrUrlOrSchema === 'string' ? pathOrUrlOrSchema : '', 32 + schema: undefined, 33 + type: 'url', 34 + }; 35 + 36 + // If the path is a filesystem path, then convert it to a URL. 37 + // NOTE: According to the JSON Reference spec, these should already be URLs, 38 + // but, in practice, many people use local filesystem paths instead. 39 + // So we're being generous here and doing the conversion automatically. 40 + // This is not intended to be a 100% bulletproof solution. 41 + // If it doesn't work for your use-case, then use a URL instead. 42 + if (resolvedInput.path && url.isFileSystemPath(resolvedInput.path)) { 43 + resolvedInput.path = url.fromFileSystemPath(resolvedInput.path); 44 + resolvedInput.type = 'file'; 45 + } else if (!resolvedInput.path && pathOrUrlOrSchema && typeof pathOrUrlOrSchema === 'object') { 46 + if ('$id' in pathOrUrlOrSchema && pathOrUrlOrSchema.$id) { 47 + // when schema id has defined an URL should use that hostname to request the references, 48 + // instead of using the current page URL 49 + const { hostname, protocol } = new URL(pathOrUrlOrSchema.$id as string); 50 + resolvedInput.path = `${protocol}//${hostname}:${protocol === 'https:' ? 443 : 80}`; 51 + resolvedInput.type = 'url'; 52 + } else { 53 + resolvedInput.schema = pathOrUrlOrSchema; 54 + resolvedInput.type = 'json'; 55 + } 56 + } 57 + 58 + if (resolvedInput.type !== 'json') { 59 + // resolve the absolute path of the schema 60 + resolvedInput.path = url.resolve(url.cwd(), resolvedInput.path); 61 + } 62 + 63 + return resolvedInput; 64 + }; 65 + 66 + // NOTE: previously used helper removed as unused 67 + 68 + /** 69 + * This class parses a JSON schema, builds a map of its JSON references and their resolved values, 70 + * and provides methods for traversing, manipulating, and dereferencing those references. 71 + */ 72 + export class $RefParser { 73 + /** 74 + * The resolved JSON references 75 + * 76 + * @type {$Refs} 77 + * @readonly 78 + */ 79 + $refs = new $Refs<JSONSchema>(); 80 + public options = getJsonSchemaRefParserDefaultOptions(); 81 + /** 82 + * The parsed (and possibly dereferenced) JSON schema object 83 + * 84 + * @type {object} 85 + * @readonly 86 + */ 87 + public schema: JSONSchema | null = null; 88 + public schemaMany: JSONSchema[] = []; 89 + public schemaManySources: string[] = []; 90 + public sourcePathToPrefix: Map<string, string> = new Map(); 91 + 92 + /** 93 + * Bundles all referenced files/URLs into a single schema that only has internal `$ref` pointers. This lets you split-up your schema however you want while you're building it, but easily combine all those files together when it's time to package or distribute the schema to other people. The resulting schema size will be small, since it will still contain internal JSON references rather than being fully-dereferenced. 94 + * 95 + * This also eliminates the risk of circular references, so the schema can be safely serialized using `JSON.stringify()`. 96 + * 97 + * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html#bundleschema-options-callback 98 + * 99 + * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file. 100 + */ 101 + public async bundle({ 102 + arrayBuffer, 103 + fetch, 104 + pathOrUrlOrSchema, 105 + resolvedInput, 106 + }: { 107 + arrayBuffer?: ArrayBuffer; 108 + fetch?: RequestInit; 109 + pathOrUrlOrSchema: JSONSchema | string | unknown; 110 + resolvedInput?: ResolvedInput; 111 + }): Promise<JSONSchema> { 112 + await this.parse({ 113 + arrayBuffer, 114 + fetch, 115 + pathOrUrlOrSchema, 116 + resolvedInput, 117 + }); 118 + 119 + await resolveExternal(this, this.options); 120 + const errors = JSONParserErrorGroup.getParserErrors(this); 121 + if (errors.length > 0) { 122 + throw new JSONParserErrorGroup(this); 123 + } 124 + _bundle(this, this.options); 125 + const errors2 = JSONParserErrorGroup.getParserErrors(this); 126 + if (errors2.length > 0) { 127 + throw new JSONParserErrorGroup(this); 128 + } 129 + return this.schema!; 130 + } 131 + 132 + /** 133 + * Bundles multiple roots (files/URLs/objects) into a single schema by creating a synthetic root 134 + * that references each input, resolving all externals, and then hoisting via the existing bundler. 135 + */ 136 + public async bundleMany({ 137 + arrayBuffer, 138 + fetch, 139 + pathOrUrlOrSchemas, 140 + resolvedInputs, 141 + }: { 142 + arrayBuffer?: ArrayBuffer[]; 143 + fetch?: RequestInit; 144 + pathOrUrlOrSchemas: Array<JSONSchema | string | unknown>; 145 + resolvedInputs?: ResolvedInput[]; 146 + }): Promise<JSONSchema> { 147 + await this.parseMany({ arrayBuffer, fetch, pathOrUrlOrSchemas, resolvedInputs }); 148 + this.mergeMany(); 149 + 150 + await resolveExternal(this, this.options); 151 + const errors = JSONParserErrorGroup.getParserErrors(this); 152 + if (errors.length > 0) { 153 + throw new JSONParserErrorGroup(this); 154 + } 155 + _bundle(this, this.options); 156 + // Merged root is ready for bundling 157 + 158 + const errors2 = JSONParserErrorGroup.getParserErrors(this); 159 + if (errors2.length > 0) { 160 + throw new JSONParserErrorGroup(this); 161 + } 162 + return this.schema!; 163 + } 164 + 165 + /** 166 + * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. 167 + * 168 + * The dereference method maintains object reference equality, meaning that all `$ref` pointers that point to the same object will be replaced with references to the same object. Again, this is great for programmatic usage, but it does introduce the risk of circular references, so be careful if you intend to serialize the schema using `JSON.stringify()`. Consider using the bundle method instead, which does not create circular references. 169 + * 170 + * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html#dereferenceschema-options-callback 171 + * 172 + * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file. 173 + */ 174 + public async dereference({ 175 + fetch, 176 + pathOrUrlOrSchema, 177 + }: { 178 + fetch?: RequestInit; 179 + pathOrUrlOrSchema: JSONSchema | string | unknown; 180 + }): Promise<JSONSchema> { 181 + await this.parse({ 182 + fetch, 183 + pathOrUrlOrSchema, 184 + }); 185 + await resolveExternal(this, this.options); 186 + const errors = JSONParserErrorGroup.getParserErrors(this); 187 + if (errors.length > 0) { 188 + throw new JSONParserErrorGroup(this); 189 + } 190 + _dereference(this, this.options); 191 + const errors2 = JSONParserErrorGroup.getParserErrors(this); 192 + if (errors2.length > 0) { 193 + throw new JSONParserErrorGroup(this); 194 + } 195 + return this.schema!; 196 + } 197 + 198 + /** 199 + * Parses the given JSON schema. 200 + * This method does not resolve any JSON references. 201 + * It just reads a single file in JSON or YAML format, and parse it as a JavaScript object. 202 + * 203 + * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file. 204 + * @returns - The returned promise resolves with the parsed JSON schema object. 205 + */ 206 + public async parse({ 207 + arrayBuffer, 208 + fetch, 209 + pathOrUrlOrSchema, 210 + resolvedInput: _resolvedInput, 211 + }: { 212 + arrayBuffer?: ArrayBuffer; 213 + fetch?: RequestInit; 214 + pathOrUrlOrSchema: JSONSchema | string | unknown; 215 + resolvedInput?: ResolvedInput; 216 + }): Promise<{ schema: JSONSchema }> { 217 + const resolvedInput = _resolvedInput || getResolvedInput({ pathOrUrlOrSchema }); 218 + const { path, type } = resolvedInput; 219 + let { schema } = resolvedInput; 220 + 221 + // reset everything 222 + this.schema = null; 223 + this.$refs = new $Refs(); 224 + 225 + if (schema) { 226 + // immediately add a new $Ref with the schema object as value 227 + const $ref = this.$refs._add(path); 228 + $ref.pathType = url.isFileSystemPath(path) ? 'file' : 'http'; 229 + $ref.value = schema; 230 + } else if (type !== 'json') { 231 + const file = newFile(path); 232 + 233 + // Add a new $Ref for this file, even though we don't have the value yet. 234 + // This ensures that we don't simultaneously read & parse the same file multiple times 235 + const $refAdded = this.$refs._add(file.url); 236 + $refAdded.pathType = type; 237 + try { 238 + const resolver = type === 'file' ? fileResolver : urlResolver; 239 + await resolver.handler({ 240 + arrayBuffer, 241 + fetch, 242 + file, 243 + }); 244 + const parseResult = await parseFile(file, this.options); 245 + $refAdded.value = parseResult.result; 246 + schema = parseResult.result; 247 + } catch (err) { 248 + if (isHandledError(err)) { 249 + $refAdded.value = err; 250 + } 251 + 252 + throw err; 253 + } 254 + } 255 + 256 + if (schema === null || typeof schema !== 'object' || Buffer.isBuffer(schema)) { 257 + throw ono.syntax(`"${this.$refs._root$Ref.path || schema}" is not a valid JSON Schema`); 258 + } 259 + 260 + this.schema = schema; 261 + 262 + return { 263 + schema, 264 + }; 265 + } 266 + 267 + private async parseMany({ 268 + arrayBuffer, 269 + fetch, 270 + pathOrUrlOrSchemas, 271 + resolvedInputs: _resolvedInputs, 272 + }: { 273 + arrayBuffer?: ArrayBuffer[]; 274 + fetch?: RequestInit; 275 + pathOrUrlOrSchemas: Array<JSONSchema | string | unknown>; 276 + resolvedInputs?: ResolvedInput[]; 277 + }): Promise<{ schemaMany: JSONSchema[] }> { 278 + const resolvedInputs = [...(_resolvedInputs || [])]; 279 + resolvedInputs.push( 280 + ...(pathOrUrlOrSchemas.map((schema) => getResolvedInput({ pathOrUrlOrSchema: schema })) || 281 + []), 282 + ); 283 + 284 + this.schemaMany = []; 285 + this.schemaManySources = []; 286 + this.sourcePathToPrefix = new Map(); 287 + 288 + for (let i = 0; i < resolvedInputs.length; i++) { 289 + const resolvedInput = resolvedInputs[i]!; 290 + const { path, type } = resolvedInput; 291 + let { schema } = resolvedInput; 292 + 293 + if (schema) { 294 + // keep schema as-is 295 + } else if (type !== 'json') { 296 + const file = newFile(path); 297 + 298 + // Add a new $Ref for this file, even though we don't have the value yet. 299 + // This ensures that we don't simultaneously read & parse the same file multiple times 300 + const $refAdded = this.$refs._add(file.url); 301 + $refAdded.pathType = type; 302 + try { 303 + const resolver = type === 'file' ? fileResolver : urlResolver; 304 + await resolver.handler({ 305 + arrayBuffer: arrayBuffer?.[i], 306 + fetch, 307 + file, 308 + }); 309 + const parseResult = await parseFile(file, this.options); 310 + $refAdded.value = parseResult.result; 311 + schema = parseResult.result; 312 + } catch (err) { 313 + if (isHandledError(err)) { 314 + $refAdded.value = err; 315 + } 316 + 317 + throw err; 318 + } 319 + } 320 + 321 + if (schema === null || typeof schema !== 'object' || Buffer.isBuffer(schema)) { 322 + throw ono.syntax(`"${this.$refs._root$Ref.path || schema}" is not a valid JSON Schema`); 323 + } 324 + 325 + this.schemaMany.push(schema); 326 + this.schemaManySources.push(path && path.length ? path : url.cwd()); 327 + } 328 + 329 + return { 330 + schemaMany: this.schemaMany, 331 + }; 332 + } 333 + 334 + public mergeMany(): JSONSchema { 335 + const schemas = this.schemaMany || []; 336 + if (schemas.length === 0) { 337 + throw ono('mergeMany called with no schemas. Did you run parseMany?'); 338 + } 339 + 340 + const merged: any = {}; 341 + 342 + // Determine spec version: prefer first occurrence of openapi, else swagger 343 + let chosenOpenapi: string | undefined; 344 + let chosenSwagger: string | undefined; 345 + for (const s of schemas) { 346 + if (!chosenOpenapi && s && typeof (s as any).openapi === 'string') { 347 + chosenOpenapi = (s as any).openapi; 348 + } 349 + if (!chosenSwagger && s && typeof (s as any).swagger === 'string') { 350 + chosenSwagger = (s as any).swagger; 351 + } 352 + if (chosenOpenapi && chosenSwagger) { 353 + break; 354 + } 355 + } 356 + if (typeof chosenOpenapi === 'string') { 357 + merged.openapi = chosenOpenapi; 358 + } else if (typeof chosenSwagger === 'string') { 359 + merged.swagger = chosenSwagger; 360 + } 361 + 362 + // Merge info: take first non-empty per-field across inputs 363 + const infoAccumulator: any = {}; 364 + for (const s of schemas) { 365 + const info = (s as any)?.info; 366 + if (info && typeof info === 'object') { 367 + for (const [k, v] of Object.entries(info)) { 368 + if (infoAccumulator[k] === undefined && v !== undefined) { 369 + infoAccumulator[k] = JSON.parse(JSON.stringify(v)); 370 + } 371 + } 372 + } 373 + } 374 + if (Object.keys(infoAccumulator).length > 0) { 375 + merged.info = infoAccumulator; 376 + } 377 + 378 + // Merge servers: union by url+description 379 + const servers: any[] = []; 380 + const seenServers = new Set<string>(); 381 + for (const s of schemas) { 382 + const arr = (s as any)?.servers; 383 + if (Array.isArray(arr)) { 384 + for (const srv of arr) { 385 + if (srv && typeof srv === 'object') { 386 + const key = `${srv.url || ''}|${srv.description || ''}`; 387 + if (!seenServers.has(key)) { 388 + seenServers.add(key); 389 + servers.push(JSON.parse(JSON.stringify(srv))); 390 + } 391 + } 392 + } 393 + } 394 + } 395 + if (servers.length > 0) { 396 + merged.servers = servers; 397 + } 398 + 399 + merged.paths = {}; 400 + merged.components = {}; 401 + 402 + const componentSections = [ 403 + 'schemas', 404 + 'parameters', 405 + 'requestBodies', 406 + 'responses', 407 + 'headers', 408 + 'securitySchemes', 409 + 'examples', 410 + 'links', 411 + 'callbacks', 412 + ]; 413 + for (const sec of componentSections) { 414 + merged.components[sec] = {}; 415 + } 416 + 417 + const tagNameSet = new Set<string>(); 418 + const tags: any[] = []; 419 + const usedOpIds = new Set<string>(); 420 + 421 + const baseName = (p: string) => { 422 + try { 423 + const withoutHash = p.split('#')[0]!; 424 + const parts = withoutHash.split('/'); 425 + const filename = parts[parts.length - 1] || 'schema'; 426 + const dot = filename.lastIndexOf('.'); 427 + const raw = dot > 0 ? filename.substring(0, dot) : filename; 428 + return raw.replace(/[^A-Za-z0-9_-]/g, '_'); 429 + } catch { 430 + return 'schema'; 431 + } 432 + }; 433 + const unique = (set: Set<string>, proposed: string) => { 434 + let name = proposed; 435 + let i = 2; 436 + while (set.has(name)) { 437 + name = `${proposed}_${i++}`; 438 + } 439 + set.add(name); 440 + return name; 441 + }; 442 + 443 + const rewriteRef = (ref: string, refMap: Map<string, string>): string => { 444 + // OAS3: #/components/{section}/{name}... 445 + let m = ref.match(/^#\/components\/([^/]+)\/([^/]+)(.*)$/); 446 + if (m) { 447 + const base = `#/components/${m[1]}/${m[2]}`; 448 + const mapped = refMap.get(base); 449 + if (mapped) { 450 + return mapped + (m[3] || ''); 451 + } 452 + } 453 + // OAS2: #/definitions/{name}... 454 + m = ref.match(/^#\/definitions\/([^/]+)(.*)$/); 455 + if (m) { 456 + const base = `#/components/schemas/${m[1]}`; 457 + const mapped = refMap.get(base); 458 + if (mapped) { 459 + // map definitions -> components/schemas 460 + return mapped + (m[2] || ''); 461 + } 462 + } 463 + return ref; 464 + }; 465 + 466 + const cloneAndRewrite = ( 467 + obj: any, 468 + refMap: Map<string, string>, 469 + tagMap: Map<string, string>, 470 + opIdPrefix: string, 471 + basePath: string, 472 + ): any => { 473 + if (obj === null || obj === undefined) { 474 + return obj; 475 + } 476 + if (Array.isArray(obj)) { 477 + return obj.map((v) => cloneAndRewrite(v, refMap, tagMap, opIdPrefix, basePath)); 478 + } 479 + if (typeof obj !== 'object') { 480 + return obj; 481 + } 482 + 483 + const out: any = {}; 484 + for (const [k, v] of Object.entries(obj)) { 485 + if (k === '$ref' && typeof v === 'string') { 486 + const s = v as string; 487 + if (s.startsWith('#')) { 488 + out[k] = rewriteRef(s, refMap); 489 + } else { 490 + const proto = url.getProtocol(s); 491 + if (proto === undefined) { 492 + // relative external ref -> absolutize against source base path 493 + out[k] = url.resolve(basePath + '#', s); 494 + } else { 495 + out[k] = s; 496 + } 497 + } 498 + } else if (k === 'tags' && Array.isArray(v) && v.every((x) => typeof x === 'string')) { 499 + out[k] = v.map((t) => tagMap.get(t) || t); 500 + } else if (k === 'operationId' && typeof v === 'string') { 501 + out[k] = unique(usedOpIds, `${opIdPrefix}_${v}`); 502 + } else { 503 + out[k] = cloneAndRewrite(v as any, refMap, tagMap, opIdPrefix, basePath); 504 + } 505 + } 506 + return out; 507 + }; 508 + 509 + for (let i = 0; i < schemas.length; i++) { 510 + const schema: any = schemas[i] || {}; 511 + const sourcePath = this.schemaManySources[i] || `multi://input/${i + 1}`; 512 + const prefix = baseName(sourcePath); 513 + 514 + // Track prefix for this source path (strip hash). Only map real file/http paths 515 + const withoutHash = url.stripHash(sourcePath); 516 + const protocol = url.getProtocol(withoutHash); 517 + if ( 518 + protocol === undefined || 519 + protocol === 'file' || 520 + protocol === 'http' || 521 + protocol === 'https' 522 + ) { 523 + this.sourcePathToPrefix.set(withoutHash, prefix); 524 + } 525 + 526 + const refMap = new Map<string, string>(); 527 + const tagMap = new Map<string, string>(); 528 + 529 + const srcComponents = (schema.components || {}) as any; 530 + for (const sec of componentSections) { 531 + const group = srcComponents[sec] || {}; 532 + for (const [name] of Object.entries(group)) { 533 + const newName = `${prefix}_${name}`; 534 + refMap.set(`#/components/${sec}/${name}`, `#/components/${sec}/${newName}`); 535 + } 536 + } 537 + 538 + const srcTags: any[] = Array.isArray(schema.tags) ? schema.tags : []; 539 + for (const t of srcTags) { 540 + if (!t || typeof t !== 'object' || typeof t.name !== 'string') { 541 + continue; 542 + } 543 + const desired = t.name; 544 + const finalName = tagNameSet.has(desired) ? `${prefix}_${desired}` : desired; 545 + tagNameSet.add(finalName); 546 + tagMap.set(desired, finalName); 547 + if (!tags.find((x) => x && x.name === finalName)) { 548 + tags.push({ ...t, name: finalName }); 549 + } 550 + } 551 + 552 + for (const sec of componentSections) { 553 + const group = (schema.components && schema.components[sec]) || {}; 554 + for (const [name, val] of Object.entries(group)) { 555 + const newName = `${prefix}_${name}`; 556 + merged.components[sec][newName] = cloneAndRewrite( 557 + val, 558 + refMap, 559 + tagMap, 560 + prefix, 561 + url.stripHash(sourcePath), 562 + ); 563 + } 564 + } 565 + 566 + const srcPaths = (schema.paths || {}) as Record<string, any>; 567 + for (const [p, item] of Object.entries(srcPaths)) { 568 + let targetPath = p; 569 + if (merged.paths[p]) { 570 + const trimmed = p.startsWith('/') ? p.substring(1) : p; 571 + targetPath = `/${prefix}/${trimmed}`; 572 + } 573 + merged.paths[targetPath] = cloneAndRewrite( 574 + item, 575 + refMap, 576 + tagMap, 577 + prefix, 578 + url.stripHash(sourcePath), 579 + ); 580 + } 581 + } 582 + 583 + if (tags.length > 0) { 584 + merged.tags = tags; 585 + } 586 + 587 + // Rebuild $refs root using the first input's path to preserve external resolution semantics 588 + const rootPath = this.schemaManySources[0] || url.cwd(); 589 + this.$refs = new $Refs(); 590 + const rootRef = this.$refs._add(rootPath); 591 + rootRef.pathType = url.isFileSystemPath(rootPath) ? 'file' : 'http'; 592 + rootRef.value = merged; 593 + this.schema = merged; 594 + return merged as JSONSchema; 595 + } 596 + } 597 + 598 + export { sendRequest } from './resolvers/url'; 599 + export type { JSONSchema } from './types';
+112
packages/json-schema-ref-parser/src/options.ts
··· 1 + import { binaryParser } from './parsers/binary'; 2 + import { jsonParser } from './parsers/json'; 3 + import { textParser } from './parsers/text'; 4 + import { yamlParser } from './parsers/yaml'; 5 + import type { JSONSchemaObject, Plugin } from './types'; 6 + 7 + export interface DereferenceOptions { 8 + /** 9 + * Determines whether circular `$ref` pointers are handled. 10 + * 11 + * If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references. 12 + * 13 + * If set to `"ignore"`, then circular references will simply be ignored. No error will be thrown, but the `$Refs.circular` property will still be set to `true`. 14 + */ 15 + circular?: boolean | 'ignore'; 16 + /** 17 + * A function, called for each path, which can return true to stop this path and all 18 + * subpaths from being dereferenced further. This is useful in schemas where some 19 + * subpaths contain literal $ref keys that should not be dereferenced. 20 + */ 21 + excludedPathMatcher?(path: string): boolean; 22 + /** 23 + * Callback invoked during dereferencing. 24 + * 25 + * @argument {string} path - The path being dereferenced (ie. the `$ref` string) 26 + * @argument {JSONSchemaObject} value - The JSON-Schema that the `$ref` resolved to 27 + * @argument {JSONSchemaObject} parent - The parent of the dereferenced object 28 + * @argument {string} parentPropName - The prop name of the parent object whose value was dereferenced 29 + */ 30 + onDereference?( 31 + path: string, 32 + value: JSONSchemaObject, 33 + parent?: JSONSchemaObject, 34 + parentPropName?: string, 35 + ): void; 36 + } 37 + 38 + /** 39 + * Options that determine how JSON schemas are parsed, resolved, and dereferenced. 40 + * 41 + * @param [options] - Overridden options 42 + * @class 43 + */ 44 + export interface $RefParserOptions { 45 + /** 46 + * The `dereference` options control how JSON Schema `$Ref` Parser will dereference `$ref` pointers within the JSON schema. 47 + */ 48 + dereference: DereferenceOptions; 49 + /** 50 + * The `parse` options determine how different types of files will be parsed. 51 + * 52 + * JSON Schema `$Ref` Parser comes with built-in JSON, YAML, plain-text, and binary parsers, any of which you can configure or disable. You can also add your own custom parsers if you want. 53 + */ 54 + parse: { 55 + binary: Plugin; 56 + json: Plugin; 57 + text: Plugin; 58 + yaml: Plugin; 59 + }; 60 + /** 61 + * The maximum amount of time (in milliseconds) that JSON Schema $Ref Parser will spend dereferencing a single schema. 62 + * It will throw a timeout error if the operation takes longer than this. 63 + */ 64 + timeoutMs?: number; 65 + } 66 + 67 + export const getJsonSchemaRefParserDefaultOptions = (): $RefParserOptions => ({ 68 + /** 69 + * Determines the types of JSON references that are allowed. 70 + */ 71 + dereference: { 72 + /** 73 + * Dereference circular (recursive) JSON references? 74 + * If false, then a {@link ReferenceError} will be thrown if a circular reference is found. 75 + * If "ignore", then circular references will not be dereferenced. 76 + * 77 + * @type {boolean|string} 78 + */ 79 + circular: true, 80 + /** 81 + * A function, called for each path, which can return true to stop this path and all 82 + * subpaths from being dereferenced further. This is useful in schemas where some 83 + * subpaths contain literal $ref keys that should not be dereferenced. 84 + * 85 + * @type {function} 86 + */ 87 + excludedPathMatcher: () => false, 88 + // @ts-expect-error 89 + referenceResolution: 'relative', 90 + }, 91 + /** 92 + * Determines how different types of files will be parsed. 93 + * 94 + * You can add additional parsers of your own, replace an existing one with 95 + * your own implementation, or disable any parser by setting it to false. 96 + */ 97 + parse: { 98 + binary: { ...binaryParser }, 99 + json: { ...jsonParser }, 100 + text: { ...textParser }, 101 + yaml: { ...yamlParser }, 102 + }, 103 + }); 104 + 105 + export type Options = $RefParserOptions; 106 + 107 + type DeepPartial<T> = T extends object 108 + ? { 109 + [P in keyof T]?: DeepPartial<T[P]>; 110 + } 111 + : T; 112 + export type ParserOptions = DeepPartial<$RefParserOptions>;
+65
packages/json-schema-ref-parser/src/parse.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + 3 + import type { $RefParserOptions } from './options'; 4 + import type { FileInfo } from './types'; 5 + import { ParserError } from './util/errors'; 6 + import type { PluginResult } from './util/plugins'; 7 + import * as plugins from './util/plugins'; 8 + import { getExtension } from './util/url'; 9 + 10 + /** 11 + * Prepares the file object so we can populate it with data and other values 12 + * when it's read and parsed. This "file object" will be passed to all 13 + * resolvers and parsers. 14 + */ 15 + export function newFile(path: string): FileInfo { 16 + let url = path; 17 + // Remove the URL fragment, if any 18 + const hashIndex = url.indexOf('#'); 19 + let hash = ''; 20 + if (hashIndex > -1) { 21 + hash = url.substring(hashIndex); 22 + url = url.substring(0, hashIndex); 23 + } 24 + return { 25 + extension: getExtension(url), 26 + hash, 27 + url, 28 + } as FileInfo; 29 + } 30 + 31 + /** 32 + * Parses the given file's contents, using the configured parser plugins. 33 + */ 34 + export const parseFile = async ( 35 + file: FileInfo, 36 + options: $RefParserOptions, 37 + ): Promise<PluginResult> => { 38 + try { 39 + // If none of the parsers are a match for this file, try all of them. This 40 + // handles situations where the file is a supported type, just with an 41 + // unknown extension. 42 + const parsers = [ 43 + options.parse.json, 44 + options.parse.yaml, 45 + options.parse.text, 46 + options.parse.binary, 47 + ]; 48 + const filtered = parsers.filter((plugin) => plugin.canHandle(file)); 49 + return await plugins.run(filtered.length ? filtered : parsers, file); 50 + } catch (error: any) { 51 + if (error && error.message && error.message.startsWith('Error parsing')) { 52 + throw error; 53 + } 54 + 55 + if (!error || !('error' in error)) { 56 + throw ono.syntax(`Unable to parse ${file.url}`); 57 + } 58 + 59 + if (error.error instanceof ParserError) { 60 + throw error.error; 61 + } 62 + 63 + throw new ParserError(error.error.message, file.url); 64 + } 65 + };
+13
packages/json-schema-ref-parser/src/parsers/binary.ts
··· 1 + import type { FileInfo, Plugin } from '../types'; 2 + 3 + const BINARY_REGEXP = /\.(jpeg|jpg|gif|png|bmp|ico)$/i; 4 + 5 + export const binaryParser: Plugin = { 6 + canHandle: (file: FileInfo) => Buffer.isBuffer(file.data) && BINARY_REGEXP.test(file.url), 7 + handler: (file: FileInfo): Buffer => 8 + Buffer.isBuffer(file.data) 9 + ? file.data 10 + : // This will reject if data is anything other than a string or typed array 11 + Buffer.from(file.data), 12 + name: 'binary', 13 + };
+38
packages/json-schema-ref-parser/src/parsers/json.ts
··· 1 + import type { FileInfo, Plugin } from '../types'; 2 + import { ParserError } from '../util/errors'; 3 + 4 + export const jsonParser: Plugin = { 5 + canHandle: (file: FileInfo) => file.extension === '.json', 6 + async handler(file: FileInfo): Promise<object | undefined> { 7 + let data = file.data; 8 + if (Buffer.isBuffer(data)) { 9 + data = data.toString(); 10 + } 11 + 12 + if (typeof data !== 'string') { 13 + // data is already a JavaScript value (object, array, number, null, NaN, etc.) 14 + return data as object; 15 + } 16 + 17 + if (!data.trim().length) { 18 + // this mirrors the YAML behavior 19 + return; 20 + } 21 + 22 + try { 23 + return JSON.parse(data); 24 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 + } catch (error: any) { 26 + try { 27 + // find the first curly brace 28 + const firstCurlyBrace = data.indexOf('{'); 29 + // remove any characters before the first curly brace 30 + data = data.slice(firstCurlyBrace); 31 + return JSON.parse(data); 32 + } catch (error: any) { 33 + throw new ParserError(error.message, file.url); 34 + } 35 + } 36 + }, 37 + name: 'json', 38 + };
+21
packages/json-schema-ref-parser/src/parsers/text.ts
··· 1 + import type { FileInfo, Plugin } from '../types'; 2 + import { ParserError } from '../util/errors'; 3 + 4 + const TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i; 5 + 6 + export const textParser: Plugin = { 7 + canHandle: (file: FileInfo) => 8 + (typeof file.data === 'string' || Buffer.isBuffer(file.data)) && TEXT_REGEXP.test(file.url), 9 + handler(file: FileInfo): string { 10 + if (typeof file.data === 'string') { 11 + return file.data; 12 + } 13 + 14 + if (!Buffer.isBuffer(file.data)) { 15 + throw new ParserError('data is not text', file.url); 16 + } 17 + 18 + return file.data.toString('utf-8'); 19 + }, 20 + name: 'text', 21 + };
+26
packages/json-schema-ref-parser/src/parsers/yaml.ts
··· 1 + import yaml from 'js-yaml'; 2 + import { JSON_SCHEMA } from 'js-yaml'; 3 + 4 + import type { FileInfo, JSONSchema, Plugin } from '../types'; 5 + import { ParserError } from '../util/errors'; 6 + 7 + export const yamlParser: Plugin = { 8 + // JSON is valid YAML 9 + canHandle: (file: FileInfo) => ['.yaml', '.yml', '.json'].includes(file.extension), 10 + handler: async (file: FileInfo): Promise<JSONSchema> => { 11 + const data = Buffer.isBuffer(file.data) ? file.data.toString() : file.data; 12 + 13 + if (typeof data !== 'string') { 14 + // data is already a JavaScript value (object, array, number, null, NaN, etc.) 15 + return data; 16 + } 17 + 18 + try { 19 + const yamlSchema = yaml.load(data, { schema: JSON_SCHEMA }) as JSONSchema; 20 + return yamlSchema; 21 + } catch (error: any) { 22 + throw new ParserError(error?.message || 'Parser Error', file.url); 23 + } 24 + }, 25 + name: 'yaml', 26 + };
+352
packages/json-schema-ref-parser/src/pointer.ts
··· 1 + import type { ParserOptions } from './options'; 2 + import $Ref from './ref'; 3 + import type { JSONSchema } from './types'; 4 + import { 5 + InvalidPointerError, 6 + isHandledError, 7 + JSONParserError, 8 + MissingPointerError, 9 + } from './util/errors'; 10 + import * as url from './util/url'; 11 + 12 + const slashes = /\//g; 13 + const tildes = /~/g; 14 + const escapedSlash = /~1/g; 15 + const escapedTilde = /~0/g; 16 + 17 + const safeDecodeURIComponent = (encodedURIComponent: string): string => { 18 + try { 19 + return decodeURIComponent(encodedURIComponent); 20 + } catch { 21 + return encodedURIComponent; 22 + } 23 + }; 24 + 25 + /** 26 + * This class represents a single JSON pointer and its resolved value. 27 + * 28 + * @param $ref 29 + * @param path 30 + * @param [friendlyPath] - The original user-specified path (used for error messages) 31 + * @class 32 + */ 33 + class Pointer<S extends object = JSONSchema> { 34 + /** 35 + * The {@link $Ref} object that contains this {@link Pointer} object. 36 + */ 37 + $ref: $Ref<S>; 38 + 39 + /** 40 + * The file path or URL, containing the JSON pointer in the hash. 41 + * This path is relative to the path of the main JSON schema file. 42 + */ 43 + path: string; 44 + 45 + /** 46 + * The original path or URL, used for error messages. 47 + */ 48 + originalPath: string; 49 + 50 + /** 51 + * The value of the JSON pointer. 52 + * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays). 53 + */ 54 + 55 + value: any; 56 + /** 57 + * Indicates whether the pointer references itself. 58 + */ 59 + circular: boolean; 60 + /** 61 + * The number of indirect references that were traversed to resolve the value. 62 + * Resolving a single pointer may require resolving multiple $Refs. 63 + */ 64 + indirections: number; 65 + 66 + constructor($ref: $Ref<S>, path: string, friendlyPath?: string) { 67 + this.$ref = $ref; 68 + 69 + this.path = path; 70 + 71 + this.originalPath = friendlyPath || path; 72 + 73 + this.value = undefined; 74 + 75 + this.circular = false; 76 + 77 + this.indirections = 0; 78 + } 79 + 80 + /** 81 + * Resolves the value of a nested property within the given object. 82 + * 83 + * @param obj - The object that will be crawled 84 + * @param options 85 + * @param pathFromRoot - the path of place that initiated resolving 86 + * 87 + * @returns 88 + * Returns a JSON pointer whose {@link Pointer#value} is the resolved value. 89 + * If resolving this value required resolving other JSON references, then 90 + * the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path 91 + * of the resolved value. 92 + */ 93 + resolve(obj: S, options?: ParserOptions, pathFromRoot?: string) { 94 + const tokens = Pointer.parse(this.path, this.originalPath); 95 + 96 + // Crawl the object, one token at a time 97 + this.value = unwrapOrThrow(obj); 98 + 99 + const errors: MissingPointerError[] = []; 100 + 101 + for (let i = 0; i < tokens.length; i++) { 102 + if (resolveIf$Ref(this, options, pathFromRoot)) { 103 + // The $ref path has changed, so append the remaining tokens to the path 104 + this.path = Pointer.join(this.path, tokens.slice(i)); 105 + } 106 + 107 + if ( 108 + typeof this.value === 'object' && 109 + this.value !== null && 110 + !isRootPath(pathFromRoot) && 111 + '$ref' in this.value 112 + ) { 113 + return this; 114 + } 115 + 116 + const token = tokens[i]!; 117 + if ( 118 + this.value[token] === undefined || 119 + (this.value[token] === null && i === tokens.length - 1) 120 + ) { 121 + // one final case is if the entry itself includes slashes, and was parsed out as a token - we can join the remaining tokens and try again 122 + let didFindSubstringSlashMatch = false; 123 + for (let j = tokens.length - 1; j > i; j--) { 124 + const joinedToken = tokens.slice(i, j + 1).join('/'); 125 + if (this.value[joinedToken] !== undefined) { 126 + this.value = this.value[joinedToken]; 127 + i = j; 128 + didFindSubstringSlashMatch = true; 129 + break; 130 + } 131 + } 132 + if (didFindSubstringSlashMatch) { 133 + continue; 134 + } 135 + 136 + this.value = null; 137 + errors.push(new MissingPointerError(token, decodeURI(this.originalPath))); 138 + } else { 139 + this.value = this.value[token]; 140 + } 141 + } 142 + 143 + if (errors.length > 0) { 144 + throw errors.length === 1 145 + ? errors[0] 146 + : new AggregateError(errors, 'Multiple missing pointer errors'); 147 + } 148 + 149 + // Resolve the final value 150 + if ( 151 + !this.value || 152 + (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot) 153 + ) { 154 + resolveIf$Ref(this, options, pathFromRoot); 155 + } 156 + 157 + return this; 158 + } 159 + 160 + /** 161 + * Sets the value of a nested property within the given object. 162 + * 163 + * @param obj - The object that will be crawled 164 + * @param value - the value to assign 165 + * @param options 166 + * 167 + * @returns 168 + * Returns the modified object, or an entirely new object if the entire object is overwritten. 169 + */ 170 + set(obj: S, value: any, options?: ParserOptions) { 171 + const tokens = Pointer.parse(this.path); 172 + let token; 173 + 174 + if (tokens.length === 0) { 175 + // There are no tokens, replace the entire object with the new value 176 + this.value = value; 177 + return value; 178 + } 179 + 180 + // Crawl the object, one token at a time 181 + this.value = unwrapOrThrow(obj); 182 + 183 + for (let i = 0; i < tokens.length - 1; i++) { 184 + resolveIf$Ref(this, options); 185 + 186 + token = tokens[i]!; 187 + if (this.value && this.value[token] !== undefined) { 188 + // The token exists 189 + this.value = this.value[token]; 190 + } else { 191 + // The token doesn't exist, so create it 192 + this.value = setValue(this, token, {}); 193 + } 194 + } 195 + 196 + // Set the value of the final token 197 + resolveIf$Ref(this, options); 198 + token = tokens[tokens.length - 1]; 199 + setValue(this, token, value); 200 + 201 + // Return the updated object 202 + return obj; 203 + } 204 + 205 + /** 206 + * Parses a JSON pointer (or a path containing a JSON pointer in the hash) 207 + * and returns an array of the pointer's tokens. 208 + * (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"]) 209 + * 210 + * The pointer is parsed according to RFC 6901 211 + * {@link https://tools.ietf.org/html/rfc6901#section-3} 212 + * 213 + * @param path 214 + * @param [originalPath] 215 + * @returns 216 + */ 217 + static parse(path: string, originalPath?: string): string[] { 218 + // Get the JSON pointer from the path's hash 219 + const pointer = url.getHash(path).substring(1); 220 + 221 + // If there's no pointer, then there are no tokens, 222 + // so return an empty array 223 + if (!pointer) { 224 + return []; 225 + } 226 + 227 + // Split into an array 228 + const split = pointer.split('/'); 229 + 230 + // Decode each part, according to RFC 6901 231 + for (let i = 0; i < split.length; i++) { 232 + split[i] = safeDecodeURIComponent( 233 + split[i]!.replace(escapedSlash, '/').replace(escapedTilde, '~'), 234 + ); 235 + } 236 + 237 + if (split[0] !== '') { 238 + throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath); 239 + } 240 + 241 + return split.slice(1); 242 + } 243 + 244 + /** 245 + * Creates a JSON pointer path, by joining one or more tokens to a base path. 246 + * 247 + * @param base - The base path (e.g. "schema.json#/definitions/person") 248 + * @param tokens - The token(s) to append (e.g. ["name", "first"]) 249 + * @returns 250 + */ 251 + static join(base: string, tokens: string | string[]) { 252 + // Ensure that the base path contains a hash 253 + if (base.indexOf('#') === -1) { 254 + base += '#'; 255 + } 256 + 257 + // Append each token to the base path 258 + tokens = Array.isArray(tokens) ? tokens : [tokens]; 259 + for (let i = 0; i < tokens.length; i++) { 260 + const token = tokens[i]!; 261 + // Encode the token, according to RFC 6901 262 + base += '/' + encodeURIComponent(token.replace(tildes, '~0').replace(slashes, '~1')); 263 + } 264 + 265 + return base; 266 + } 267 + } 268 + 269 + /** 270 + * If the given pointer's {@link Pointer#value} is a JSON reference, 271 + * then the reference is resolved and {@link Pointer#value} is replaced with the resolved value. 272 + * In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the 273 + * resolution path of the new value. 274 + * 275 + * @param pointer 276 + * @param options 277 + * @param [pathFromRoot] - the path of place that initiated resolving 278 + * @returns - Returns `true` if the resolution path changed 279 + */ 280 + function resolveIf$Ref(pointer: any, options: any, pathFromRoot?: any) { 281 + // Is the value a JSON reference? (and allowed?) 282 + 283 + if ($Ref.isAllowed$Ref(pointer.value)) { 284 + const $refPath = url.resolve(pointer.path, pointer.value.$ref); 285 + 286 + if ($refPath === pointer.path && !isRootPath(pathFromRoot)) { 287 + // The value is a reference to itself, so there's nothing to do. 288 + pointer.circular = true; 289 + } else { 290 + const resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options); 291 + if (resolved === null) { 292 + return false; 293 + } 294 + 295 + pointer.indirections += resolved.indirections + 1; 296 + 297 + if ($Ref.isExtended$Ref(pointer.value)) { 298 + // This JSON reference "extends" the resolved value, rather than simply pointing to it. 299 + // So the resolved path does NOT change. Just the value does. 300 + pointer.value = $Ref.dereference(pointer.value, resolved.value); 301 + return false; 302 + } else { 303 + // Resolve the reference 304 + pointer.$ref = resolved.$ref; 305 + pointer.path = resolved.path; 306 + pointer.value = resolved.value; 307 + } 308 + 309 + return true; 310 + } 311 + } 312 + return undefined; 313 + } 314 + export default Pointer; 315 + 316 + /** 317 + * Sets the specified token value of the {@link Pointer#value}. 318 + * 319 + * The token is evaluated according to RFC 6901. 320 + * {@link https://tools.ietf.org/html/rfc6901#section-4} 321 + * 322 + * @param pointer - The JSON Pointer whose value will be modified 323 + * @param token - A JSON Pointer token that indicates how to modify `obj` 324 + * @param value - The value to assign 325 + * @returns - Returns the assigned value 326 + */ 327 + function setValue(pointer: any, token: any, value: any) { 328 + if (pointer.value && typeof pointer.value === 'object') { 329 + if (token === '-' && Array.isArray(pointer.value)) { 330 + pointer.value.push(value); 331 + } else { 332 + pointer.value[token] = value; 333 + } 334 + } else { 335 + throw new JSONParserError( 336 + `Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`, 337 + ); 338 + } 339 + return value; 340 + } 341 + 342 + function unwrapOrThrow(value: any) { 343 + if (isHandledError(value)) { 344 + throw value; 345 + } 346 + 347 + return value; 348 + } 349 + 350 + function isRootPath(pathFromRoot: any): boolean { 351 + return typeof pathFromRoot == 'string' && Pointer.parse(pathFromRoot).length == 0; 352 + }
+283
packages/json-schema-ref-parser/src/ref.ts
··· 1 + import type { ParserOptions } from './options'; 2 + import Pointer from './pointer'; 3 + import type $Refs from './refs'; 4 + import type { JSONSchema } from './types'; 5 + import type { 6 + JSONParserError, 7 + MissingPointerError, 8 + ParserError, 9 + ResolverError, 10 + } from './util/errors'; 11 + import { normalizeError } from './util/errors'; 12 + 13 + export type $RefError = JSONParserError | ResolverError | ParserError | MissingPointerError; 14 + 15 + /** 16 + * This class represents a single JSON reference and its resolved value. 17 + * 18 + * @class 19 + */ 20 + class $Ref<S extends object = JSONSchema> { 21 + /** 22 + * The file path or URL of the referenced file. 23 + * This path is relative to the path of the main JSON schema file. 24 + * 25 + * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file. 26 + * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get 27 + * specific JSON pointers within the file. 28 + * 29 + * @type {string} 30 + */ 31 + path: undefined | string; 32 + 33 + /** 34 + * The resolved value of the JSON reference. 35 + * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays). 36 + * 37 + * @type {?*} 38 + */ 39 + value: any; 40 + 41 + /** 42 + * The {@link $Refs} object that contains this {@link $Ref} object. 43 + * 44 + * @type {$Refs} 45 + */ 46 + $refs: $Refs<S>; 47 + 48 + /** 49 + * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.) 50 + */ 51 + pathType: string | unknown; 52 + 53 + /** 54 + * List of all errors. Undefined if no errors. 55 + */ 56 + errors: Array<$RefError> = []; 57 + 58 + constructor($refs: $Refs<S>) { 59 + this.$refs = $refs; 60 + } 61 + 62 + /** 63 + * Pushes an error to errors array. 64 + * 65 + * @param err - The error to be pushed 66 + * @returns 67 + */ 68 + addError(err: $RefError) { 69 + if (this.errors === undefined) { 70 + this.errors = []; 71 + } 72 + 73 + const existingErrors = this.errors.map(({ footprint }: any) => footprint); 74 + 75 + // the path has been almost certainly set at this point, 76 + // but just in case something went wrong, normalizeError injects path if necessary 77 + // moreover, certain errors might point at the same spot, so filter them out to reduce noise 78 + if ('errors' in err && Array.isArray(err.errors)) { 79 + this.errors.push( 80 + ...err.errors 81 + .map(normalizeError) 82 + .filter(({ footprint }: any) => !existingErrors.includes(footprint)), 83 + ); 84 + } else if (!('footprint' in err) || !existingErrors.includes(err.footprint)) { 85 + this.errors.push(normalizeError(err)); 86 + } 87 + } 88 + 89 + /** 90 + * Determines whether the given JSON reference exists within this {@link $Ref#value}. 91 + * 92 + * @param path - The full path being resolved, optionally with a JSON pointer in the hash 93 + * @param options 94 + * @returns 95 + */ 96 + exists(path: string, options?: ParserOptions) { 97 + try { 98 + this.resolve(path, options); 99 + return true; 100 + } catch { 101 + return false; 102 + } 103 + } 104 + 105 + /** 106 + * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value. 107 + * 108 + * @param path - The full path being resolved, optionally with a JSON pointer in the hash 109 + * @param options 110 + * @returns - Returns the resolved value 111 + */ 112 + get(path: string, options?: ParserOptions) { 113 + return this.resolve(path, options)?.value; 114 + } 115 + 116 + /** 117 + * Resolves the given JSON reference within this {@link $Ref#value}. 118 + * 119 + * @param path - The full path being resolved, optionally with a JSON pointer in the hash 120 + * @param options 121 + * @param friendlyPath - The original user-specified path (used for error messages) 122 + * @param pathFromRoot - The path of `obj` from the schema root 123 + * @returns 124 + */ 125 + resolve(path: string, options?: ParserOptions, friendlyPath?: string, pathFromRoot?: string) { 126 + const pointer = new Pointer<S>(this, path, friendlyPath); 127 + return pointer.resolve(this.value, options, pathFromRoot); 128 + } 129 + 130 + /** 131 + * Sets the value of a nested property within this {@link $Ref#value}. 132 + * If the property, or any of its parents don't exist, they will be created. 133 + * 134 + * @param path - The full path of the property to set, optionally with a JSON pointer in the hash 135 + * @param value - The value to assign 136 + */ 137 + set(path: string, value: any) { 138 + const pointer = new Pointer(this, path); 139 + this.value = pointer.set(this.value, value); 140 + } 141 + 142 + /** 143 + * Determines whether the given value is a JSON reference. 144 + * 145 + * @param value - The value to inspect 146 + * @returns 147 + */ 148 + static is$Ref(value: unknown): value is { $ref: string; length?: number } { 149 + return ( 150 + Boolean(value) && 151 + typeof value === 'object' && 152 + value !== null && 153 + '$ref' in value && 154 + typeof value.$ref === 'string' && 155 + value.$ref.length > 0 156 + ); 157 + } 158 + 159 + /** 160 + * Determines whether the given value is an external JSON reference. 161 + * 162 + * @param value - The value to inspect 163 + * @returns 164 + */ 165 + static isExternal$Ref(value: unknown): boolean { 166 + return $Ref.is$Ref(value) && value.$ref![0] !== '#'; 167 + } 168 + 169 + /** 170 + * Determines whether the given value is a JSON reference, and whether it is allowed by the options. 171 + * 172 + * @param value - The value to inspect 173 + * @param options 174 + * @returns 175 + */ 176 + static isAllowed$Ref(value: unknown) { 177 + if (this.is$Ref(value)) { 178 + if (value.$ref.substring(0, 2) === '#/' || value.$ref === '#') { 179 + // It's a JSON Pointer reference, which is always allowed 180 + return true; 181 + } else if (value.$ref[0] !== '#') { 182 + // It's an external reference, which is allowed by the options 183 + return true; 184 + } 185 + } 186 + return undefined; 187 + } 188 + 189 + /** 190 + * Determines whether the given value is a JSON reference that "extends" its resolved value. 191 + * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to 192 + * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved 193 + * value, plus the extra properties. 194 + * 195 + * @example: { 196 + person: { 197 + properties: { 198 + firstName: { type: string } 199 + lastName: { type: string } 200 + } 201 + } 202 + employee: { 203 + properties: { 204 + $ref: #/person/properties 205 + salary: { type: number } 206 + } 207 + } 208 + } 209 + * In this example, "employee" is an extended $ref, since it extends "person" with an additional 210 + * property (salary). The result is a NEW value that looks like this: 211 + * 212 + * { 213 + * properties: { 214 + * firstName: { type: string } 215 + * lastName: { type: string } 216 + * salary: { type: number } 217 + * } 218 + * } 219 + * 220 + * @param value - The value to inspect 221 + * @returns 222 + */ 223 + static isExtended$Ref(value: unknown) { 224 + return $Ref.is$Ref(value) && Object.keys(value).length > 1; 225 + } 226 + 227 + /** 228 + * Returns the resolved value of a JSON Reference. 229 + * If necessary, the resolved value is merged with the JSON Reference to create a new object 230 + * 231 + * @example: { 232 + person: { 233 + properties: { 234 + firstName: { type: string } 235 + lastName: { type: string } 236 + } 237 + } 238 + employee: { 239 + properties: { 240 + $ref: #/person/properties 241 + salary: { type: number } 242 + } 243 + } 244 + } When "person" and "employee" are merged, you end up with the following object: 245 + * 246 + * { 247 + * properties: { 248 + * firstName: { type: string } 249 + * lastName: { type: string } 250 + * salary: { type: number } 251 + * } 252 + * } 253 + * 254 + * @param $ref - The JSON reference object (the one with the "$ref" property) 255 + * @param resolvedValue - The resolved value, which can be any type 256 + * @returns - Returns the dereferenced value 257 + */ 258 + static dereference<S extends object = JSONSchema>($ref: $Ref<S>, resolvedValue: S): S { 259 + if (resolvedValue && typeof resolvedValue === 'object' && $Ref.isExtended$Ref($ref)) { 260 + const merged = {}; 261 + for (const key of Object.keys($ref)) { 262 + if (key !== '$ref') { 263 + // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message 264 + merged[key] = $ref[key]; 265 + } 266 + } 267 + 268 + for (const key of Object.keys(resolvedValue)) { 269 + if (!(key in merged)) { 270 + // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message 271 + merged[key] = resolvedValue[key]; 272 + } 273 + } 274 + 275 + return merged as S; 276 + } else { 277 + // Completely replace the original reference with the resolved value 278 + return resolvedValue; 279 + } 280 + } 281 + } 282 + 283 + export default $Ref;
+231
packages/json-schema-ref-parser/src/refs.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + import type { JSONSchema4Type, JSONSchema6Type, JSONSchema7Type } from 'json-schema'; 3 + 4 + import type { ParserOptions } from './options'; 5 + import $Ref from './ref'; 6 + import type { JSONSchema } from './types'; 7 + import convertPathToPosix from './util/convert-path-to-posix'; 8 + import * as url from './util/url'; 9 + 10 + interface $RefsMap<S extends object = JSONSchema> { 11 + [url: string]: $Ref<S>; 12 + } 13 + /** 14 + * When you call the resolve method, the value that gets passed to the callback function (or Promise) is a $Refs object. This same object is accessible via the parser.$refs property of $RefParser objects. 15 + * 16 + * This object is a map of JSON References and their resolved values. It also has several convenient helper methods that make it easy for you to navigate and manipulate the JSON References. 17 + * 18 + * See https://apitools.dev/json-schema-ref-parser/docs/refs.html 19 + */ 20 + export default class $Refs<S extends object = JSONSchema> { 21 + /** 22 + * This property is true if the schema contains any circular references. You may want to check this property before serializing the dereferenced schema as JSON, since JSON.stringify() does not support circular references by default. 23 + * 24 + * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#circular 25 + */ 26 + public circular: boolean; 27 + 28 + /** 29 + * Returns the paths/URLs of all the files in your schema (including the main schema file). 30 + * 31 + * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#pathstypes 32 + * 33 + * @param types (optional) Optionally only return certain types of paths ("file", "http", etc.) 34 + */ 35 + paths(...types: (string | string[])[]): string[] { 36 + const paths = getPaths(this._$refs, types.flat()); 37 + return paths.map((path) => convertPathToPosix(path.decoded)); 38 + } 39 + 40 + /** 41 + * Returns a map of paths/URLs and their correspond values. 42 + * 43 + * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#valuestypes 44 + * 45 + * @param types (optional) Optionally only return values from certain locations ("file", "http", etc.) 46 + */ 47 + values(...types: (string | string[])[]): S { 48 + const $refs = this._$refs; 49 + const paths = getPaths($refs, types.flat()); 50 + return paths.reduce<Record<string, any>>((obj, path) => { 51 + obj[convertPathToPosix(path.decoded)] = $refs[path.encoded]!.value; 52 + return obj; 53 + }, {}) as S; 54 + } 55 + 56 + /** 57 + * Returns `true` if the given path exists in the schema; otherwise, returns `false` 58 + * 59 + * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#existsref 60 + * 61 + * @param $ref The JSON Reference path, optionally with a JSON Pointer in the hash 62 + */ 63 + /** 64 + * Determines whether the given JSON reference exists. 65 + * 66 + * @param path - The path being resolved, optionally with a JSON pointer in the hash 67 + * @param [options] 68 + * @returns 69 + */ 70 + exists(path: string, options: any) { 71 + try { 72 + this._resolve(path, '', options); 73 + return true; 74 + } catch { 75 + return false; 76 + } 77 + } 78 + 79 + /** 80 + * Resolves the given JSON reference and returns the resolved value. 81 + * 82 + * @param path - The path being resolved, with a JSON pointer in the hash 83 + * @param [options] 84 + * @returns - Returns the resolved value 85 + */ 86 + get(path: string, options?: ParserOptions): JSONSchema4Type | JSONSchema6Type | JSONSchema7Type { 87 + return this._resolve(path, '', options)!.value; 88 + } 89 + 90 + /** 91 + * Sets the value at the given path in the schema. If the property, or any of its parents, don't exist, they will be created. 92 + * 93 + * @param path The JSON Reference path, optionally with a JSON Pointer in the hash 94 + * @param value The value to assign. Can be anything (object, string, number, etc.) 95 + */ 96 + set(path: string, value: JSONSchema4Type | JSONSchema6Type | JSONSchema7Type) { 97 + const absPath = url.resolve(this._root$Ref.path!, path); 98 + const withoutHash = url.stripHash(absPath); 99 + const $ref = this._$refs[withoutHash]; 100 + 101 + if (!$ref) { 102 + throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`); 103 + } 104 + 105 + $ref.set(absPath, value); 106 + } 107 + /** 108 + * Returns the specified {@link $Ref} object, or undefined. 109 + * 110 + * @param path - The path being resolved, optionally with a JSON pointer in the hash 111 + * @returns 112 + * @protected 113 + */ 114 + _get$Ref(path: string) { 115 + path = url.resolve(this._root$Ref.path!, path); 116 + const withoutHash = url.stripHash(path); 117 + return this._$refs[withoutHash]; 118 + } 119 + 120 + /** 121 + * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object. 122 + * 123 + * @param path - The file path or URL of the referenced file 124 + */ 125 + _add(path: string) { 126 + const withoutHash = url.stripHash(path); 127 + 128 + const $ref = new $Ref<S>(this); 129 + $ref.path = withoutHash; 130 + 131 + this._$refs[withoutHash] = $ref; 132 + this._root$Ref = this._root$Ref || $ref; 133 + 134 + return $ref; 135 + } 136 + 137 + /** 138 + * Resolves the given JSON reference. 139 + * 140 + * @param path - The path being resolved, optionally with a JSON pointer in the hash 141 + * @param pathFromRoot - The path of `obj` from the schema root 142 + * @param [options] 143 + * @returns 144 + * @protected 145 + */ 146 + _resolve(path: string, pathFromRoot: string, options?: ParserOptions) { 147 + const absPath = url.resolve(this._root$Ref.path!, path); 148 + const withoutHash = url.stripHash(absPath); 149 + const $ref = this._$refs[withoutHash]; 150 + 151 + if (!$ref) { 152 + throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`); 153 + } 154 + 155 + return $ref.resolve(absPath, options, path, pathFromRoot); 156 + } 157 + 158 + /** 159 + * A map of paths/urls to {@link $Ref} objects 160 + * 161 + * @type {object} 162 + * @protected 163 + */ 164 + _$refs: $RefsMap<S> = {}; 165 + 166 + /** 167 + * The {@link $Ref} object that is the root of the JSON schema. 168 + * 169 + * @type {$Ref} 170 + * @protected 171 + */ 172 + _root$Ref: $Ref<S>; 173 + 174 + constructor() { 175 + /** 176 + * Indicates whether the schema contains any circular references. 177 + * 178 + * @type {boolean} 179 + */ 180 + this.circular = false; 181 + 182 + this._$refs = {}; 183 + 184 + // @ts-ignore 185 + this._root$Ref = null; 186 + } 187 + 188 + /** 189 + * Returns the paths of all the files/URLs that are referenced by the JSON schema, 190 + * including the schema itself. 191 + * 192 + * @param [types] - Only return paths of the given types ("file", "http", etc.) 193 + * @returns 194 + */ 195 + /** 196 + * Returns the map of JSON references and their resolved values. 197 + * 198 + * @param [types] - Only return references of the given types ("file", "http", etc.) 199 + * @returns 200 + */ 201 + 202 + /** 203 + * Returns a POJO (plain old JavaScript object) for serialization as JSON. 204 + * 205 + * @returns {object} 206 + */ 207 + toJSON = this.values; 208 + } 209 + 210 + /** 211 + * Returns the encoded and decoded paths keys of the given object. 212 + * 213 + * @param $refs - The object whose keys are URL-encoded paths 214 + * @param [types] - Only return paths of the given types ("file", "http", etc.) 215 + * @returns 216 + */ 217 + function getPaths<S extends object = JSONSchema>($refs: $RefsMap<S>, types: string[]) { 218 + let paths = Object.keys($refs); 219 + 220 + // Filter the paths by type 221 + types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types); 222 + if (types.length > 0 && types[0]) { 223 + paths = paths.filter((key) => types.includes($refs[key]!.pathType as string)); 224 + } 225 + 226 + // Decode local filesystem paths 227 + return paths.map((path) => ({ 228 + decoded: $refs[path]!.pathType === 'file' ? url.toFileSystemPath(path, true) : path, 229 + encoded: path, 230 + })); 231 + }
+142
packages/json-schema-ref-parser/src/resolve-external.ts
··· 1 + import type { $RefParser } from '.'; 2 + import { getResolvedInput } from '.'; 3 + import type { $RefParserOptions } from './options'; 4 + import { newFile, parseFile } from './parse'; 5 + import Pointer from './pointer'; 6 + import $Ref from './ref'; 7 + import type $Refs from './refs'; 8 + import { fileResolver } from './resolvers/file'; 9 + import { urlResolver } from './resolvers/url'; 10 + import type { JSONSchema } from './types'; 11 + import { isHandledError } from './util/errors'; 12 + import * as url from './util/url'; 13 + 14 + /** 15 + * Crawls the JSON schema, finds all external JSON references, and resolves their values. 16 + * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}. 17 + * 18 + * NOTE: We only care about EXTERNAL references here. INTERNAL references are only relevant when dereferencing. 19 + * 20 + * @returns 21 + * The promise resolves once all JSON references in the schema have been resolved, 22 + * including nested references that are contained in externally-referenced files. 23 + */ 24 + export function resolveExternal(parser: $RefParser, options: $RefParserOptions) { 25 + try { 26 + // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path); 27 + const promises = crawl(parser.schema, parser.$refs._root$Ref.path + '#', parser.$refs, options); 28 + return Promise.all(promises); 29 + } catch (e) { 30 + return Promise.reject(e); 31 + } 32 + } 33 + 34 + /** 35 + * Recursively crawls the given value, and resolves any external JSON references. 36 + * 37 + * @param obj - The value to crawl. If it's not an object or array, it will be ignored. 38 + * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash 39 + * @param {boolean} external - Whether `obj` was found in an external document. 40 + * @param $refs 41 + * @param options 42 + * @param seen - Internal. 43 + * 44 + * @returns 45 + * Returns an array of promises. There will be one promise for each JSON reference in `obj`. 46 + * If `obj` does not contain any JSON references, then the array will be empty. 47 + * If any of the JSON references point to files that contain additional JSON references, 48 + * then the corresponding promise will internally reference an array of promises. 49 + */ 50 + function crawl<S extends object = JSONSchema>( 51 + obj: string | Buffer | S | undefined | null, 52 + path: string, 53 + $refs: $Refs<S>, 54 + options: $RefParserOptions, 55 + seen?: Set<any>, 56 + external?: boolean, 57 + ) { 58 + seen ||= new Set(); 59 + let promises: any = []; 60 + 61 + if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj) && !seen.has(obj)) { 62 + seen.add(obj); // Track previously seen objects to avoid infinite recursion 63 + if ($Ref.isExternal$Ref(obj)) { 64 + promises.push(resolve$Ref<S>(obj, path, $refs, options)); 65 + } 66 + 67 + const keys = Object.keys(obj) as string[]; 68 + for (const key of keys) { 69 + const keyPath = Pointer.join(path, key); 70 + const value = obj[key as keyof typeof obj] as string | JSONSchema | Buffer | undefined; 71 + promises = promises.concat(crawl(value, keyPath, $refs, options, seen, external)); 72 + } 73 + } 74 + 75 + return promises; 76 + } 77 + 78 + /** 79 + * Resolves the given JSON Reference, and then crawls the resulting value. 80 + * 81 + * @param $ref - The JSON Reference to resolve 82 + * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash 83 + * @param $refs 84 + * @param options 85 + * 86 + * @returns 87 + * The promise resolves once all JSON references in the object have been resolved, 88 + * including nested references that are contained in externally-referenced files. 89 + */ 90 + async function resolve$Ref<S extends object = JSONSchema>( 91 + $ref: S, 92 + path: string, 93 + $refs: $Refs<S>, 94 + options: $RefParserOptions, 95 + ) { 96 + const resolvedPath = url.resolve(path, ($ref as JSONSchema).$ref!); 97 + const withoutHash = url.stripHash(resolvedPath); 98 + 99 + // $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath); 100 + 101 + // If this ref points back to an input source we've already merged, avoid re-importing 102 + // by checking if the path (without hash) matches a known source in parser and we can serve it internally later. 103 + // We keep normal flow but ensure cache hit if already added. 104 + // Do we already have this $ref? 105 + const ref = $refs._$refs[withoutHash]; 106 + if (ref) { 107 + // We've already parsed this $ref, so crawl it to resolve its own externals 108 + const promises = crawl(ref.value as S, `${withoutHash}#`, $refs, options, new Set(), true); 109 + return Promise.all(promises); 110 + } 111 + 112 + // Parse the $referenced file/url 113 + const file = newFile(resolvedPath); 114 + 115 + // Add a new $Ref for this file, even though we don't have the value yet. 116 + // This ensures that we don't simultaneously read & parse the same file multiple times 117 + const $refAdded = $refs._add(file.url); 118 + 119 + try { 120 + const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: resolvedPath }); 121 + 122 + $refAdded.pathType = resolvedInput.type; 123 + 124 + let promises: any = []; 125 + 126 + if (resolvedInput.type !== 'json') { 127 + const resolver = resolvedInput.type === 'file' ? fileResolver : urlResolver; 128 + await resolver.handler({ file }); 129 + const parseResult = await parseFile(file, options); 130 + $refAdded.value = parseResult.result; 131 + promises = crawl(parseResult.result, `${withoutHash}#`, $refs, options, new Set(), true); 132 + } 133 + 134 + return Promise.all(promises); 135 + } catch (err) { 136 + if (isHandledError(err)) { 137 + $refAdded.value = err; 138 + } 139 + 140 + throw err; 141 + } 142 + }
+25
packages/json-schema-ref-parser/src/resolvers/file.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + import fs from 'fs'; 3 + 4 + import type { FileInfo } from '../types'; 5 + import { ResolverError } from '../util/errors'; 6 + import * as url from '../util/url'; 7 + 8 + export const fileResolver = { 9 + handler: async ({ file }: { file: FileInfo }): Promise<void> => { 10 + let path: string | undefined; 11 + 12 + try { 13 + path = url.toFileSystemPath(file.url); 14 + } catch (error: any) { 15 + throw new ResolverError(ono.uri(error, `Malformed URI: ${file.url}`), file.url); 16 + } 17 + 18 + try { 19 + const data = await fs.promises.readFile(path); 20 + file.data = data; 21 + } catch (error: any) { 22 + throw new ResolverError(ono(error, `Error opening file "${path}"`), path); 23 + } 24 + }, 25 + };
+99
packages/json-schema-ref-parser/src/resolvers/url.ts
··· 1 + import { ono } from '@jsdevtools/ono'; 2 + 3 + import type { FileInfo } from '../types'; 4 + import { ResolverError } from '../util/errors'; 5 + import { resolve } from '../util/url'; 6 + 7 + export const sendRequest = async ({ 8 + fetchOptions, 9 + redirects = [], 10 + timeout = 60_000, 11 + url, 12 + }: { 13 + fetchOptions?: RequestInit; 14 + redirects?: string[]; 15 + timeout?: number; 16 + url: URL | string; 17 + }): Promise<{ 18 + fetchOptions?: RequestInit; 19 + response: Response; 20 + }> => { 21 + url = new URL(url); 22 + redirects.push(url.href); 23 + 24 + const controller = new AbortController(); 25 + const timeoutId = setTimeout(() => { 26 + controller.abort(); 27 + }, timeout); 28 + const response = await fetch(url, { 29 + signal: controller.signal, 30 + ...fetchOptions, 31 + }); 32 + clearTimeout(timeoutId); 33 + 34 + if (response.status >= 300 && response.status <= 399) { 35 + if (redirects.length > 5) { 36 + throw new ResolverError( 37 + ono( 38 + { status: response.status }, 39 + `Error requesting ${redirects[0]}. \nToo many redirects: \n ${redirects.join(' \n ')}`, 40 + ), 41 + ); 42 + } 43 + 44 + if (!('location' in response.headers) || !response.headers.location) { 45 + throw ono( 46 + { status: response.status }, 47 + `HTTP ${response.status} redirect with no location header`, 48 + ); 49 + } 50 + 51 + return sendRequest({ 52 + fetchOptions, 53 + redirects, 54 + timeout, 55 + url: resolve(url.href, response.headers.location as string), 56 + }); 57 + } 58 + 59 + return { fetchOptions, response }; 60 + }; 61 + 62 + export const urlResolver = { 63 + handler: async ({ 64 + arrayBuffer, 65 + fetch: _fetch, 66 + file, 67 + }: { 68 + arrayBuffer?: ArrayBuffer; 69 + fetch?: RequestInit; 70 + file: FileInfo; 71 + }): Promise<void> => { 72 + let data = arrayBuffer; 73 + 74 + if (!data) { 75 + try { 76 + const { fetchOptions, response } = await sendRequest({ 77 + fetchOptions: { 78 + method: 'GET', 79 + ..._fetch, 80 + }, 81 + url: file.url, 82 + }); 83 + 84 + if (response.status >= 400) { 85 + // gracefully handle HEAD method not allowed 86 + if (response.status !== 405 || fetchOptions?.method !== 'HEAD') { 87 + throw ono({ status: response.status }, `HTTP ERROR ${response.status}`); 88 + } 89 + } 90 + 91 + data = response.body ? await response.arrayBuffer() : new ArrayBuffer(0); 92 + } catch (error: any) { 93 + throw new ResolverError(ono(error, `Error requesting ${file.url}`), file.url); 94 + } 95 + } 96 + 97 + file.data = Buffer.from(data!); 98 + }, 99 + };
+58
packages/json-schema-ref-parser/src/types/index.ts
··· 1 + import type { 2 + JSONSchema4, 3 + JSONSchema4Object, 4 + JSONSchema6, 5 + JSONSchema6Object, 6 + JSONSchema7, 7 + JSONSchema7Object, 8 + } from 'json-schema'; 9 + 10 + export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; 11 + export type JSONSchemaObject = JSONSchema4Object | JSONSchema6Object | JSONSchema7Object; 12 + 13 + export interface Plugin { 14 + /** 15 + * Can this parser be used to process this file? 16 + */ 17 + canHandle: (file: FileInfo) => boolean; 18 + /** 19 + * This is where the real work of a parser happens. The `parse` method accepts the same file info object as the `canHandle` function, but rather than returning a boolean value, the `parse` method should return a JavaScript representation of the file contents. For our CSV parser, that is a two-dimensional array of lines and values. For your parser, it might be an object, a string, a custom class, or anything else. 20 + * 21 + * Unlike the `canHandle` function, the `parse` method can also be asynchronous. This might be important if your parser needs to retrieve data from a database or if it relies on an external HTTP service to return the parsed value. You can return your asynchronous value via a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) or a Node.js-style error-first callback. Here are examples of both approaches: 22 + */ 23 + handler: ( 24 + file: FileInfo, 25 + ) => 26 + | string 27 + | Buffer 28 + | JSONSchema 29 + | Promise<{ data: Buffer }> 30 + | Promise<string | Buffer | JSONSchema>; 31 + name: 'binary' | 'file' | 'http' | 'json' | 'text' | 'yaml'; 32 + } 33 + 34 + /** 35 + * JSON Schema `$Ref` Parser supports plug-ins, such as resolvers and parsers. These plug-ins can have methods such as `canHandle()`, `read()`, `canHandle()`, and `parse()`. All of these methods accept the same object as their parameter: an object containing information about the file being read or parsed. 36 + * 37 + * The file info object currently only consists of a few properties, but it may grow in the future if plug-ins end up needing more information. 38 + * 39 + * See https://apitools.dev/json-schema-ref-parser/docs/plugins/file-info-object.html 40 + */ 41 + export interface FileInfo { 42 + /** 43 + * The raw file contents, in whatever form they were returned by the resolver that read the file. 44 + */ 45 + data: string | Buffer; 46 + /** 47 + * The lowercase file extension, such as ".json", ".yaml", ".txt", etc. 48 + */ 49 + extension: string; 50 + /** 51 + * The hash (URL fragment) of the file URL, including the # symbol. If the URL doesn't have a hash, then this will be an empty string. 52 + */ 53 + hash: string; 54 + /** 55 + * The full URL of the file. This could be any type of URL, including "http://", "https://", "file://", "ftp://", "mongodb://", or even a local filesystem path (when running in Node.js). 56 + */ 57 + url: string; 58 + }
+8
packages/json-schema-ref-parser/src/util/convert-path-to-posix.ts
··· 1 + export default function convertPathToPosix(filePath: string): string { 2 + // Extended-length paths on Windows should not be converted 3 + if (filePath.startsWith('\\\\?\\')) { 4 + return filePath; 5 + } 6 + 7 + return filePath.replaceAll('\\', '/'); 8 + }
+155
packages/json-schema-ref-parser/src/util/errors.ts
··· 1 + import { Ono } from '@jsdevtools/ono'; 2 + 3 + import type { $RefParser } from '..'; 4 + import type $Ref from '../ref'; 5 + import type { JSONSchema } from '../types'; 6 + import { getHash, stripHash, toFileSystemPath } from './url'; 7 + 8 + export type JSONParserErrorType = 9 + | 'EUNKNOWN' 10 + | 'EPARSER' 11 + | 'EUNMATCHEDPARSER' 12 + | 'ETIMEOUT' 13 + | 'ERESOLVER' 14 + | 'EUNMATCHEDRESOLVER' 15 + | 'EMISSINGPOINTER' 16 + | 'EINVALIDPOINTER'; 17 + 18 + export class JSONParserError extends Error { 19 + public readonly name: string; 20 + public readonly message: string; 21 + public source: string | undefined; 22 + public path: Array<string | number> | null; 23 + public readonly code: JSONParserErrorType; 24 + public constructor(message: string, source?: string) { 25 + super(); 26 + 27 + this.code = 'EUNKNOWN'; 28 + this.name = 'JSONParserError'; 29 + this.message = message; 30 + this.source = source; 31 + this.path = null; 32 + 33 + Ono.extend(this); 34 + } 35 + 36 + get footprint() { 37 + return `${this.path}+${this.source}+${this.code}+${this.message}`; 38 + } 39 + } 40 + 41 + export class JSONParserErrorGroup<S extends object = JSONSchema> extends Error { 42 + files: $RefParser; 43 + 44 + constructor(parser: $RefParser) { 45 + super(); 46 + 47 + this.files = parser; 48 + this.name = 'JSONParserErrorGroup'; 49 + this.message = `${this.errors.length} error${ 50 + this.errors.length > 1 ? 's' : '' 51 + } occurred while reading '${toFileSystemPath(parser.$refs._root$Ref!.path)}'`; 52 + 53 + Ono.extend(this); 54 + } 55 + 56 + static getParserErrors<S extends object = JSONSchema>(parser: $RefParser) { 57 + const errors = []; 58 + 59 + for (const $ref of Object.values(parser.$refs._$refs) as $Ref<S>[]) { 60 + if ($ref.errors) { 61 + errors.push(...$ref.errors); 62 + } 63 + } 64 + 65 + return errors; 66 + } 67 + 68 + get errors(): Array< 69 + | JSONParserError 70 + | InvalidPointerError 71 + | ResolverError 72 + | ParserError 73 + | MissingPointerError 74 + | UnmatchedParserError 75 + | UnmatchedResolverError 76 + > { 77 + return JSONParserErrorGroup.getParserErrors<S>(this.files); 78 + } 79 + } 80 + 81 + export class ParserError extends JSONParserError { 82 + code = 'EPARSER' as JSONParserErrorType; 83 + name = 'ParserError'; 84 + constructor(message: any, source: any) { 85 + super(`Error parsing ${source}: ${message}`, source); 86 + } 87 + } 88 + 89 + export class UnmatchedParserError extends JSONParserError { 90 + code = 'EUNMATCHEDPARSER' as JSONParserErrorType; 91 + name = 'UnmatchedParserError'; 92 + 93 + constructor(source: string) { 94 + super(`Could not find parser for "${source}"`, source); 95 + } 96 + } 97 + 98 + export class ResolverError extends JSONParserError { 99 + code = 'ERESOLVER' as JSONParserErrorType; 100 + name = 'ResolverError'; 101 + ioErrorCode?: string; 102 + constructor(ex: Error | any, source?: string) { 103 + super(ex.message || `Error reading file "${source}"`, source); 104 + if ('code' in ex) { 105 + this.ioErrorCode = String(ex.code); 106 + } 107 + } 108 + } 109 + 110 + export class UnmatchedResolverError extends JSONParserError { 111 + code = 'EUNMATCHEDRESOLVER' as JSONParserErrorType; 112 + name = 'UnmatchedResolverError'; 113 + constructor(source: any) { 114 + super(`Could not find resolver for "${source}"`, source); 115 + } 116 + } 117 + 118 + export class MissingPointerError extends JSONParserError { 119 + code = 'EMISSINGPOINTER' as JSONParserErrorType; 120 + name = 'MissingPointerError'; 121 + constructor(token: string, path: string) { 122 + super( 123 + `Missing $ref pointer "${getHash(path)}". Token "${token}" does not exist.`, 124 + stripHash(path), 125 + ); 126 + } 127 + } 128 + 129 + export class TimeoutError extends JSONParserError { 130 + code = 'ETIMEOUT' as JSONParserErrorType; 131 + name = 'TimeoutError'; 132 + constructor(timeout: number) { 133 + super(`Dereferencing timeout reached: ${timeout}ms`); 134 + } 135 + } 136 + 137 + export class InvalidPointerError extends JSONParserError { 138 + code = 'EUNMATCHEDRESOLVER' as JSONParserErrorType; 139 + name = 'InvalidPointerError'; 140 + constructor(pointer: string, path: string) { 141 + super(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`, stripHash(path)); 142 + } 143 + } 144 + 145 + export function isHandledError(err: any): err is JSONParserError { 146 + return err instanceof JSONParserError || err instanceof JSONParserErrorGroup; 147 + } 148 + 149 + export function normalizeError(err: any) { 150 + if (err.path === null) { 151 + err.path = []; 152 + } 153 + 154 + return err; 155 + }
+2
packages/json-schema-ref-parser/src/util/is-windows.ts
··· 1 + const isWindowsConst = /^win/.test(globalThis.process ? globalThis.process.platform : ''); 2 + export const isWindows = () => isWindowsConst;
+55
packages/json-schema-ref-parser/src/util/plugins.ts
··· 1 + import type { FileInfo, JSONSchema, Plugin } from '../types'; 2 + 3 + export interface PluginResult { 4 + error?: any; 5 + plugin: Pick<Plugin, 'handler'>; 6 + result?: string | Buffer | JSONSchema; 7 + } 8 + 9 + /** 10 + * Runs the specified method of the given plugins, in order, until one of them returns a successful result. 11 + * Each method can return a synchronous value, a Promise, or call an error-first callback. 12 + * If the promise resolves successfully, or the callback is called without an error, then the result 13 + * is immediately returned and no further plugins are called. 14 + * If the promise rejects, or the callback is called with an error, then the next plugin is called. 15 + * If ALL plugins fail, then the last error is thrown. 16 + */ 17 + export async function run(plugins: Pick<Plugin, 'handler'>[], file: FileInfo) { 18 + let index = 0; 19 + let lastError: PluginResult; 20 + let plugin: Pick<Plugin, 'handler'>; 21 + 22 + return new Promise<PluginResult>((resolve, reject) => { 23 + const runNextPlugin = async () => { 24 + plugin = plugins[index++]!; 25 + 26 + if (!plugin) { 27 + // there are no more functions, re-throw the last error 28 + return reject(lastError); 29 + } 30 + 31 + try { 32 + const result = await plugin.handler(file); 33 + 34 + if (result !== undefined) { 35 + return resolve({ 36 + plugin, 37 + result, 38 + }); 39 + } 40 + 41 + if (index === plugins.length) { 42 + throw new Error('No promise has been returned.'); 43 + } 44 + } catch (e) { 45 + lastError = { 46 + error: e, 47 + plugin, 48 + }; 49 + runNextPlugin(); 50 + } 51 + }; 52 + 53 + runNextPlugin(); 54 + }); 55 + }
+265
packages/json-schema-ref-parser/src/util/url.ts
··· 1 + import path, { join, win32 } from 'node:path'; 2 + 3 + import convertPathToPosix from './convert-path-to-posix'; 4 + import { isWindows } from './is-windows'; 5 + 6 + const forwardSlashPattern = /\//g; 7 + const protocolPattern = /^(\w{2,}):\/\//i; 8 + 9 + // RegExp patterns to URL-encode special characters in local filesystem paths 10 + const urlEncodePatterns = [ 11 + [/\?/g, '%3F'], 12 + [/#/g, '%23'], 13 + ] as [RegExp, string][]; 14 + 15 + // RegExp patterns to URL-decode special characters for local filesystem paths 16 + const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@']; 17 + 18 + /** 19 + * Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF. 20 + * 21 + * @returns 22 + */ 23 + export function resolve(from: string, to: string) { 24 + const fromUrl = new URL(convertPathToPosix(from), 'resolve://'); 25 + const resolvedUrl = new URL(convertPathToPosix(to), fromUrl); 26 + const endSpaces = to.match(/(\s*)$/)?.[1] || ''; 27 + if (resolvedUrl.protocol === 'resolve:') { 28 + // `from` is a relative URL. 29 + const { hash, pathname, search } = resolvedUrl; 30 + return pathname + search + hash + endSpaces; 31 + } 32 + return resolvedUrl.toString() + endSpaces; 33 + } 34 + 35 + /** 36 + * Returns the current working directory (in Node) or the current page URL (in browsers). 37 + * 38 + * @returns 39 + */ 40 + export function cwd() { 41 + if (typeof window !== 'undefined') { 42 + return location.href; 43 + } 44 + 45 + const path = process.cwd(); 46 + 47 + const lastChar = path.slice(-1); 48 + if (lastChar === '/' || lastChar === '\\') { 49 + return path; 50 + } else { 51 + return path + '/'; 52 + } 53 + } 54 + 55 + /** 56 + * Returns the protocol of the given URL, or `undefined` if it has no protocol. 57 + * 58 + * @param path 59 + * @returns 60 + */ 61 + export function getProtocol(path: string | undefined) { 62 + const match = protocolPattern.exec(path || ''); 63 + if (match) { 64 + return match[1]!.toLowerCase(); 65 + } 66 + return undefined; 67 + } 68 + 69 + /** 70 + * Returns the lowercased file extension of the given URL, 71 + * or an empty string if it has no extension. 72 + * 73 + * @param path 74 + * @returns 75 + */ 76 + export function getExtension(path: any) { 77 + const lastDot = path.lastIndexOf('.'); 78 + if (lastDot > -1) { 79 + return stripQuery(path.substr(lastDot).toLowerCase()); 80 + } 81 + return ''; 82 + } 83 + 84 + /** 85 + * Removes the query, if any, from the given path. 86 + * 87 + * @param path 88 + * @returns 89 + */ 90 + export function stripQuery(path: any) { 91 + const queryIndex = path.indexOf('?'); 92 + if (queryIndex > -1) { 93 + path = path.substr(0, queryIndex); 94 + } 95 + return path; 96 + } 97 + 98 + /** 99 + * Returns the hash (URL fragment), of the given path. 100 + * If there is no hash, then the root hash ("#") is returned. 101 + * 102 + * @param path 103 + * @returns 104 + */ 105 + export function getHash(path: undefined | string) { 106 + if (!path) { 107 + return '#'; 108 + } 109 + const hashIndex = path.indexOf('#'); 110 + if (hashIndex > -1) { 111 + return path.substring(hashIndex); 112 + } 113 + return '#'; 114 + } 115 + 116 + /** 117 + * Removes the hash (URL fragment), if any, from the given path. 118 + * 119 + * @param path 120 + * @returns 121 + */ 122 + export function stripHash(path?: string | undefined) { 123 + if (!path) { 124 + return ''; 125 + } 126 + const hashIndex = path.indexOf('#'); 127 + if (hashIndex > -1) { 128 + path = path.substring(0, hashIndex); 129 + } 130 + return path; 131 + } 132 + 133 + /** 134 + * Determines whether the given path is a filesystem path. 135 + * This includes "file://" URLs. 136 + * 137 + * @param path 138 + * @returns 139 + */ 140 + export function isFileSystemPath(path: string | undefined) { 141 + // @ts-ignore 142 + if (typeof window !== 'undefined' || (typeof process !== 'undefined' && process.browser)) { 143 + // We're running in a browser, so assume that all paths are URLs. 144 + // This way, even relative paths will be treated as URLs rather than as filesystem paths 145 + return false; 146 + } 147 + 148 + const protocol = getProtocol(path); 149 + return protocol === undefined || protocol === 'file'; 150 + } 151 + 152 + /** 153 + * Converts a filesystem path to a properly-encoded URL. 154 + * 155 + * This is intended to handle situations where JSON Schema $Ref Parser is called 156 + * with a filesystem path that contains characters which are not allowed in URLs. 157 + * 158 + * @example 159 + * The following filesystem paths would be converted to the following URLs: 160 + * 161 + * <"!@#$%^&*+=?'>.json ==> %3C%22!@%23$%25%5E&*+=%3F\'%3E.json 162 + * C:\\My Documents\\File (1).json ==> C:/My%20Documents/File%20(1).json 163 + * file://Project #42/file.json ==> file://Project%20%2342/file.json 164 + * 165 + * @param path 166 + * @returns 167 + */ 168 + export function fromFileSystemPath(path: string) { 169 + // Step 1: On Windows, replace backslashes with forward slashes, 170 + // rather than encoding them as "%5C" 171 + if (isWindows()) { 172 + const projectDir = cwd(); 173 + const upperPath = path.toUpperCase(); 174 + const projectDirPosixPath = convertPathToPosix(projectDir); 175 + const posixUpper = projectDirPosixPath.toUpperCase(); 176 + const hasProjectDir = upperPath.includes(posixUpper); 177 + const hasProjectUri = upperPath.includes(posixUpper); 178 + const isAbsolutePath = 179 + win32.isAbsolute(path) || 180 + path.startsWith('http://') || 181 + path.startsWith('https://') || 182 + path.startsWith('file://'); 183 + 184 + if (!(hasProjectDir || hasProjectUri || isAbsolutePath) && !projectDir.startsWith('http')) { 185 + path = join(projectDir, path); 186 + } 187 + path = convertPathToPosix(path); 188 + } 189 + 190 + // Step 2: `encodeURI` will take care of MOST characters 191 + path = encodeURI(path); 192 + 193 + // Step 3: Manually encode characters that are not encoded by `encodeURI`. 194 + // This includes characters such as "#" and "?", which have special meaning in URLs, 195 + // but are just normal characters in a filesystem path. 196 + for (const pattern of urlEncodePatterns) { 197 + path = path.replace(pattern[0], pattern[1]); 198 + } 199 + 200 + return path; 201 + } 202 + 203 + /** 204 + * Converts a URL to a local filesystem path. 205 + */ 206 + export function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string { 207 + // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc. 208 + path = decodeURI(path!); 209 + 210 + // Step 2: Manually decode characters that are not decoded by `decodeURI`. 211 + // This includes characters such as "#" and "?", which have special meaning in URLs, 212 + // but are just normal characters in a filesystem path. 213 + for (let i = 0; i < urlDecodePatterns.length; i += 2) { 214 + path = path.replace(urlDecodePatterns[i]!, urlDecodePatterns[i + 1] as string); 215 + } 216 + 217 + // Step 3: If it's a "file://" URL, then format it consistently 218 + // or convert it to a local filesystem path 219 + let isFileUrl = path.substr(0, 7).toLowerCase() === 'file://'; 220 + if (isFileUrl) { 221 + // Strip-off the protocol, and the initial "/", if there is one 222 + path = path[7] === '/' ? path.substr(8) : path.substr(7); 223 + 224 + // insert a colon (":") after the drive letter on Windows 225 + if (isWindows() && path[1] === '/') { 226 + path = path[0] + ':' + path.substr(1); 227 + } 228 + 229 + if (keepFileProtocol) { 230 + // Return the consistently-formatted "file://" URL 231 + path = 'file:///' + path; 232 + } else { 233 + // Convert the "file://" URL to a local filesystem path. 234 + // On Windows, it will start with something like "C:/". 235 + // On Posix, it will start with "/" 236 + isFileUrl = false; 237 + path = isWindows() ? path : '/' + path; 238 + } 239 + } 240 + 241 + // Step 4: Normalize Windows paths (unless it's a "file://" URL) 242 + if (isWindows() && !isFileUrl) { 243 + // Replace forward slashes with backslashes 244 + path = path.replace(forwardSlashPattern, '\\'); 245 + 246 + // Capitalize the drive letter 247 + if (path.substr(1, 2) === ':\\') { 248 + path = path[0]!.toUpperCase() + path.substr(1); 249 + } 250 + } 251 + 252 + return path; 253 + } 254 + 255 + export function relative(from: string, to: string) { 256 + if (!isFileSystemPath(from) || !isFileSystemPath(to)) { 257 + return resolve(from, to); 258 + } 259 + 260 + const fromDir = path.dirname(stripHash(from)); 261 + const toPath = stripHash(to); 262 + 263 + const result = path.relative(fromDir, toPath); 264 + return result + getHash(to); 265 + }
+10
packages/json-schema-ref-parser/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "composite": true, 5 + "outDir": "dist", 6 + "rootDir": "src", 7 + "types": ["vitest/globals"] 8 + }, 9 + "include": ["src"] 10 + }
+11
packages/json-schema-ref-parser/tsdown.config.ts
··· 1 + import { defineConfig } from 'tsdown'; 2 + 3 + export default defineConfig({ 4 + clean: true, 5 + dts: true, 6 + entry: ['src/index.ts'], 7 + format: ['esm'], 8 + minify: false, 9 + sourcemap: true, 10 + treeshake: true, 11 + });
+1 -1
packages/openapi-python/package.json
··· 61 61 }, 62 62 "dependencies": { 63 63 "@hey-api/codegen-core": "workspace:*", 64 - "@hey-api/json-schema-ref-parser": "1.2.4", 64 + "@hey-api/json-schema-ref-parser": "workspace:*", 65 65 "@hey-api/shared": "workspace:*", 66 66 "@hey-api/types": "workspace:*", 67 67 "ansi-colors": "4.1.3",
+6 -1
packages/openapi-python/tsconfig.json
··· 7 7 "types": ["vitest/globals", "node"] 8 8 }, 9 9 "include": ["src"], 10 - "references": [{ "path": "../types" }, { "path": "../codegen-core" }, { "path": "../shared" }] 10 + "references": [ 11 + { "path": "../types" }, 12 + { "path": "../codegen-core" }, 13 + { "path": "../shared" }, 14 + { "path": "../json-schema-ref-parser" } 15 + ] 11 16 }
+1 -1
packages/openapi-ts/package.json
··· 70 70 }, 71 71 "dependencies": { 72 72 "@hey-api/codegen-core": "workspace:*", 73 - "@hey-api/json-schema-ref-parser": "1.2.4", 73 + "@hey-api/json-schema-ref-parser": "workspace:*", 74 74 "@hey-api/shared": "workspace:*", 75 75 "@hey-api/types": "workspace:*", 76 76 "ansi-colors": "4.1.3",
+6 -1
packages/openapi-ts/tsconfig.json
··· 7 7 "types": ["vitest/globals", "node"] 8 8 }, 9 9 "include": ["src"], 10 - "references": [{ "path": "../types" }, { "path": "../codegen-core" }, { "path": "../shared" }] 10 + "references": [ 11 + { "path": "../types" }, 12 + { "path": "../codegen-core" }, 13 + { "path": "../shared" }, 14 + { "path": "../json-schema-ref-parser" } 15 + ] 11 16 }
+1 -1
packages/shared/package.json
··· 41 41 }, 42 42 "dependencies": { 43 43 "@hey-api/codegen-core": "workspace:*", 44 - "@hey-api/json-schema-ref-parser": "1.2.4", 44 + "@hey-api/json-schema-ref-parser": "workspace:*", 45 45 "@hey-api/types": "workspace:*", 46 46 "ansi-colors": "4.1.3", 47 47 "cross-spawn": "7.0.6",
+5 -1
packages/shared/tsconfig.json
··· 7 7 "types": ["bun", "vitest/globals"] 8 8 }, 9 9 "include": ["src"], 10 - "references": [{ "path": "../types" }, { "path": "../codegen-core" }] 10 + "references": [ 11 + { "path": "../types" }, 12 + { "path": "../codegen-core" }, 13 + { "path": "../json-schema-ref-parser" } 14 + ] 11 15 }
+31 -23
pnpm-lock.yaml
··· 1228 1228 specifier: 6.1.1 1229 1229 version: 6.1.1(rollup@4.56.0)(typescript@5.9.3) 1230 1230 1231 + packages/json-schema-ref-parser: 1232 + dependencies: 1233 + '@jsdevtools/ono': 1234 + specifier: 7.1.3 1235 + version: 7.1.3 1236 + '@types/json-schema': 1237 + specifier: 7.0.15 1238 + version: 7.0.15 1239 + js-yaml: 1240 + specifier: 4.1.1 1241 + version: 4.1.1 1242 + devDependencies: 1243 + '@types/js-yaml': 1244 + specifier: 4.0.9 1245 + version: 4.0.9 1246 + typescript: 1247 + specifier: 5.9.3 1248 + version: 5.9.3 1249 + 1231 1250 packages/nuxt: 1232 1251 dependencies: 1233 1252 '@nuxt/kit': ··· 1268 1287 specifier: workspace:* 1269 1288 version: link:../codegen-core 1270 1289 '@hey-api/json-schema-ref-parser': 1271 - specifier: 1.2.4 1272 - version: 1.2.4 1290 + specifier: workspace:* 1291 + version: link:../json-schema-ref-parser 1273 1292 '@hey-api/shared': 1274 1293 specifier: workspace:* 1275 1294 version: link:../shared ··· 1302 1321 specifier: workspace:* 1303 1322 version: link:../codegen-core 1304 1323 '@hey-api/json-schema-ref-parser': 1305 - specifier: 1.2.4 1306 - version: 1.2.4 1324 + specifier: workspace:* 1325 + version: link:../json-schema-ref-parser 1307 1326 '@hey-api/shared': 1308 1327 specifier: workspace:* 1309 1328 version: link:../shared ··· 1528 1547 specifier: workspace:* 1529 1548 version: link:../codegen-core 1530 1549 '@hey-api/json-schema-ref-parser': 1531 - specifier: 1.2.4 1532 - version: 1.2.4 1550 + specifier: workspace:* 1551 + version: link:../json-schema-ref-parser 1533 1552 '@hey-api/types': 1534 1553 specifier: workspace:* 1535 1554 version: link:../types ··· 3792 3811 '@fontsource/fira-mono@5.0.0': 3793 3812 resolution: {integrity: sha512-IsinH/oLYJyv/sQv7SbKmjoAXZsSjm6Q1Tz5GBBXCXi3Jg9MzXmKvWm9bSLC8lFI6CDsi8GkH/DAgZ98t8bhTQ==} 3794 3813 3795 - '@hey-api/json-schema-ref-parser@1.2.4': 3796 - resolution: {integrity: sha512-uuOaZ6tStUgRJFUqnX3Xdbs792++ezxOLI5NMxuikVklpbFWk2wcvIZbeX+qTWDv6kiS1Ik2EVKQgeQFWHML4A==} 3797 - engines: {node: '>= 16'} 3798 - 3799 3814 '@hono/node-server@1.19.9': 3800 3815 resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} 3801 3816 engines: {node: '>=18.14.1'} ··· 6969 6984 6970 6985 '@types/jasmine@5.1.9': 6971 6986 resolution: {integrity: sha512-8t4HtkW4wxiPVedMpeZ63n3vlWxEIquo/zc1Tm8ElU+SqVV7+D3Na2PWaJUp179AzTragMWVwkMv7mvty0NfyQ==} 6987 + 6988 + '@types/js-yaml@4.0.9': 6989 + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} 6972 6990 6973 6991 '@types/jsdom@27.0.0': 6974 6992 resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} ··· 11032 11050 11033 11051 lodash.uniq@4.5.0: 11034 11052 resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} 11035 - 11036 - lodash@4.17.21: 11037 - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 11038 11053 11039 11054 lodash@4.17.23: 11040 11055 resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} ··· 17742 17757 17743 17758 '@fontsource/fira-mono@5.0.0': {} 17744 17759 17745 - '@hey-api/json-schema-ref-parser@1.2.4': 17746 - dependencies: 17747 - '@jsdevtools/ono': 7.1.3 17748 - '@types/json-schema': 7.0.15 17749 - js-yaml: 4.1.1 17750 - lodash: 4.17.23 17751 - 17752 17760 '@hono/node-server@1.19.9(hono@4.11.8)': 17753 17761 dependencies: 17754 17762 hono: 4.11.8 ··· 21356 21364 21357 21365 '@types/jasmine@5.1.9': {} 21358 21366 21367 + '@types/js-yaml@4.0.9': {} 21368 + 21359 21369 '@types/jsdom@27.0.0': 21360 21370 dependencies: 21361 21371 '@types/node': 24.10.10 ··· 26492 26502 lodash.startcase@4.4.0: {} 26493 26503 26494 26504 lodash.uniq@4.5.0: {} 26495 - 26496 - lodash@4.17.21: {} 26497 26505 26498 26506 lodash@4.17.23: {} 26499 26507 ··· 31935 31943 eslint-visitor-keys: 3.4.3 31936 31944 espree: 9.6.1 31937 31945 esquery: 1.6.0 31938 - lodash: 4.17.21 31946 + lodash: 4.17.23 31939 31947 semver: 7.7.3 31940 31948 transitivePeerDependencies: 31941 31949 - supports-color
+11
specs/json-schema-ref-parser/circular-ref-with-description.json
··· 1 + { 2 + "schemas": { 3 + "Foo": { 4 + "$ref": "#/schemas/Bar" 5 + }, 6 + "Bar": { 7 + "description": "ok", 8 + "$ref": "#/schemas/Foo" 9 + } 10 + } 11 + }
+34
specs/json-schema-ref-parser/multiple-refs.json
··· 1 + { 2 + "paths": { 3 + "/test1/{pathId}": { 4 + "get": { 5 + "summary": "First endpoint using the same pathId schema", 6 + "parameters": [ 7 + { 8 + "$ref": "path-parameter.json#/components/parameters/pathId" 9 + } 10 + ], 11 + "responses": { 12 + "200": { 13 + "description": "Test 1 response" 14 + } 15 + } 16 + } 17 + }, 18 + "/test2/{pathId}": { 19 + "get": { 20 + "summary": "Second endpoint using the same pathId schema", 21 + "parameters": [ 22 + { 23 + "$ref": "path-parameter.json#/components/parameters/pathId" 24 + } 25 + ], 26 + "responses": { 27 + "200": { 28 + "description": "Test 2 response" 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+46
specs/json-schema-ref-parser/openapi-paths-ref.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Sample API", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/foo": { 9 + "get": { 10 + "summary": "Get foo", 11 + "responses": { 12 + "200": { 13 + "description": "OK", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "type": "object", 18 + "properties": { 19 + "bar": { 20 + "type": "string" 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }, 29 + "post": { 30 + "summary": "Create foo", 31 + "responses": { 32 + "200": { 33 + "description": "OK", 34 + "content": { 35 + "application/json": { 36 + "schema": { 37 + "$ref": "#/paths/~1foo/get/responses/200/content/application~1json/schema" 38 + } 39 + } 40 + } 41 + } 42 + } 43 + } 44 + } 45 + } 46 + }
+16
specs/json-schema-ref-parser/path-parameter.json
··· 1 + { 2 + "components": { 3 + "parameters": { 4 + "pathId": { 5 + "name": "pathId", 6 + "in": "path", 7 + "required": true, 8 + "schema": { 9 + "type": "string", 10 + "format": "uuid", 11 + "description": "Unique identifier for the path" 12 + } 13 + } 14 + } 15 + } 16 + }
+1
tsconfig.json
··· 2 2 "files": [], 3 3 "references": [ 4 4 { "path": "./packages/codegen-core" }, 5 + { "path": "./packages/json-schema-ref-parser" }, 5 6 { "path": "./packages/openapi-python" }, 6 7 { "path": "./packages/openapi-ts" }, 7 8 { "path": "./packages/shared" },
+7
vitest.config.ts
··· 45 45 { 46 46 extends: true, 47 47 test: { 48 + name: '@hey-api/json-schema-ref-parser', 49 + root: 'packages/json-schema-ref-parser', 50 + }, 51 + }, 52 + { 53 + extends: true, 54 + test: { 48 55 name: '@test/openapi-ts', 49 56 root: 'packages/openapi-ts-tests/main', 50 57 setupFiles: ['./vitest.setup.ts'],