UNPKG

@hey-api/json-schema-ref-parser

Version:

Parse, Resolve, and Dereference JSON Schema $ref pointers

1 lines 136 kB
{"version":3,"file":"index.mjs","names":["Pointer","errors: MissingPointerError[]","url.resolve","url.getHash","$Ref","Pointer","url.resolve","hash","url.getHash","url.stripHash","$Ref","newEntry: InventoryEntry","crawl","key","parserAny: any","inventory: Array<InventoryEntry>","binaryParser: Plugin","jsonParser: Plugin","error: any","error","textParser: Plugin","yamlParser: Plugin","error: any","lastError: PluginResult","plugin: Pick<Plugin, 'handler'>","resolve","plugins.run","error: any","url.resolve","url.stripHash","$Ref","url.toFileSystemPath","path: string | undefined","url.toFileSystemPath","error: any","error: any","promises: Array<Promise<unknown>>","$Ref","Pointer","url.resolve","url.stripHash","promises: ReadonlyArray<Promise<unknown>>","resolvedInput: ResolvedInput","url.isFileSystemPath","url.fromFileSystemPath","url.resolve","url.cwd","merged: any","chosenOpenapi: string | undefined","chosenSwagger: string | undefined","infoAccumulator: any","servers: any[]","tags: any[]","out: any","url.getProtocol","schema: any","url.stripHash","srcTags: any[]"],"sources":["../src/util/convert-path-to-posix.ts","../src/util/is-windows.ts","../src/util/url.ts","../src/util/errors.ts","../src/ref.ts","../src/pointer.ts","../src/bundle.ts","../src/parsers/binary.ts","../src/parsers/json.ts","../src/parsers/text.ts","../src/parsers/yaml.ts","../src/options.ts","../src/util/plugins.ts","../src/parse.ts","../src/refs.ts","../src/resolvers/file.ts","../src/resolvers/url.ts","../src/resolve-external.ts","../src/index.ts"],"sourcesContent":["export default function convertPathToPosix(filePath: string): string {\n // Extended-length paths on Windows should not be converted\n if (filePath.startsWith('\\\\\\\\?\\\\')) {\n return filePath;\n }\n\n return filePath.replaceAll('\\\\', '/');\n}\n","const isWindowsConst = /^win/.test(globalThis.process ? globalThis.process.platform : '');\nexport const isWindows = () => isWindowsConst;\n","import path, { join, win32 } from 'node:path';\n\nimport convertPathToPosix from './convert-path-to-posix';\nimport { isWindows } from './is-windows';\n\nconst forwardSlashPattern = /\\//g;\nconst protocolPattern = /^(\\w{2,}):\\/\\//i;\n\n// RegExp patterns to URL-encode special characters in local filesystem paths\nconst urlEncodePatterns = [\n [/\\?/g, '%3F'],\n [/#/g, '%23'],\n] as [RegExp, string][];\n\n// RegExp patterns to URL-decode special characters for local filesystem paths\nconst urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'];\n\n/**\n * Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.\n *\n * @returns\n */\nexport function resolve(from: string, to: string) {\n const fromUrl = new URL(convertPathToPosix(from), 'resolve://');\n const resolvedUrl = new URL(convertPathToPosix(to), fromUrl);\n const endSpaces = to.match(/(\\s*)$/)?.[1] || '';\n if (resolvedUrl.protocol === 'resolve:') {\n // `from` is a relative URL.\n const { hash, pathname, search } = resolvedUrl;\n return pathname + search + hash + endSpaces;\n }\n return resolvedUrl.toString() + endSpaces;\n}\n\n/**\n * Returns the current working directory (in Node) or the current page URL (in browsers).\n *\n * @returns\n */\nexport function cwd() {\n if (typeof window !== 'undefined') {\n return location.href;\n }\n\n const path = process.cwd();\n\n const lastChar = path.slice(-1);\n if (lastChar === '/' || lastChar === '\\\\') {\n return path;\n } else {\n return path + '/';\n }\n}\n\n/**\n * Returns the protocol of the given URL, or `undefined` if it has no protocol.\n *\n * @param path\n * @returns\n */\nexport function getProtocol(path: string | undefined) {\n const match = protocolPattern.exec(path || '');\n if (match) {\n return match[1]!.toLowerCase();\n }\n return undefined;\n}\n\n/**\n * Returns the lowercased file extension of the given URL,\n * or an empty string if it has no extension.\n *\n * @param path\n * @returns\n */\nexport function getExtension(path: any) {\n const lastDot = path.lastIndexOf('.');\n if (lastDot > -1) {\n return stripQuery(path.substr(lastDot).toLowerCase());\n }\n return '';\n}\n\n/**\n * Removes the query, if any, from the given path.\n *\n * @param path\n * @returns\n */\nexport function stripQuery(path: any) {\n const queryIndex = path.indexOf('?');\n if (queryIndex > -1) {\n path = path.substr(0, queryIndex);\n }\n return path;\n}\n\n/**\n * Returns the hash (URL fragment), of the given path.\n * If there is no hash, then the root hash (\"#\") is returned.\n *\n * @param path\n * @returns\n */\nexport function getHash(path: undefined | string) {\n if (!path) {\n return '#';\n }\n const hashIndex = path.indexOf('#');\n if (hashIndex > -1) {\n return path.substring(hashIndex);\n }\n return '#';\n}\n\n/**\n * Removes the hash (URL fragment), if any, from the given path.\n *\n * @param path\n * @returns\n */\nexport function stripHash(path?: string | undefined) {\n if (!path) {\n return '';\n }\n const hashIndex = path.indexOf('#');\n if (hashIndex > -1) {\n path = path.substring(0, hashIndex);\n }\n return path;\n}\n\n/**\n * Determines whether the given path is a filesystem path.\n * This includes \"file://\" URLs.\n *\n * @param path\n * @returns\n */\nexport function isFileSystemPath(path: string | undefined) {\n // @ts-ignore\n if (typeof window !== 'undefined' || (typeof process !== 'undefined' && process.browser)) {\n // We're running in a browser, so assume that all paths are URLs.\n // This way, even relative paths will be treated as URLs rather than as filesystem paths\n return false;\n }\n\n const protocol = getProtocol(path);\n return protocol === undefined || protocol === 'file';\n}\n\n/**\n * Converts a filesystem path to a properly-encoded URL.\n *\n * This is intended to handle situations where JSON Schema $Ref Parser is called\n * with a filesystem path that contains characters which are not allowed in URLs.\n *\n * @example\n * The following filesystem paths would be converted to the following URLs:\n *\n * <\"!@#$%^&*+=?'>.json ==> %3C%22!@%23$%25%5E&*+=%3F\\'%3E.json\n * C:\\\\My Documents\\\\File (1).json ==> C:/My%20Documents/File%20(1).json\n * file://Project #42/file.json ==> file://Project%20%2342/file.json\n *\n * @param path\n * @returns\n */\nexport function fromFileSystemPath(path: string) {\n // Step 1: On Windows, replace backslashes with forward slashes,\n // rather than encoding them as \"%5C\"\n if (isWindows()) {\n const projectDir = cwd();\n const upperPath = path.toUpperCase();\n const projectDirPosixPath = convertPathToPosix(projectDir);\n const posixUpper = projectDirPosixPath.toUpperCase();\n const hasProjectDir = upperPath.includes(posixUpper);\n const hasProjectUri = upperPath.includes(posixUpper);\n const isAbsolutePath =\n win32.isAbsolute(path) ||\n path.startsWith('http://') ||\n path.startsWith('https://') ||\n path.startsWith('file://');\n\n if (!(hasProjectDir || hasProjectUri || isAbsolutePath) && !projectDir.startsWith('http')) {\n path = join(projectDir, path);\n }\n path = convertPathToPosix(path);\n }\n\n // Step 2: `encodeURI` will take care of MOST characters\n path = encodeURI(path);\n\n // Step 3: Manually encode characters that are not encoded by `encodeURI`.\n // This includes characters such as \"#\" and \"?\", which have special meaning in URLs,\n // but are just normal characters in a filesystem path.\n for (const pattern of urlEncodePatterns) {\n path = path.replace(pattern[0], pattern[1]);\n }\n\n return path;\n}\n\n/**\n * Converts a URL to a local filesystem path.\n */\nexport function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string {\n // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.\n path = decodeURI(path!);\n\n // Step 2: Manually decode characters that are not decoded by `decodeURI`.\n // This includes characters such as \"#\" and \"?\", which have special meaning in URLs,\n // but are just normal characters in a filesystem path.\n for (let i = 0; i < urlDecodePatterns.length; i += 2) {\n path = path.replace(urlDecodePatterns[i]!, urlDecodePatterns[i + 1] as string);\n }\n\n // Step 3: If it's a \"file://\" URL, then format it consistently\n // or convert it to a local filesystem path\n let isFileUrl = path.substr(0, 7).toLowerCase() === 'file://';\n if (isFileUrl) {\n // Strip-off the protocol, and the initial \"/\", if there is one\n path = path[7] === '/' ? path.substr(8) : path.substr(7);\n\n // insert a colon (\":\") after the drive letter on Windows\n if (isWindows() && path[1] === '/') {\n path = path[0] + ':' + path.substr(1);\n }\n\n if (keepFileProtocol) {\n // Return the consistently-formatted \"file://\" URL\n path = 'file:///' + path;\n } else {\n // Convert the \"file://\" URL to a local filesystem path.\n // On Windows, it will start with something like \"C:/\".\n // On Posix, it will start with \"/\"\n isFileUrl = false;\n path = isWindows() ? path : '/' + path;\n }\n }\n\n // Step 4: Normalize Windows paths (unless it's a \"file://\" URL)\n if (isWindows() && !isFileUrl) {\n // Replace forward slashes with backslashes\n path = path.replace(forwardSlashPattern, '\\\\');\n\n // Capitalize the drive letter\n if (path.substr(1, 2) === ':\\\\') {\n path = path[0]!.toUpperCase() + path.substr(1);\n }\n }\n\n return path;\n}\n\nexport function relative(from: string, to: string) {\n if (!isFileSystemPath(from) || !isFileSystemPath(to)) {\n return resolve(from, to);\n }\n\n const fromDir = path.dirname(stripHash(from));\n const toPath = stripHash(to);\n\n const result = path.relative(fromDir, toPath);\n return result + getHash(to);\n}\n","import { Ono } from '@jsdevtools/ono';\n\nimport type { $RefParser } from '..';\nimport type $Ref from '../ref';\nimport type { JSONSchema } from '../types';\nimport { getHash, stripHash, toFileSystemPath } from './url';\n\nexport type JSONParserErrorType =\n | 'EUNKNOWN'\n | 'EPARSER'\n | 'EUNMATCHEDPARSER'\n | 'ETIMEOUT'\n | 'ERESOLVER'\n | 'EUNMATCHEDRESOLVER'\n | 'EMISSINGPOINTER'\n | 'EINVALIDPOINTER';\n\nexport class JSONParserError extends Error {\n public readonly name: string;\n public readonly message: string;\n public source: string | undefined;\n public path: Array<string | number> | null;\n public readonly code: JSONParserErrorType;\n public constructor(message: string, source?: string) {\n super();\n\n this.code = 'EUNKNOWN';\n this.name = 'JSONParserError';\n this.message = message;\n this.source = source;\n this.path = null;\n\n Ono.extend(this);\n }\n\n get footprint() {\n return `${this.path}+${this.source}+${this.code}+${this.message}`;\n }\n}\n\nexport class JSONParserErrorGroup<S extends object = JSONSchema> extends Error {\n files: $RefParser;\n\n constructor(parser: $RefParser) {\n super();\n\n this.files = parser;\n this.name = 'JSONParserErrorGroup';\n this.message = `${this.errors.length} error${\n this.errors.length > 1 ? 's' : ''\n } occurred while reading '${toFileSystemPath(parser.$refs._root$Ref!.path)}'`;\n\n Ono.extend(this);\n }\n\n static getParserErrors<S extends object = JSONSchema>(parser: $RefParser) {\n const errors = [];\n\n for (const $ref of Object.values(parser.$refs._$refs) as $Ref<S>[]) {\n if ($ref.errors) {\n errors.push(...$ref.errors);\n }\n }\n\n return errors;\n }\n\n get errors(): Array<\n | JSONParserError\n | InvalidPointerError\n | ResolverError\n | ParserError\n | MissingPointerError\n | UnmatchedParserError\n | UnmatchedResolverError\n > {\n return JSONParserErrorGroup.getParserErrors<S>(this.files);\n }\n}\n\nexport class ParserError extends JSONParserError {\n code = 'EPARSER' as JSONParserErrorType;\n name = 'ParserError';\n constructor(message: any, source: any) {\n super(`Error parsing ${source}: ${message}`, source);\n }\n}\n\nexport class UnmatchedParserError extends JSONParserError {\n code = 'EUNMATCHEDPARSER' as JSONParserErrorType;\n name = 'UnmatchedParserError';\n\n constructor(source: string) {\n super(`Could not find parser for \"${source}\"`, source);\n }\n}\n\nexport class ResolverError extends JSONParserError {\n code = 'ERESOLVER' as JSONParserErrorType;\n name = 'ResolverError';\n ioErrorCode?: string;\n constructor(ex: Error | any, source?: string) {\n super(ex.message || `Error reading file \"${source}\"`, source);\n if ('code' in ex) {\n this.ioErrorCode = String(ex.code);\n }\n }\n}\n\nexport class UnmatchedResolverError extends JSONParserError {\n code = 'EUNMATCHEDRESOLVER' as JSONParserErrorType;\n name = 'UnmatchedResolverError';\n constructor(source: any) {\n super(`Could not find resolver for \"${source}\"`, source);\n }\n}\n\nexport class MissingPointerError extends JSONParserError {\n code = 'EMISSINGPOINTER' as JSONParserErrorType;\n name = 'MissingPointerError';\n constructor(token: string, path: string) {\n super(\n `Missing $ref pointer \"${getHash(path)}\". Token \"${token}\" does not exist.`,\n stripHash(path),\n );\n }\n}\n\nexport class TimeoutError extends JSONParserError {\n code = 'ETIMEOUT' as JSONParserErrorType;\n name = 'TimeoutError';\n constructor(timeout: number) {\n super(`Dereferencing timeout reached: ${timeout}ms`);\n }\n}\n\nexport class InvalidPointerError extends JSONParserError {\n code = 'EUNMATCHEDRESOLVER' as JSONParserErrorType;\n name = 'InvalidPointerError';\n constructor(pointer: string, path: string) {\n super(`Invalid $ref pointer \"${pointer}\". Pointers must begin with \"#/\"`, stripHash(path));\n }\n}\n\nexport function isHandledError(err: any): err is JSONParserError {\n return err instanceof JSONParserError || err instanceof JSONParserErrorGroup;\n}\n\nexport function normalizeError(err: any) {\n if (err.path === null) {\n err.path = [];\n }\n\n return err;\n}\n","import type { ParserOptions } from './options';\nimport Pointer from './pointer';\nimport type $Refs from './refs';\nimport type { JSONSchema } from './types';\nimport type {\n JSONParserError,\n MissingPointerError,\n ParserError,\n ResolverError,\n} from './util/errors';\nimport { normalizeError } from './util/errors';\n\nexport type $RefError = JSONParserError | ResolverError | ParserError | MissingPointerError;\n\n/**\n * This class represents a single JSON reference and its resolved value.\n *\n * @class\n */\nclass $Ref<S extends object = JSONSchema> {\n /**\n * The file path or URL of the referenced file.\n * This path is relative to the path of the main JSON schema file.\n *\n * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file.\n * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get\n * specific JSON pointers within the file.\n *\n * @type {string}\n */\n path: undefined | string;\n\n /**\n * The resolved value of the JSON reference.\n * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).\n *\n * @type {?*}\n */\n value: any;\n\n /**\n * The {@link $Refs} object that contains this {@link $Ref} object.\n *\n * @type {$Refs}\n */\n $refs: $Refs<S>;\n\n /**\n * Indicates the type of {@link $Ref#path} (e.g. \"file\", \"http\", etc.)\n */\n pathType: string | unknown;\n\n /**\n * List of all errors. Undefined if no errors.\n */\n errors: Array<$RefError> = [];\n\n constructor($refs: $Refs<S>) {\n this.$refs = $refs;\n }\n\n /**\n * Pushes an error to errors array.\n *\n * @param err - The error to be pushed\n * @returns\n */\n addError(err: $RefError) {\n if (this.errors === undefined) {\n this.errors = [];\n }\n\n const existingErrors = this.errors.map(({ footprint }: any) => footprint);\n\n // the path has been almost certainly set at this point,\n // but just in case something went wrong, normalizeError injects path if necessary\n // moreover, certain errors might point at the same spot, so filter them out to reduce noise\n if ('errors' in err && Array.isArray(err.errors)) {\n this.errors.push(\n ...err.errors\n .map(normalizeError)\n .filter(({ footprint }: any) => !existingErrors.includes(footprint)),\n );\n } else if (!('footprint' in err) || !existingErrors.includes(err.footprint)) {\n this.errors.push(normalizeError(err));\n }\n }\n\n /**\n * Determines whether the given JSON reference exists within this {@link $Ref#value}.\n *\n * @param path - The full path being resolved, optionally with a JSON pointer in the hash\n * @param options\n * @returns\n */\n exists(path: string, options?: ParserOptions) {\n try {\n this.resolve(path, options);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value.\n *\n * @param path - The full path being resolved, optionally with a JSON pointer in the hash\n * @param options\n * @returns - Returns the resolved value\n */\n get(path: string, options?: ParserOptions) {\n return this.resolve(path, options)?.value;\n }\n\n /**\n * Resolves the given JSON reference within this {@link $Ref#value}.\n *\n * @param path - The full path being resolved, optionally with a JSON pointer in the hash\n * @param options\n * @param friendlyPath - The original user-specified path (used for error messages)\n * @param pathFromRoot - The path of `obj` from the schema root\n * @returns\n */\n resolve(path: string, options?: ParserOptions, friendlyPath?: string, pathFromRoot?: string) {\n const pointer = new Pointer<S>(this, path, friendlyPath);\n return pointer.resolve(this.value, options, pathFromRoot);\n }\n\n /**\n * Sets the value of a nested property within this {@link $Ref#value}.\n * If the property, or any of its parents don't exist, they will be created.\n *\n * @param path - The full path of the property to set, optionally with a JSON pointer in the hash\n * @param value - The value to assign\n */\n set(path: string, value: any) {\n const pointer = new Pointer(this, path);\n this.value = pointer.set(this.value, value);\n }\n\n /**\n * Determines whether the given value is a JSON reference.\n *\n * @param value - The value to inspect\n * @returns\n */\n static is$Ref(value: unknown): value is { $ref: string; length?: number } {\n return (\n Boolean(value) &&\n typeof value === 'object' &&\n value !== null &&\n '$ref' in value &&\n typeof value.$ref === 'string' &&\n value.$ref.length > 0\n );\n }\n\n /**\n * Determines whether the given value is an external JSON reference.\n *\n * @param value - The value to inspect\n * @returns\n */\n static isExternal$Ref(value: unknown): boolean {\n return $Ref.is$Ref(value) && value.$ref![0] !== '#';\n }\n\n /**\n * Determines whether the given value is a JSON reference, and whether it is allowed by the options.\n *\n * @param value - The value to inspect\n * @param options\n * @returns\n */\n static isAllowed$Ref(value: unknown) {\n if (this.is$Ref(value)) {\n if (value.$ref.substring(0, 2) === '#/' || value.$ref === '#') {\n // It's a JSON Pointer reference, which is always allowed\n return true;\n } else if (value.$ref[0] !== '#') {\n // It's an external reference, which is allowed by the options\n return true;\n }\n }\n return undefined;\n }\n\n /**\n * Determines whether the given value is a JSON reference that \"extends\" its resolved value.\n * That is, it has extra properties (in addition to \"$ref\"), so rather than simply pointing to\n * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved\n * value, plus the extra properties.\n *\n * @example: {\n person: {\n properties: {\n firstName: { type: string }\n lastName: { type: string }\n }\n }\n employee: {\n properties: {\n $ref: #/person/properties\n salary: { type: number }\n }\n }\n }\n * In this example, \"employee\" is an extended $ref, since it extends \"person\" with an additional\n * property (salary). The result is a NEW value that looks like this:\n *\n * {\n * properties: {\n * firstName: { type: string }\n * lastName: { type: string }\n * salary: { type: number }\n * }\n * }\n *\n * @param value - The value to inspect\n * @returns\n */\n static isExtended$Ref(value: unknown) {\n return $Ref.is$Ref(value) && Object.keys(value).length > 1;\n }\n\n /**\n * Returns the resolved value of a JSON Reference.\n * If necessary, the resolved value is merged with the JSON Reference to create a new object\n *\n * @example: {\n person: {\n properties: {\n firstName: { type: string }\n lastName: { type: string }\n }\n }\n employee: {\n properties: {\n $ref: #/person/properties\n salary: { type: number }\n }\n }\n } When \"person\" and \"employee\" are merged, you end up with the following object:\n *\n * {\n * properties: {\n * firstName: { type: string }\n * lastName: { type: string }\n * salary: { type: number }\n * }\n * }\n *\n * @param $ref - The JSON reference object (the one with the \"$ref\" property)\n * @param resolvedValue - The resolved value, which can be any type\n * @returns - Returns the dereferenced value\n */\n static dereference<S extends object = JSONSchema>($ref: $Ref<S>, resolvedValue: S): S {\n if (resolvedValue && typeof resolvedValue === 'object' && $Ref.isExtended$Ref($ref)) {\n const merged = {};\n for (const key of Object.keys($ref)) {\n if (key !== '$ref') {\n // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message\n merged[key] = $ref[key];\n }\n }\n\n for (const key of Object.keys(resolvedValue)) {\n if (!(key in merged)) {\n // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message\n merged[key] = resolvedValue[key];\n }\n }\n\n return merged as S;\n } else {\n // Completely replace the original reference with the resolved value\n return resolvedValue;\n }\n }\n}\n\nexport default $Ref;\n","import type { ParserOptions } from './options';\nimport $Ref from './ref';\nimport type { JSONSchema } from './types';\nimport {\n InvalidPointerError,\n isHandledError,\n JSONParserError,\n MissingPointerError,\n} from './util/errors';\nimport * as url from './util/url';\n\nconst slashes = /\\//g;\nconst tildes = /~/g;\nconst escapedSlash = /~1/g;\nconst escapedTilde = /~0/g;\n\nconst safeDecodeURIComponent = (encodedURIComponent: string): string => {\n try {\n return decodeURIComponent(encodedURIComponent);\n } catch {\n return encodedURIComponent;\n }\n};\n\n/**\n * This class represents a single JSON pointer and its resolved value.\n *\n * @param $ref\n * @param path\n * @param [friendlyPath] - The original user-specified path (used for error messages)\n * @class\n */\nclass Pointer<S extends object = JSONSchema> {\n /**\n * The {@link $Ref} object that contains this {@link Pointer} object.\n */\n $ref: $Ref<S>;\n\n /**\n * The file path or URL, containing the JSON pointer in the hash.\n * This path is relative to the path of the main JSON schema file.\n */\n path: string;\n\n /**\n * The original path or URL, used for error messages.\n */\n originalPath: string;\n\n /**\n * The value of the JSON pointer.\n * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).\n */\n\n value: any;\n /**\n * Indicates whether the pointer references itself.\n */\n circular: boolean;\n /**\n * The number of indirect references that were traversed to resolve the value.\n * Resolving a single pointer may require resolving multiple $Refs.\n */\n indirections: number;\n\n constructor($ref: $Ref<S>, path: string, friendlyPath?: string) {\n this.$ref = $ref;\n\n this.path = path;\n\n this.originalPath = friendlyPath || path;\n\n this.value = undefined;\n\n this.circular = false;\n\n this.indirections = 0;\n }\n\n /**\n * Resolves the value of a nested property within the given object.\n *\n * @param obj - The object that will be crawled\n * @param options\n * @param pathFromRoot - the path of place that initiated resolving\n *\n * @returns\n * Returns a JSON pointer whose {@link Pointer#value} is the resolved value.\n * If resolving this value required resolving other JSON references, then\n * the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path\n * of the resolved value.\n */\n resolve(obj: S, options?: ParserOptions, pathFromRoot?: string) {\n const tokens = Pointer.parse(this.path, this.originalPath);\n\n // Crawl the object, one token at a time\n this.value = unwrapOrThrow(obj);\n\n const errors: MissingPointerError[] = [];\n\n for (let i = 0; i < tokens.length; i++) {\n if (resolveIf$Ref(this, options, pathFromRoot)) {\n // The $ref path has changed, so append the remaining tokens to the path\n this.path = Pointer.join(this.path, tokens.slice(i));\n }\n\n if (\n typeof this.value === 'object' &&\n this.value !== null &&\n !isRootPath(pathFromRoot) &&\n '$ref' in this.value\n ) {\n return this;\n }\n\n const token = tokens[i]!;\n if (\n this.value[token] === undefined ||\n (this.value[token] === null && i === tokens.length - 1)\n ) {\n // 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\n let didFindSubstringSlashMatch = false;\n for (let j = tokens.length - 1; j > i; j--) {\n const joinedToken = tokens.slice(i, j + 1).join('/');\n if (this.value[joinedToken] !== undefined) {\n this.value = this.value[joinedToken];\n i = j;\n didFindSubstringSlashMatch = true;\n break;\n }\n }\n if (didFindSubstringSlashMatch) {\n continue;\n }\n\n this.value = null;\n errors.push(new MissingPointerError(token, decodeURI(this.originalPath)));\n } else {\n this.value = this.value[token];\n }\n }\n\n if (errors.length > 0) {\n throw errors.length === 1\n ? errors[0]\n : new AggregateError(errors, 'Multiple missing pointer errors');\n }\n\n // Resolve the final value\n if (\n !this.value ||\n (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)\n ) {\n resolveIf$Ref(this, options, pathFromRoot);\n }\n\n return this;\n }\n\n /**\n * Sets the value of a nested property within the given object.\n *\n * @param obj - The object that will be crawled\n * @param value - the value to assign\n * @param options\n *\n * @returns\n * Returns the modified object, or an entirely new object if the entire object is overwritten.\n */\n set(obj: S, value: any, options?: ParserOptions) {\n const tokens = Pointer.parse(this.path);\n let token;\n\n if (tokens.length === 0) {\n // There are no tokens, replace the entire object with the new value\n this.value = value;\n return value;\n }\n\n // Crawl the object, one token at a time\n this.value = unwrapOrThrow(obj);\n\n for (let i = 0; i < tokens.length - 1; i++) {\n resolveIf$Ref(this, options);\n\n token = tokens[i]!;\n if (this.value && this.value[token] !== undefined) {\n // The token exists\n this.value = this.value[token];\n } else {\n // The token doesn't exist, so create it\n this.value = setValue(this, token, {});\n }\n }\n\n // Set the value of the final token\n resolveIf$Ref(this, options);\n token = tokens[tokens.length - 1];\n setValue(this, token, value);\n\n // Return the updated object\n return obj;\n }\n\n /**\n * Parses a JSON pointer (or a path containing a JSON pointer in the hash)\n * and returns an array of the pointer's tokens.\n * (e.g. \"schema.json#/definitions/person/name\" => [\"definitions\", \"person\", \"name\"])\n *\n * The pointer is parsed according to RFC 6901\n * {@link https://tools.ietf.org/html/rfc6901#section-3}\n *\n * @param path\n * @param [originalPath]\n * @returns\n */\n static parse(path: string, originalPath?: string): string[] {\n // Get the JSON pointer from the path's hash\n const pointer = url.getHash(path).substring(1);\n\n // If there's no pointer, then there are no tokens,\n // so return an empty array\n if (!pointer) {\n return [];\n }\n\n // Split into an array\n const split = pointer.split('/');\n\n // Decode each part, according to RFC 6901\n for (let i = 0; i < split.length; i++) {\n split[i] = safeDecodeURIComponent(\n split[i]!.replace(escapedSlash, '/').replace(escapedTilde, '~'),\n );\n }\n\n if (split[0] !== '') {\n throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath);\n }\n\n return split.slice(1);\n }\n\n /**\n * Creates a JSON pointer path, by joining one or more tokens to a base path.\n *\n * @param base - The base path (e.g. \"schema.json#/definitions/person\")\n * @param tokens - The token(s) to append (e.g. [\"name\", \"first\"])\n * @returns\n */\n static join(base: string, tokens: string | string[]) {\n // Ensure that the base path contains a hash\n if (base.indexOf('#') === -1) {\n base += '#';\n }\n\n // Append each token to the base path\n tokens = Array.isArray(tokens) ? tokens : [tokens];\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i]!;\n // Encode the token, according to RFC 6901\n base += '/' + encodeURIComponent(token.replace(tildes, '~0').replace(slashes, '~1'));\n }\n\n return base;\n }\n}\n\n/**\n * If the given pointer's {@link Pointer#value} is a JSON reference,\n * then the reference is resolved and {@link Pointer#value} is replaced with the resolved value.\n * In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the\n * resolution path of the new value.\n *\n * @param pointer\n * @param options\n * @param [pathFromRoot] - the path of place that initiated resolving\n * @returns - Returns `true` if the resolution path changed\n */\nfunction resolveIf$Ref(pointer: any, options: any, pathFromRoot?: any) {\n // Is the value a JSON reference? (and allowed?)\n\n if ($Ref.isAllowed$Ref(pointer.value)) {\n const $refPath = url.resolve(pointer.path, pointer.value.$ref);\n\n if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {\n // The value is a reference to itself, so there's nothing to do.\n pointer.circular = true;\n } else {\n const resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options);\n if (resolved === null) {\n return false;\n }\n\n pointer.indirections += resolved.indirections + 1;\n\n if ($Ref.isExtended$Ref(pointer.value)) {\n // This JSON reference \"extends\" the resolved value, rather than simply pointing to it.\n // So the resolved path does NOT change. Just the value does.\n pointer.value = $Ref.dereference(pointer.value, resolved.value);\n return false;\n } else {\n // Resolve the reference\n pointer.$ref = resolved.$ref;\n pointer.path = resolved.path;\n pointer.value = resolved.value;\n }\n\n return true;\n }\n }\n return undefined;\n}\nexport default Pointer;\n\n/**\n * Sets the specified token value of the {@link Pointer#value}.\n *\n * The token is evaluated according to RFC 6901.\n * {@link https://tools.ietf.org/html/rfc6901#section-4}\n *\n * @param pointer - The JSON Pointer whose value will be modified\n * @param token - A JSON Pointer token that indicates how to modify `obj`\n * @param value - The value to assign\n * @returns - Returns the assigned value\n */\nfunction setValue(pointer: any, token: any, value: any) {\n if (pointer.value && typeof pointer.value === 'object') {\n if (token === '-' && Array.isArray(pointer.value)) {\n pointer.value.push(value);\n } else {\n pointer.value[token] = value;\n }\n } else {\n throw new JSONParserError(\n `Error assigning $ref pointer \"${pointer.path}\". \\nCannot set \"${token}\" of a non-object.`,\n );\n }\n return value;\n}\n\nfunction unwrapOrThrow(value: any) {\n if (isHandledError(value)) {\n throw value;\n }\n\n return value;\n}\n\nfunction isRootPath(pathFromRoot: any): boolean {\n return typeof pathFromRoot == 'string' && Pointer.parse(pathFromRoot).length == 0;\n}\n","import type { $RefParser } from '.';\nimport type { ParserOptions } from './options';\nimport Pointer from './pointer';\nimport $Ref from './ref';\nimport type $Refs from './refs';\nimport type { JSONSchema } from './types';\nimport { MissingPointerError } from './util/errors';\nimport * as url from './util/url';\n\nexport interface InventoryEntry {\n $ref: any;\n circular: any;\n depth: any;\n extended: any;\n external: any;\n file: any;\n hash: any;\n indirections: any;\n key: any;\n originalContainerType?: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers';\n parent: any;\n pathFromRoot: any;\n value: any;\n}\n\n/**\n * Fast lookup using Map instead of linear search with deep equality\n */\nconst createInventoryLookup = () => {\n const lookup = new Map<string, InventoryEntry>();\n const objectIds = new WeakMap<object, string>(); // Use WeakMap to avoid polluting objects\n let idCounter = 0;\n\n const getObjectId = (obj: any) => {\n if (!objectIds.has(obj)) {\n objectIds.set(obj, `obj_${++idCounter}`);\n }\n return objectIds.get(obj)!;\n };\n\n const createInventoryKey = ($refParent: any, $refKey: any) =>\n // Use WeakMap-based lookup to avoid polluting the actual schema objects\n `${getObjectId($refParent)}_${$refKey}`;\n\n return {\n add: (entry: InventoryEntry) => {\n const key = createInventoryKey(entry.parent, entry.key);\n lookup.set(key, entry);\n },\n find: ($refParent: any, $refKey: any) => {\n const key = createInventoryKey($refParent, $refKey);\n const result = lookup.get(key);\n return result;\n },\n remove: (entry: InventoryEntry) => {\n const key = createInventoryKey(entry.parent, entry.key);\n lookup.delete(key);\n },\n };\n};\n\n/**\n * Determine the container type from a JSON Pointer path.\n * Analyzes the path tokens to identify the appropriate OpenAPI component container.\n *\n * @param path - The JSON Pointer path to analyze\n * @returns The container type: \"schemas\", \"parameters\", \"requestBodies\", \"responses\", or \"headers\"\n */\nconst getContainerTypeFromPath = (\n path: string,\n): 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers' => {\n const tokens = Pointer.parse(path);\n const has = (t: string) => tokens.includes(t);\n // Prefer more specific containers first\n if (has('parameters')) {\n return 'parameters';\n }\n if (has('requestBody')) {\n return 'requestBodies';\n }\n if (has('headers')) {\n return 'headers';\n }\n if (has('responses')) {\n return 'responses';\n }\n if (has('schema')) {\n return 'schemas';\n }\n // default: treat as schema-like\n return 'schemas';\n};\n\n/**\n * Inventories the given JSON Reference (i.e. records detailed information about it so we can\n * optimize all $refs in the schema), and then crawls the resolved value.\n */\nconst inventory$Ref = <S extends object = JSONSchema>({\n $refKey,\n $refParent,\n $refs,\n indirections,\n inventory,\n inventoryLookup,\n options,\n path,\n pathFromRoot,\n resolvedRefs = new Map(),\n visitedObjects = new WeakSet(),\n}: {\n /**\n * The key in `$refParent` that is a JSON Reference\n */\n $refKey: string | null;\n /**\n * The object that contains a JSON Reference as one of its keys\n */\n $refParent: any;\n $refs: $Refs<S>;\n /**\n * unknown\n */\n indirections: number;\n /**\n * An array of already-inventoried $ref pointers\n */\n inventory: Array<InventoryEntry>;\n /**\n * Fast lookup for inventory entries\n */\n inventoryLookup: ReturnType<typeof createInventoryLookup>;\n options: ParserOptions;\n /**\n * The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash\n */\n path: string;\n /**\n * The path of the JSON Reference at `$refKey`, from the schema root\n */\n pathFromRoot: string;\n /**\n * Cache for resolved $ref targets to avoid redundant resolution\n */\n resolvedRefs?: Map<string, any>;\n /**\n * Set of already visited objects to avoid infinite loops and redundant processing\n */\n visitedObjects?: WeakSet<object>;\n}) => {\n const $ref = $refKey === null ? $refParent : $refParent[$refKey];\n const $refPath = url.resolve(path, $ref.$ref);\n\n // Check cache first to avoid redundant resolution\n let pointer = resolvedRefs.get($refPath);\n if (!pointer) {\n try {\n pointer = $refs._resolve($refPath, pathFromRoot, options);\n } catch (error) {\n if (error instanceof MissingPointerError) {\n // The ref couldn't be resolved in the target file. This commonly\n // happens when a wrapper file redirects via $ref to a versioned\n // file, and the bundler's crawl path retains the wrapper URL.\n // Try resolving the hash fragment against other files in $refs\n // that might contain the target schema.\n const hash = url.getHash($refPath);\n if (hash) {\n const baseFile = url.stripHash($refPath);\n for (const filePath of Object.keys($refs._$refs)) {\n if (filePath === baseFile) continue;\n try {\n pointer = $refs._resolve(filePath + hash, pathFromRoot, options);\n if (pointer) break;\n } catch {\n // try next file\n }\n }\n }\n if (!pointer) {\n console.warn(`Skipping unresolvable $ref: ${$refPath}`);\n return;\n }\n } else {\n throw error;\n }\n }\n\n if (pointer) {\n resolvedRefs.set($refPath, pointer);\n }\n }\n\n if (pointer === null) return;\n\n const parsed = Pointer.parse(pathFromRoot);\n const depth = parsed.length;\n const file = url.stripHash(pointer.path);\n const hash = url.getHash(pointer.path);\n const external = file !== $refs._root$Ref.path;\n const extended = $Ref.isExtended$Ref($ref);\n indirections += pointer.indirections;\n\n // Check if this exact location (parent + key + pathFromRoot) has already been inventoried\n const existingEntry = inventoryLookup.find($refParent, $refKey);\n\n if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {\n // This exact location has already been inventoried, so we don't need to process it again\n if (depth < existingEntry.depth || indirections < existingEntry.indirections) {\n removeFromInventory(inventory, existingEntry);\n inventoryLookup.remove(existingEntry);\n } else {\n return;\n }\n }\n\n const newEntry: InventoryEntry = {\n $ref, // The JSON Reference (e.g. {$ref: string})\n circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)\n depth, // How far from the JSON Schema root is this $ref pointer?\n extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to \"$ref\")\n external, // Does this $ref pointer point to a file other than the main JSON Schema file?\n file, // The file that the $ref pointer resolves to\n hash, // The hash within `file` that the $ref pointer resolves to\n indirections, // The number of indirect references that were traversed to resolve the value\n key: $refKey,\n // The resolved value of the $ref pointer\n originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined,\n\n // The key in `parent` that is the $ref pointer\n parent: $refParent,\n\n // The object that contains this $ref pointer\n pathFromRoot,\n // The path to the $ref pointer, from the JSON Schema root\n value: pointer.value, // The original container type in the external file\n };\n\n inventory.push(newEntry);\n inventoryLookup.add(newEntry);\n\n // Recursively crawl the resolved value.\n // When the resolution followed a $ref chain to a different file,\n // use the resolved file as the base path so that local $ref values\n // (e.g. #/components/schemas/SiblingSchema) inside the resolved\n // value resolve against the correct file.\n if (!existingEntry || external) {\n let crawlPath = pointer.path;\n\n const originalFile = url.stripHash($refPath);\n if (file !== originalFile) {\n crawlPath = file + url.getHash(pointer.path);\n }\n\n crawl({\n $refs,\n indirections: indirections + 1,\n inventory,\n inventoryLookup,\n key: null,\n options,\n parent: pointer.value,\n path: crawlPath,\n pathFromRoot,\n resolvedRefs,\n visitedObjects,\n });\n }\n};\n\n/**\n * Recursively crawls the given value, and inventories all JSON references.\n */\nconst crawl = <S extends object = JSONSchema>({\n $refs,\n indirections,\n inventory,\n inventoryLookup,\n key,\n options,\n parent,\n path,\n pathFromRoot,\n resolvedRefs = new Map(),\n visitedObjects = new WeakSet(),\n}: {\n $refs: $Refs<S>;\n indirections: number;\n /**\n * An array of already-inventoried $ref pointers\n */\n inventory: Array<InventoryEntry>;\n /**\n * Fast lookup for inventory entries\n */\n inventoryLookup: ReturnType<typeof createInventoryLookup>;\n /**\n * The property key of `parent` to be crawled\n */\n key: string | null;\n options: ParserOptions;\n /**\n * The object containing the value to crawl. If the value is not an object or array, it will be ignored.\n */\n parent: object | $RefParser;\n /**\n * The full path of the property being crawled, possibly with a JSON Pointer in the hash\n */\n path: string;\n /**\n * The path of the property being crawled, from the schema root\n */\n pathFromRoot: string;\n /**\n * Cache for resolved $ref targets to avoid redundant resolution\n */\n resolvedRefs?: Map<string, any>;\n /**\n * Set of already visited objects to avoid infinite loops and redundant processing\n */\n visitedObjects?: WeakSet<object>;\n}) => {\n const obj = key === null ? parent : parent[key as keyof typeof parent];\n\n if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) {\n // Early exit if we've already processed this exact object\n if (visitedObjects.has(obj)) return;\n\n if ($Ref.isAllowed$Ref(obj)) {\n inventory$Ref({\n $refKey: key,\n $refParent: parent,\n $refs,\n indirections,\n inventory,\n inventoryLookup,\n options,\n path,\n pathFromRoot,\n resolvedRefs,\n visitedObjects,\n });\n } else {\n // Mark this object as visited BEFORE processing its children\n visitedObjects.add(obj);\n\n // Crawl the object in a specific order that's optimized for bundling.\n // This is important because it determines how `pathFromRoot` gets built,\n // which later determines which keys get dereferenced and which ones get remapped\n const keys = Object.keys(obj).sort((a, b) => {\n // Most people will expect references to be bundled into the \"definitions\" property,\n // so we always crawl that property first, if it exists.\n if (a === 'definitions') {\n return -1;\n } else if (b === 'definitions') {\n return 1;\n } else {\n // Otherwise, crawl the keys based on their length.\n // This produces the shortest possible bundled references\n return a.length - b.length;\n }\n }) as Array<keyof typeof obj>;\n\n for (const key of keys) {\n const keyPath = Pointer.join(path, key);\n const keyPathFromRoot = Pointer.join(pathFromRoot, key);\n const value = obj[key];\n\n if ($Ref.isAllowed$Ref(value)) {\n inventory$Ref({\n $refKey: key,\n $refParent: obj,\n $refs,\n indirections,\n inventory,\n inventoryLookup,\n options,\n path,\n pathFromRoot: keyPathFromRoot,\n resolvedRefs,\n visitedObjects,\n });\n } else {\n crawl({\n $refs,\n indirections,\n inventory,\n inventoryLookup,\n key,\n options,\n parent: obj,\n path: keyPath,\n pathFromRoot: keyPathFromRoot,\n resolvedRefs,\n visitedObjects,\n });\n }\n }\n }\n }\n};\n\n/**\n * Remap external refs by hoisting resolved values into a shared container in the root schema\n * and pointing all occurrences to those internal definitions. Internal refs remain internal.\n */\nfunction remap(parser: $RefParser, inventory: Array<InventoryEntry>) {\n const root = parser.schema as any;\n\n // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them\n inventory.sort((a: InventoryEntry, b: InventoryEntry) => {\n if (a.file !== b.file) {\n // Group all the $refs that point to the same file\n return a.file < b.file ? -1 : +1;\n } else if (a.hash !== b.hash) {\n // Group all the $refs that point to the same part of the file\n return a.hash < b.hash ? -1 : +1;\n } else if (a.circular !== b.circular) {\n // If the $ref points to itself, then sort it higher than other $refs that point to this $ref\n return a.circular ? -1 : +1;\n } else if (a.extended !== b.extended) {\n // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value\n return a.extended ? +1 : -1;\n } else if (a.indirections !== b.indirections) {\n // Sort direct references higher than indirect references\n return a.indirections - b.indirections;\n } else if (a.depth !== b.depth) {\n // Sort $refs by how close they are to the JSON Schema root\n return a.depth - b.depth;\n } else {\n // Determine how far each $ref is from the \"definitions\" property.\n // Most people will expect references to be bundled into the the \"definitions\" property if possible.\n const aDefinitionsIndex = a.pathFromRoot.lastIndexOf('/definitions');\n const bDefinitionsIndex = b.pathFromRoot.lastIndexOf('/definitions');\n if (aDefinitionsIndex !== bDefinitionsIndex) {\n // Give higher priority to the $ref that's closer to the \"definitions\" property\n return bDefinitionsIndex - aDefinitionsIndex;\n } else {\n // All else is equal, so use the shorter path, which will produce the shortest possible reference\n return a.pathFromRoot.length - b.pathFromRoot.length;\n }\n }\n });\n\n // Ensure or return a container by component type. Prefer OpenAPI-aware placement;\n // otherwise use existing root containers; otherwise create components/*.\n const ensureContainer = (\n type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',\n ) => {\n const isOas3 = !!(root && typeof root === 'object' && typeof root.openapi === 'string');\n const isOas2 = !!(root && typeof root === 'object' && typeof root.swagger === 'string');\n\n if (isOas3) {\n if (!root.components || typeof root.components !== 'object') {\n root.components = {};\n }\n if (!root.components[type] || typeof root.components[type] !== 'object') {\n root.components[type] = {};\n }\n return { obj: root.components[type], prefix: `#/components/${type}` } as const;\n }\n\n if (isOas2) {\n if (type === 'schemas') {\n if (!root.definitions || typeof root.definitions !== 'object') {\n root.definitions = {};\n }\n return { obj: root.definitions, prefix: '#/definitions' } as const;\n }\n if (type === 'parameters') {\n if (!root.parameters || typeof root.parameters !== 'object') {\n root.parameters = {};\n }\n return { obj: root.parameters, prefix: '#/parameters' } as const;\n }\n if (type === 'responses') {\n if (!root.responses || typeof root.responses !== 'object') {\n root.responses = {};\n }\n return { obj: root.responses, prefix: '#/responses' } as const;\n }\n // requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions\n if (!root.definitions || typeof root