···11+# JSON Schema $Ref Parser
22+33+#### Parse, Resolve, and Dereference JSON Schema $ref pointers
44+55+## Installation
66+77+Install using [npm](https://docs.npmjs.com/about-npm/):
88+99+```bash
1010+npm install @hey-api/json-schema-ref-parser
1111+yarn add @hey-api/json-schema-ref-parser
1212+bun add @hey-api/json-schema-ref-parser
1313+```
1414+1515+## The Problem:
1616+1717+You've got a JSON Schema with `$ref` pointers to other files and/or URLs. Maybe you know all the referenced files ahead
1818+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
1919+format. Maybe some of the files contain cross-references to each other.
2020+2121+```json
2222+{
2323+ "definitions": {
2424+ "person": {
2525+ // references an external file
2626+ "$ref": "schemas/people/Bruce-Wayne.json"
2727+ },
2828+ "place": {
2929+ // references a sub-schema in an external file
3030+ "$ref": "schemas/places.yaml#/definitions/Gotham-City"
3131+ },
3232+ "thing": {
3333+ // references a URL
3434+ "$ref": "http://wayne-enterprises.com/things/batmobile"
3535+ },
3636+ "color": {
3737+ // references a value in an external file via an internal reference
3838+ "$ref": "#/definitions/thing/properties/colors/black-as-the-night"
3939+ }
4040+ }
4141+}
4242+```
4343+4444+## The Solution:
4545+4646+JSON Schema $Ref Parser is a full [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03)
4747+and [JSON Pointer](https://tools.ietf.org/html/rfc6901) implementation that crawls even the most
4848+complex [JSON Schemas](http://json-schema.org/latest/json-schema-core.html) and gives you simple, straightforward
4949+JavaScript objects.
5050+5151+- Use **JSON** or **YAML** schemas — or even a mix of both!
5252+- Supports `$ref` pointers to external files and URLs, as well as custom sources such as databases
5353+- Can bundle multiple files into a single schema that only has _internal_ `$ref` pointers
5454+- Can dereference your schema, producing a plain-old JavaScript object that's easy to work with
5555+- Supports circular references, nested references,
5656+ back-references, and cross-references between files
5757+- Maintains object reference equality — `$ref` pointers to the same value always resolve to the same object
5858+ instance
5959+- Compatible with Node LTS and beyond, and all major web browsers on Windows, Mac, and Linux
6060+6161+## Example
6262+6363+```javascript
6464+import { $RefParser } from '@hey-api/json-schema-ref-parser';
6565+6666+try {
6767+ const parser = new $RefParser();
6868+ await parser.dereference({ pathOrUrlOrSchema: mySchema });
6969+ console.log(parser.schema.definitions.person.properties.firstName);
7070+} catch (err) {
7171+ console.error(err);
7272+}
7373+```
7474+7575+### New in this fork (@hey-api)
7676+7777+- **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.
7878+7979+```javascript
8080+import { $RefParser } from '@hey-api/json-schema-ref-parser';
8181+8282+const parser = new $RefParser();
8383+const merged = await parser.bundleMany({
8484+ pathOrUrlOrSchemas: [
8585+ './specs/a.yaml',
8686+ 'https://example.com/b.yaml',
8787+ { openapi: '3.1.0', info: { title: 'Inline' }, paths: {} },
8888+ ],
8989+});
9090+9191+// merged.components.* will contain prefixed names like a_<name>, b_<name>, etc.
9292+```
9393+9494+- **Dereference hooks**: Fine-tune dereferencing with `excludedPathMatcher(path) => boolean` to skip subpaths and `onDereference(path, value, parent, parentPropName)` to observe replacements.
9595+9696+```javascript
9797+const parser = new $RefParser();
9898+parser.options.dereference.excludedPathMatcher = (p) => p.includes('/example/');
9999+parser.options.dereference.onDereference = (p, v) => {
100100+ // inspect p / v as needed
101101+};
102102+await parser.dereference({ pathOrUrlOrSchema: './openapi.yaml' });
103103+```
104104+105105+- **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.
106106+107107+```javascript
108108+await new $RefParser().bundle({
109109+ pathOrUrlOrSchema: {
110110+ $id: 'https://api.example.com/openapi.json',
111111+ openapi: '3.1.0',
112112+ paths: {
113113+ '/ping': { get: { responses: { 200: { description: 'ok' } } } },
114114+ },
115115+ },
116116+});
117117+```
···11+import { ono } from '@jsdevtools/ono';
22+33+import type { $RefParser } from '.';
44+import type { DereferenceOptions, ParserOptions } from './options';
55+import Pointer from './pointer';
66+import $Ref from './ref';
77+import type $Refs from './refs';
88+import type { JSONSchema } from './types';
99+import { TimeoutError } from './util/errors';
1010+import * as url from './util/url';
1111+1212+export default dereference;
1313+1414+/**
1515+ * Crawls the JSON schema, finds all JSON references, and dereferences them.
1616+ * This method mutates the JSON schema object, replacing JSON references with their resolved value.
1717+ *
1818+ * @param parser
1919+ * @param options
2020+ */
2121+function dereference(parser: $RefParser, options: ParserOptions) {
2222+ const start = Date.now();
2323+ // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
2424+ const dereferenced = crawl<JSONSchema>(
2525+ parser.schema,
2626+ parser.$refs._root$Ref.path!,
2727+ '#',
2828+ new Set(),
2929+ new Set(),
3030+ new Map(),
3131+ parser.$refs,
3232+ options,
3333+ start,
3434+ );
3535+ parser.$refs.circular = dereferenced.circular;
3636+ parser.schema = dereferenced.value;
3737+}
3838+3939+/**
4040+ * Recursively crawls the given value, and dereferences any JSON references.
4141+ *
4242+ * @param obj - The value to crawl. If it's not an object or array, it will be ignored.
4343+ * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash
4444+ * @param pathFromRoot - The path of `obj` from the schema root
4545+ * @param parents - An array of the parent objects that have already been dereferenced
4646+ * @param processedObjects - An array of all the objects that have already been processed
4747+ * @param dereferencedCache - An map of all the dereferenced objects
4848+ * @param $refs
4949+ * @param options
5050+ * @param startTime - The time when the dereferencing started
5151+ * @returns
5252+ */
5353+function crawl<S extends object = JSONSchema>(
5454+ obj: any,
5555+ path: string,
5656+ pathFromRoot: string,
5757+ parents: Set<any>,
5858+ processedObjects: Set<any>,
5959+ dereferencedCache: any,
6060+ $refs: $Refs<S>,
6161+ options: ParserOptions,
6262+ startTime: number,
6363+) {
6464+ let dereferenced;
6565+ const result = {
6666+ circular: false,
6767+ value: obj,
6868+ };
6969+7070+ if (options && options.timeoutMs) {
7171+ if (Date.now() - startTime > options.timeoutMs) {
7272+ throw new TimeoutError(options.timeoutMs);
7373+ }
7474+ }
7575+ const derefOptions = (options.dereference || {}) as DereferenceOptions;
7676+ const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
7777+7878+ if (derefOptions?.circular === 'ignore' || !processedObjects.has(obj)) {
7979+ if (
8080+ obj &&
8181+ typeof obj === 'object' &&
8282+ !ArrayBuffer.isView(obj) &&
8383+ !isExcludedPath(pathFromRoot)
8484+ ) {
8585+ parents.add(obj);
8686+ processedObjects.add(obj);
8787+8888+ if ($Ref.isAllowed$Ref(obj)) {
8989+ dereferenced = dereference$Ref(
9090+ obj,
9191+ path,
9292+ pathFromRoot,
9393+ parents,
9494+ processedObjects,
9595+ dereferencedCache,
9696+ $refs,
9797+ options,
9898+ startTime,
9999+ );
100100+ result.circular = dereferenced.circular;
101101+ result.value = dereferenced.value;
102102+ } else {
103103+ for (const key of Object.keys(obj)) {
104104+ const keyPath = Pointer.join(path, key);
105105+ const keyPathFromRoot = Pointer.join(pathFromRoot, key);
106106+107107+ if (isExcludedPath(keyPathFromRoot)) {
108108+ continue;
109109+ }
110110+111111+ const value = obj[key];
112112+ let circular = false;
113113+114114+ if ($Ref.isAllowed$Ref(value)) {
115115+ dereferenced = dereference$Ref(
116116+ value,
117117+ keyPath,
118118+ keyPathFromRoot,
119119+ parents,
120120+ processedObjects,
121121+ dereferencedCache,
122122+ $refs,
123123+ options,
124124+ startTime,
125125+ );
126126+ circular = dereferenced.circular;
127127+ // Avoid pointless mutations; breaks frozen objects to no profit
128128+ if (obj[key] !== dereferenced.value) {
129129+ obj[key] = dereferenced.value;
130130+ derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
131131+ }
132132+ } else {
133133+ if (!parents.has(value)) {
134134+ dereferenced = crawl(
135135+ value,
136136+ keyPath,
137137+ keyPathFromRoot,
138138+ parents,
139139+ processedObjects,
140140+ dereferencedCache,
141141+ $refs,
142142+ options,
143143+ startTime,
144144+ );
145145+ circular = dereferenced.circular;
146146+ // Avoid pointless mutations; breaks frozen objects to no profit
147147+ if (obj[key] !== dereferenced.value) {
148148+ obj[key] = dereferenced.value;
149149+ }
150150+ } else {
151151+ circular = foundCircularReference(keyPath, $refs, options);
152152+ }
153153+ }
154154+155155+ // Set the "isCircular" flag if this or any other property is circular
156156+ result.circular = result.circular || circular;
157157+ }
158158+ }
159159+160160+ parents.delete(obj);
161161+ }
162162+ }
163163+164164+ return result;
165165+}
166166+167167+/**
168168+ * Dereferences the given JSON Reference, and then crawls the resulting value.
169169+ *
170170+ * @param $ref - The JSON Reference to resolve
171171+ * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash
172172+ * @param pathFromRoot - The path of `$ref` from the schema root
173173+ * @param parents - An array of the parent objects that have already been dereferenced
174174+ * @param processedObjects - An array of all the objects that have already been dereferenced
175175+ * @param dereferencedCache - An map of all the dereferenced objects
176176+ * @param $refs
177177+ * @param options
178178+ * @returns
179179+ */
180180+function dereference$Ref<S extends object = JSONSchema>(
181181+ $ref: any,
182182+ path: string,
183183+ pathFromRoot: string,
184184+ parents: Set<any>,
185185+ processedObjects: any,
186186+ dereferencedCache: any,
187187+ $refs: $Refs<S>,
188188+ options: ParserOptions,
189189+ startTime: number,
190190+) {
191191+ const $refPath = url.resolve(path, $ref.$ref);
192192+193193+ const cache = dereferencedCache.get($refPath);
194194+ if (cache && !cache.circular) {
195195+ const refKeys = Object.keys($ref);
196196+ if (refKeys.length > 1) {
197197+ const extraKeys = {};
198198+ for (const key of refKeys) {
199199+ if (key !== '$ref' && !(key in cache.value)) {
200200+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
201201+ extraKeys[key] = $ref[key];
202202+ }
203203+ }
204204+ return {
205205+ circular: cache.circular,
206206+ value: Object.assign({}, structuredClone(cache.value), extraKeys),
207207+ };
208208+ }
209209+210210+ // Return a deep-cloned value so each occurrence is an independent copy
211211+ return { circular: cache.circular, value: structuredClone(cache.value) };
212212+ }
213213+214214+ const pointer = $refs._resolve($refPath, path, options);
215215+216216+ if (pointer === null) {
217217+ return {
218218+ circular: false,
219219+ value: null,
220220+ };
221221+ }
222222+223223+ // Check for circular references
224224+ const directCircular = pointer.circular;
225225+ let circular = directCircular || parents.has(pointer.value);
226226+ if (circular) {
227227+ foundCircularReference(path, $refs, options);
228228+ }
229229+230230+ // Dereference the JSON reference
231231+ let dereferencedValue = $Ref.dereference($ref, pointer.value);
232232+233233+ // Crawl the dereferenced value (unless it's circular)
234234+ if (!circular) {
235235+ // Determine if the dereferenced value is circular
236236+ const dereferenced = crawl(
237237+ dereferencedValue,
238238+ pointer.path,
239239+ pathFromRoot,
240240+ parents,
241241+ processedObjects,
242242+ dereferencedCache,
243243+ $refs,
244244+ options,
245245+ startTime,
246246+ );
247247+ circular = dereferenced.circular;
248248+ dereferencedValue = dereferenced.value;
249249+ }
250250+251251+ if (circular && !directCircular && options.dereference?.circular === 'ignore') {
252252+ // The user has chosen to "ignore" circular references, so don't change the value
253253+ dereferencedValue = $ref;
254254+ }
255255+256256+ if (directCircular) {
257257+ // The pointer is a DIRECT circular reference (i.e. it references itself).
258258+ // So replace the $ref path with the absolute path from the JSON Schema root
259259+ dereferencedValue.$ref = pathFromRoot;
260260+ }
261261+262262+ const dereferencedObject = {
263263+ circular,
264264+ value: dereferencedValue,
265265+ };
266266+267267+ // only cache if no extra properties than $ref
268268+ if (Object.keys($ref).length === 1) {
269269+ dereferencedCache.set($refPath, dereferencedObject);
270270+ }
271271+272272+ return dereferencedObject;
273273+}
274274+275275+/**
276276+ * Called when a circular reference is found.
277277+ * It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false.
278278+ *
279279+ * @param keyPath - The JSON Reference path of the circular reference
280280+ * @param $refs
281281+ * @param options
282282+ * @returns - always returns true, to indicate that a circular reference was found
283283+ */
284284+function foundCircularReference(keyPath: any, $refs: any, options: any) {
285285+ $refs.circular = true;
286286+ if (!options.dereference.circular) {
287287+ throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
288288+ }
289289+ return true;
290290+}
+599
packages/json-schema-ref-parser/src/index.ts
···11+import { ono } from '@jsdevtools/ono';
22+33+import { bundle as _bundle } from './bundle';
44+import _dereference from './dereference';
55+import { getJsonSchemaRefParserDefaultOptions } from './options';
66+import { newFile, parseFile } from './parse';
77+import $Refs from './refs';
88+import { resolveExternal } from './resolve-external';
99+import { fileResolver } from './resolvers/file';
1010+import { urlResolver } from './resolvers/url';
1111+import type { JSONSchema } from './types';
1212+import { isHandledError, JSONParserErrorGroup } from './util/errors';
1313+import * as url from './util/url';
1414+1515+interface ResolvedInput {
1616+ path: string;
1717+ schema: string | JSONSchema | Buffer | Awaited<JSONSchema> | undefined;
1818+ type: 'file' | 'json' | 'url';
1919+}
2020+2121+export const getResolvedInput = ({
2222+ pathOrUrlOrSchema,
2323+}: {
2424+ pathOrUrlOrSchema: JSONSchema | string | unknown;
2525+}): ResolvedInput => {
2626+ if (!pathOrUrlOrSchema) {
2727+ throw ono(`Expected a file path, URL, or object. Got ${pathOrUrlOrSchema}`);
2828+ }
2929+3030+ const resolvedInput: ResolvedInput = {
3131+ path: typeof pathOrUrlOrSchema === 'string' ? pathOrUrlOrSchema : '',
3232+ schema: undefined,
3333+ type: 'url',
3434+ };
3535+3636+ // If the path is a filesystem path, then convert it to a URL.
3737+ // NOTE: According to the JSON Reference spec, these should already be URLs,
3838+ // but, in practice, many people use local filesystem paths instead.
3939+ // So we're being generous here and doing the conversion automatically.
4040+ // This is not intended to be a 100% bulletproof solution.
4141+ // If it doesn't work for your use-case, then use a URL instead.
4242+ if (resolvedInput.path && url.isFileSystemPath(resolvedInput.path)) {
4343+ resolvedInput.path = url.fromFileSystemPath(resolvedInput.path);
4444+ resolvedInput.type = 'file';
4545+ } else if (!resolvedInput.path && pathOrUrlOrSchema && typeof pathOrUrlOrSchema === 'object') {
4646+ if ('$id' in pathOrUrlOrSchema && pathOrUrlOrSchema.$id) {
4747+ // when schema id has defined an URL should use that hostname to request the references,
4848+ // instead of using the current page URL
4949+ const { hostname, protocol } = new URL(pathOrUrlOrSchema.$id as string);
5050+ resolvedInput.path = `${protocol}//${hostname}:${protocol === 'https:' ? 443 : 80}`;
5151+ resolvedInput.type = 'url';
5252+ } else {
5353+ resolvedInput.schema = pathOrUrlOrSchema;
5454+ resolvedInput.type = 'json';
5555+ }
5656+ }
5757+5858+ if (resolvedInput.type !== 'json') {
5959+ // resolve the absolute path of the schema
6060+ resolvedInput.path = url.resolve(url.cwd(), resolvedInput.path);
6161+ }
6262+6363+ return resolvedInput;
6464+};
6565+6666+// NOTE: previously used helper removed as unused
6767+6868+/**
6969+ * This class parses a JSON schema, builds a map of its JSON references and their resolved values,
7070+ * and provides methods for traversing, manipulating, and dereferencing those references.
7171+ */
7272+export class $RefParser {
7373+ /**
7474+ * The resolved JSON references
7575+ *
7676+ * @type {$Refs}
7777+ * @readonly
7878+ */
7979+ $refs = new $Refs<JSONSchema>();
8080+ public options = getJsonSchemaRefParserDefaultOptions();
8181+ /**
8282+ * The parsed (and possibly dereferenced) JSON schema object
8383+ *
8484+ * @type {object}
8585+ * @readonly
8686+ */
8787+ public schema: JSONSchema | null = null;
8888+ public schemaMany: JSONSchema[] = [];
8989+ public schemaManySources: string[] = [];
9090+ public sourcePathToPrefix: Map<string, string> = new Map();
9191+9292+ /**
9393+ * 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.
9494+ *
9595+ * This also eliminates the risk of circular references, so the schema can be safely serialized using `JSON.stringify()`.
9696+ *
9797+ * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html#bundleschema-options-callback
9898+ *
9999+ * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file.
100100+ */
101101+ public async bundle({
102102+ arrayBuffer,
103103+ fetch,
104104+ pathOrUrlOrSchema,
105105+ resolvedInput,
106106+ }: {
107107+ arrayBuffer?: ArrayBuffer;
108108+ fetch?: RequestInit;
109109+ pathOrUrlOrSchema: JSONSchema | string | unknown;
110110+ resolvedInput?: ResolvedInput;
111111+ }): Promise<JSONSchema> {
112112+ await this.parse({
113113+ arrayBuffer,
114114+ fetch,
115115+ pathOrUrlOrSchema,
116116+ resolvedInput,
117117+ });
118118+119119+ await resolveExternal(this, this.options);
120120+ const errors = JSONParserErrorGroup.getParserErrors(this);
121121+ if (errors.length > 0) {
122122+ throw new JSONParserErrorGroup(this);
123123+ }
124124+ _bundle(this, this.options);
125125+ const errors2 = JSONParserErrorGroup.getParserErrors(this);
126126+ if (errors2.length > 0) {
127127+ throw new JSONParserErrorGroup(this);
128128+ }
129129+ return this.schema!;
130130+ }
131131+132132+ /**
133133+ * Bundles multiple roots (files/URLs/objects) into a single schema by creating a synthetic root
134134+ * that references each input, resolving all externals, and then hoisting via the existing bundler.
135135+ */
136136+ public async bundleMany({
137137+ arrayBuffer,
138138+ fetch,
139139+ pathOrUrlOrSchemas,
140140+ resolvedInputs,
141141+ }: {
142142+ arrayBuffer?: ArrayBuffer[];
143143+ fetch?: RequestInit;
144144+ pathOrUrlOrSchemas: Array<JSONSchema | string | unknown>;
145145+ resolvedInputs?: ResolvedInput[];
146146+ }): Promise<JSONSchema> {
147147+ await this.parseMany({ arrayBuffer, fetch, pathOrUrlOrSchemas, resolvedInputs });
148148+ this.mergeMany();
149149+150150+ await resolveExternal(this, this.options);
151151+ const errors = JSONParserErrorGroup.getParserErrors(this);
152152+ if (errors.length > 0) {
153153+ throw new JSONParserErrorGroup(this);
154154+ }
155155+ _bundle(this, this.options);
156156+ // Merged root is ready for bundling
157157+158158+ const errors2 = JSONParserErrorGroup.getParserErrors(this);
159159+ if (errors2.length > 0) {
160160+ throw new JSONParserErrorGroup(this);
161161+ }
162162+ return this.schema!;
163163+ }
164164+165165+ /**
166166+ * 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.
167167+ *
168168+ * 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.
169169+ *
170170+ * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html#dereferenceschema-options-callback
171171+ *
172172+ * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file.
173173+ */
174174+ public async dereference({
175175+ fetch,
176176+ pathOrUrlOrSchema,
177177+ }: {
178178+ fetch?: RequestInit;
179179+ pathOrUrlOrSchema: JSONSchema | string | unknown;
180180+ }): Promise<JSONSchema> {
181181+ await this.parse({
182182+ fetch,
183183+ pathOrUrlOrSchema,
184184+ });
185185+ await resolveExternal(this, this.options);
186186+ const errors = JSONParserErrorGroup.getParserErrors(this);
187187+ if (errors.length > 0) {
188188+ throw new JSONParserErrorGroup(this);
189189+ }
190190+ _dereference(this, this.options);
191191+ const errors2 = JSONParserErrorGroup.getParserErrors(this);
192192+ if (errors2.length > 0) {
193193+ throw new JSONParserErrorGroup(this);
194194+ }
195195+ return this.schema!;
196196+ }
197197+198198+ /**
199199+ * Parses the given JSON schema.
200200+ * This method does not resolve any JSON references.
201201+ * It just reads a single file in JSON or YAML format, and parse it as a JavaScript object.
202202+ *
203203+ * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file.
204204+ * @returns - The returned promise resolves with the parsed JSON schema object.
205205+ */
206206+ public async parse({
207207+ arrayBuffer,
208208+ fetch,
209209+ pathOrUrlOrSchema,
210210+ resolvedInput: _resolvedInput,
211211+ }: {
212212+ arrayBuffer?: ArrayBuffer;
213213+ fetch?: RequestInit;
214214+ pathOrUrlOrSchema: JSONSchema | string | unknown;
215215+ resolvedInput?: ResolvedInput;
216216+ }): Promise<{ schema: JSONSchema }> {
217217+ const resolvedInput = _resolvedInput || getResolvedInput({ pathOrUrlOrSchema });
218218+ const { path, type } = resolvedInput;
219219+ let { schema } = resolvedInput;
220220+221221+ // reset everything
222222+ this.schema = null;
223223+ this.$refs = new $Refs();
224224+225225+ if (schema) {
226226+ // immediately add a new $Ref with the schema object as value
227227+ const $ref = this.$refs._add(path);
228228+ $ref.pathType = url.isFileSystemPath(path) ? 'file' : 'http';
229229+ $ref.value = schema;
230230+ } else if (type !== 'json') {
231231+ const file = newFile(path);
232232+233233+ // Add a new $Ref for this file, even though we don't have the value yet.
234234+ // This ensures that we don't simultaneously read & parse the same file multiple times
235235+ const $refAdded = this.$refs._add(file.url);
236236+ $refAdded.pathType = type;
237237+ try {
238238+ const resolver = type === 'file' ? fileResolver : urlResolver;
239239+ await resolver.handler({
240240+ arrayBuffer,
241241+ fetch,
242242+ file,
243243+ });
244244+ const parseResult = await parseFile(file, this.options);
245245+ $refAdded.value = parseResult.result;
246246+ schema = parseResult.result;
247247+ } catch (err) {
248248+ if (isHandledError(err)) {
249249+ $refAdded.value = err;
250250+ }
251251+252252+ throw err;
253253+ }
254254+ }
255255+256256+ if (schema === null || typeof schema !== 'object' || Buffer.isBuffer(schema)) {
257257+ throw ono.syntax(`"${this.$refs._root$Ref.path || schema}" is not a valid JSON Schema`);
258258+ }
259259+260260+ this.schema = schema;
261261+262262+ return {
263263+ schema,
264264+ };
265265+ }
266266+267267+ private async parseMany({
268268+ arrayBuffer,
269269+ fetch,
270270+ pathOrUrlOrSchemas,
271271+ resolvedInputs: _resolvedInputs,
272272+ }: {
273273+ arrayBuffer?: ArrayBuffer[];
274274+ fetch?: RequestInit;
275275+ pathOrUrlOrSchemas: Array<JSONSchema | string | unknown>;
276276+ resolvedInputs?: ResolvedInput[];
277277+ }): Promise<{ schemaMany: JSONSchema[] }> {
278278+ const resolvedInputs = [...(_resolvedInputs || [])];
279279+ resolvedInputs.push(
280280+ ...(pathOrUrlOrSchemas.map((schema) => getResolvedInput({ pathOrUrlOrSchema: schema })) ||
281281+ []),
282282+ );
283283+284284+ this.schemaMany = [];
285285+ this.schemaManySources = [];
286286+ this.sourcePathToPrefix = new Map();
287287+288288+ for (let i = 0; i < resolvedInputs.length; i++) {
289289+ const resolvedInput = resolvedInputs[i]!;
290290+ const { path, type } = resolvedInput;
291291+ let { schema } = resolvedInput;
292292+293293+ if (schema) {
294294+ // keep schema as-is
295295+ } else if (type !== 'json') {
296296+ const file = newFile(path);
297297+298298+ // Add a new $Ref for this file, even though we don't have the value yet.
299299+ // This ensures that we don't simultaneously read & parse the same file multiple times
300300+ const $refAdded = this.$refs._add(file.url);
301301+ $refAdded.pathType = type;
302302+ try {
303303+ const resolver = type === 'file' ? fileResolver : urlResolver;
304304+ await resolver.handler({
305305+ arrayBuffer: arrayBuffer?.[i],
306306+ fetch,
307307+ file,
308308+ });
309309+ const parseResult = await parseFile(file, this.options);
310310+ $refAdded.value = parseResult.result;
311311+ schema = parseResult.result;
312312+ } catch (err) {
313313+ if (isHandledError(err)) {
314314+ $refAdded.value = err;
315315+ }
316316+317317+ throw err;
318318+ }
319319+ }
320320+321321+ if (schema === null || typeof schema !== 'object' || Buffer.isBuffer(schema)) {
322322+ throw ono.syntax(`"${this.$refs._root$Ref.path || schema}" is not a valid JSON Schema`);
323323+ }
324324+325325+ this.schemaMany.push(schema);
326326+ this.schemaManySources.push(path && path.length ? path : url.cwd());
327327+ }
328328+329329+ return {
330330+ schemaMany: this.schemaMany,
331331+ };
332332+ }
333333+334334+ public mergeMany(): JSONSchema {
335335+ const schemas = this.schemaMany || [];
336336+ if (schemas.length === 0) {
337337+ throw ono('mergeMany called with no schemas. Did you run parseMany?');
338338+ }
339339+340340+ const merged: any = {};
341341+342342+ // Determine spec version: prefer first occurrence of openapi, else swagger
343343+ let chosenOpenapi: string | undefined;
344344+ let chosenSwagger: string | undefined;
345345+ for (const s of schemas) {
346346+ if (!chosenOpenapi && s && typeof (s as any).openapi === 'string') {
347347+ chosenOpenapi = (s as any).openapi;
348348+ }
349349+ if (!chosenSwagger && s && typeof (s as any).swagger === 'string') {
350350+ chosenSwagger = (s as any).swagger;
351351+ }
352352+ if (chosenOpenapi && chosenSwagger) {
353353+ break;
354354+ }
355355+ }
356356+ if (typeof chosenOpenapi === 'string') {
357357+ merged.openapi = chosenOpenapi;
358358+ } else if (typeof chosenSwagger === 'string') {
359359+ merged.swagger = chosenSwagger;
360360+ }
361361+362362+ // Merge info: take first non-empty per-field across inputs
363363+ const infoAccumulator: any = {};
364364+ for (const s of schemas) {
365365+ const info = (s as any)?.info;
366366+ if (info && typeof info === 'object') {
367367+ for (const [k, v] of Object.entries(info)) {
368368+ if (infoAccumulator[k] === undefined && v !== undefined) {
369369+ infoAccumulator[k] = JSON.parse(JSON.stringify(v));
370370+ }
371371+ }
372372+ }
373373+ }
374374+ if (Object.keys(infoAccumulator).length > 0) {
375375+ merged.info = infoAccumulator;
376376+ }
377377+378378+ // Merge servers: union by url+description
379379+ const servers: any[] = [];
380380+ const seenServers = new Set<string>();
381381+ for (const s of schemas) {
382382+ const arr = (s as any)?.servers;
383383+ if (Array.isArray(arr)) {
384384+ for (const srv of arr) {
385385+ if (srv && typeof srv === 'object') {
386386+ const key = `${srv.url || ''}|${srv.description || ''}`;
387387+ if (!seenServers.has(key)) {
388388+ seenServers.add(key);
389389+ servers.push(JSON.parse(JSON.stringify(srv)));
390390+ }
391391+ }
392392+ }
393393+ }
394394+ }
395395+ if (servers.length > 0) {
396396+ merged.servers = servers;
397397+ }
398398+399399+ merged.paths = {};
400400+ merged.components = {};
401401+402402+ const componentSections = [
403403+ 'schemas',
404404+ 'parameters',
405405+ 'requestBodies',
406406+ 'responses',
407407+ 'headers',
408408+ 'securitySchemes',
409409+ 'examples',
410410+ 'links',
411411+ 'callbacks',
412412+ ];
413413+ for (const sec of componentSections) {
414414+ merged.components[sec] = {};
415415+ }
416416+417417+ const tagNameSet = new Set<string>();
418418+ const tags: any[] = [];
419419+ const usedOpIds = new Set<string>();
420420+421421+ const baseName = (p: string) => {
422422+ try {
423423+ const withoutHash = p.split('#')[0]!;
424424+ const parts = withoutHash.split('/');
425425+ const filename = parts[parts.length - 1] || 'schema';
426426+ const dot = filename.lastIndexOf('.');
427427+ const raw = dot > 0 ? filename.substring(0, dot) : filename;
428428+ return raw.replace(/[^A-Za-z0-9_-]/g, '_');
429429+ } catch {
430430+ return 'schema';
431431+ }
432432+ };
433433+ const unique = (set: Set<string>, proposed: string) => {
434434+ let name = proposed;
435435+ let i = 2;
436436+ while (set.has(name)) {
437437+ name = `${proposed}_${i++}`;
438438+ }
439439+ set.add(name);
440440+ return name;
441441+ };
442442+443443+ const rewriteRef = (ref: string, refMap: Map<string, string>): string => {
444444+ // OAS3: #/components/{section}/{name}...
445445+ let m = ref.match(/^#\/components\/([^/]+)\/([^/]+)(.*)$/);
446446+ if (m) {
447447+ const base = `#/components/${m[1]}/${m[2]}`;
448448+ const mapped = refMap.get(base);
449449+ if (mapped) {
450450+ return mapped + (m[3] || '');
451451+ }
452452+ }
453453+ // OAS2: #/definitions/{name}...
454454+ m = ref.match(/^#\/definitions\/([^/]+)(.*)$/);
455455+ if (m) {
456456+ const base = `#/components/schemas/${m[1]}`;
457457+ const mapped = refMap.get(base);
458458+ if (mapped) {
459459+ // map definitions -> components/schemas
460460+ return mapped + (m[2] || '');
461461+ }
462462+ }
463463+ return ref;
464464+ };
465465+466466+ const cloneAndRewrite = (
467467+ obj: any,
468468+ refMap: Map<string, string>,
469469+ tagMap: Map<string, string>,
470470+ opIdPrefix: string,
471471+ basePath: string,
472472+ ): any => {
473473+ if (obj === null || obj === undefined) {
474474+ return obj;
475475+ }
476476+ if (Array.isArray(obj)) {
477477+ return obj.map((v) => cloneAndRewrite(v, refMap, tagMap, opIdPrefix, basePath));
478478+ }
479479+ if (typeof obj !== 'object') {
480480+ return obj;
481481+ }
482482+483483+ const out: any = {};
484484+ for (const [k, v] of Object.entries(obj)) {
485485+ if (k === '$ref' && typeof v === 'string') {
486486+ const s = v as string;
487487+ if (s.startsWith('#')) {
488488+ out[k] = rewriteRef(s, refMap);
489489+ } else {
490490+ const proto = url.getProtocol(s);
491491+ if (proto === undefined) {
492492+ // relative external ref -> absolutize against source base path
493493+ out[k] = url.resolve(basePath + '#', s);
494494+ } else {
495495+ out[k] = s;
496496+ }
497497+ }
498498+ } else if (k === 'tags' && Array.isArray(v) && v.every((x) => typeof x === 'string')) {
499499+ out[k] = v.map((t) => tagMap.get(t) || t);
500500+ } else if (k === 'operationId' && typeof v === 'string') {
501501+ out[k] = unique(usedOpIds, `${opIdPrefix}_${v}`);
502502+ } else {
503503+ out[k] = cloneAndRewrite(v as any, refMap, tagMap, opIdPrefix, basePath);
504504+ }
505505+ }
506506+ return out;
507507+ };
508508+509509+ for (let i = 0; i < schemas.length; i++) {
510510+ const schema: any = schemas[i] || {};
511511+ const sourcePath = this.schemaManySources[i] || `multi://input/${i + 1}`;
512512+ const prefix = baseName(sourcePath);
513513+514514+ // Track prefix for this source path (strip hash). Only map real file/http paths
515515+ const withoutHash = url.stripHash(sourcePath);
516516+ const protocol = url.getProtocol(withoutHash);
517517+ if (
518518+ protocol === undefined ||
519519+ protocol === 'file' ||
520520+ protocol === 'http' ||
521521+ protocol === 'https'
522522+ ) {
523523+ this.sourcePathToPrefix.set(withoutHash, prefix);
524524+ }
525525+526526+ const refMap = new Map<string, string>();
527527+ const tagMap = new Map<string, string>();
528528+529529+ const srcComponents = (schema.components || {}) as any;
530530+ for (const sec of componentSections) {
531531+ const group = srcComponents[sec] || {};
532532+ for (const [name] of Object.entries(group)) {
533533+ const newName = `${prefix}_${name}`;
534534+ refMap.set(`#/components/${sec}/${name}`, `#/components/${sec}/${newName}`);
535535+ }
536536+ }
537537+538538+ const srcTags: any[] = Array.isArray(schema.tags) ? schema.tags : [];
539539+ for (const t of srcTags) {
540540+ if (!t || typeof t !== 'object' || typeof t.name !== 'string') {
541541+ continue;
542542+ }
543543+ const desired = t.name;
544544+ const finalName = tagNameSet.has(desired) ? `${prefix}_${desired}` : desired;
545545+ tagNameSet.add(finalName);
546546+ tagMap.set(desired, finalName);
547547+ if (!tags.find((x) => x && x.name === finalName)) {
548548+ tags.push({ ...t, name: finalName });
549549+ }
550550+ }
551551+552552+ for (const sec of componentSections) {
553553+ const group = (schema.components && schema.components[sec]) || {};
554554+ for (const [name, val] of Object.entries(group)) {
555555+ const newName = `${prefix}_${name}`;
556556+ merged.components[sec][newName] = cloneAndRewrite(
557557+ val,
558558+ refMap,
559559+ tagMap,
560560+ prefix,
561561+ url.stripHash(sourcePath),
562562+ );
563563+ }
564564+ }
565565+566566+ const srcPaths = (schema.paths || {}) as Record<string, any>;
567567+ for (const [p, item] of Object.entries(srcPaths)) {
568568+ let targetPath = p;
569569+ if (merged.paths[p]) {
570570+ const trimmed = p.startsWith('/') ? p.substring(1) : p;
571571+ targetPath = `/${prefix}/${trimmed}`;
572572+ }
573573+ merged.paths[targetPath] = cloneAndRewrite(
574574+ item,
575575+ refMap,
576576+ tagMap,
577577+ prefix,
578578+ url.stripHash(sourcePath),
579579+ );
580580+ }
581581+ }
582582+583583+ if (tags.length > 0) {
584584+ merged.tags = tags;
585585+ }
586586+587587+ // Rebuild $refs root using the first input's path to preserve external resolution semantics
588588+ const rootPath = this.schemaManySources[0] || url.cwd();
589589+ this.$refs = new $Refs();
590590+ const rootRef = this.$refs._add(rootPath);
591591+ rootRef.pathType = url.isFileSystemPath(rootPath) ? 'file' : 'http';
592592+ rootRef.value = merged;
593593+ this.schema = merged;
594594+ return merged as JSONSchema;
595595+ }
596596+}
597597+598598+export { sendRequest } from './resolvers/url';
599599+export type { JSONSchema } from './types';
+112
packages/json-schema-ref-parser/src/options.ts
···11+import { binaryParser } from './parsers/binary';
22+import { jsonParser } from './parsers/json';
33+import { textParser } from './parsers/text';
44+import { yamlParser } from './parsers/yaml';
55+import type { JSONSchemaObject, Plugin } from './types';
66+77+export interface DereferenceOptions {
88+ /**
99+ * Determines whether circular `$ref` pointers are handled.
1010+ *
1111+ * If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.
1212+ *
1313+ * 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`.
1414+ */
1515+ circular?: boolean | 'ignore';
1616+ /**
1717+ * A function, called for each path, which can return true to stop this path and all
1818+ * subpaths from being dereferenced further. This is useful in schemas where some
1919+ * subpaths contain literal $ref keys that should not be dereferenced.
2020+ */
2121+ excludedPathMatcher?(path: string): boolean;
2222+ /**
2323+ * Callback invoked during dereferencing.
2424+ *
2525+ * @argument {string} path - The path being dereferenced (ie. the `$ref` string)
2626+ * @argument {JSONSchemaObject} value - The JSON-Schema that the `$ref` resolved to
2727+ * @argument {JSONSchemaObject} parent - The parent of the dereferenced object
2828+ * @argument {string} parentPropName - The prop name of the parent object whose value was dereferenced
2929+ */
3030+ onDereference?(
3131+ path: string,
3232+ value: JSONSchemaObject,
3333+ parent?: JSONSchemaObject,
3434+ parentPropName?: string,
3535+ ): void;
3636+}
3737+3838+/**
3939+ * Options that determine how JSON schemas are parsed, resolved, and dereferenced.
4040+ *
4141+ * @param [options] - Overridden options
4242+ * @class
4343+ */
4444+export interface $RefParserOptions {
4545+ /**
4646+ * The `dereference` options control how JSON Schema `$Ref` Parser will dereference `$ref` pointers within the JSON schema.
4747+ */
4848+ dereference: DereferenceOptions;
4949+ /**
5050+ * The `parse` options determine how different types of files will be parsed.
5151+ *
5252+ * 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.
5353+ */
5454+ parse: {
5555+ binary: Plugin;
5656+ json: Plugin;
5757+ text: Plugin;
5858+ yaml: Plugin;
5959+ };
6060+ /**
6161+ * The maximum amount of time (in milliseconds) that JSON Schema $Ref Parser will spend dereferencing a single schema.
6262+ * It will throw a timeout error if the operation takes longer than this.
6363+ */
6464+ timeoutMs?: number;
6565+}
6666+6767+export const getJsonSchemaRefParserDefaultOptions = (): $RefParserOptions => ({
6868+ /**
6969+ * Determines the types of JSON references that are allowed.
7070+ */
7171+ dereference: {
7272+ /**
7373+ * Dereference circular (recursive) JSON references?
7474+ * If false, then a {@link ReferenceError} will be thrown if a circular reference is found.
7575+ * If "ignore", then circular references will not be dereferenced.
7676+ *
7777+ * @type {boolean|string}
7878+ */
7979+ circular: true,
8080+ /**
8181+ * A function, called for each path, which can return true to stop this path and all
8282+ * subpaths from being dereferenced further. This is useful in schemas where some
8383+ * subpaths contain literal $ref keys that should not be dereferenced.
8484+ *
8585+ * @type {function}
8686+ */
8787+ excludedPathMatcher: () => false,
8888+ // @ts-expect-error
8989+ referenceResolution: 'relative',
9090+ },
9191+ /**
9292+ * Determines how different types of files will be parsed.
9393+ *
9494+ * You can add additional parsers of your own, replace an existing one with
9595+ * your own implementation, or disable any parser by setting it to false.
9696+ */
9797+ parse: {
9898+ binary: { ...binaryParser },
9999+ json: { ...jsonParser },
100100+ text: { ...textParser },
101101+ yaml: { ...yamlParser },
102102+ },
103103+});
104104+105105+export type Options = $RefParserOptions;
106106+107107+type DeepPartial<T> = T extends object
108108+ ? {
109109+ [P in keyof T]?: DeepPartial<T[P]>;
110110+ }
111111+ : T;
112112+export type ParserOptions = DeepPartial<$RefParserOptions>;
+65
packages/json-schema-ref-parser/src/parse.ts
···11+import { ono } from '@jsdevtools/ono';
22+33+import type { $RefParserOptions } from './options';
44+import type { FileInfo } from './types';
55+import { ParserError } from './util/errors';
66+import type { PluginResult } from './util/plugins';
77+import * as plugins from './util/plugins';
88+import { getExtension } from './util/url';
99+1010+/**
1111+ * Prepares the file object so we can populate it with data and other values
1212+ * when it's read and parsed. This "file object" will be passed to all
1313+ * resolvers and parsers.
1414+ */
1515+export function newFile(path: string): FileInfo {
1616+ let url = path;
1717+ // Remove the URL fragment, if any
1818+ const hashIndex = url.indexOf('#');
1919+ let hash = '';
2020+ if (hashIndex > -1) {
2121+ hash = url.substring(hashIndex);
2222+ url = url.substring(0, hashIndex);
2323+ }
2424+ return {
2525+ extension: getExtension(url),
2626+ hash,
2727+ url,
2828+ } as FileInfo;
2929+}
3030+3131+/**
3232+ * Parses the given file's contents, using the configured parser plugins.
3333+ */
3434+export const parseFile = async (
3535+ file: FileInfo,
3636+ options: $RefParserOptions,
3737+): Promise<PluginResult> => {
3838+ try {
3939+ // If none of the parsers are a match for this file, try all of them. This
4040+ // handles situations where the file is a supported type, just with an
4141+ // unknown extension.
4242+ const parsers = [
4343+ options.parse.json,
4444+ options.parse.yaml,
4545+ options.parse.text,
4646+ options.parse.binary,
4747+ ];
4848+ const filtered = parsers.filter((plugin) => plugin.canHandle(file));
4949+ return await plugins.run(filtered.length ? filtered : parsers, file);
5050+ } catch (error: any) {
5151+ if (error && error.message && error.message.startsWith('Error parsing')) {
5252+ throw error;
5353+ }
5454+5555+ if (!error || !('error' in error)) {
5656+ throw ono.syntax(`Unable to parse ${file.url}`);
5757+ }
5858+5959+ if (error.error instanceof ParserError) {
6060+ throw error.error;
6161+ }
6262+6363+ throw new ParserError(error.error.message, file.url);
6464+ }
6565+};
···11+import yaml from 'js-yaml';
22+import { JSON_SCHEMA } from 'js-yaml';
33+44+import type { FileInfo, JSONSchema, Plugin } from '../types';
55+import { ParserError } from '../util/errors';
66+77+export const yamlParser: Plugin = {
88+ // JSON is valid YAML
99+ canHandle: (file: FileInfo) => ['.yaml', '.yml', '.json'].includes(file.extension),
1010+ handler: async (file: FileInfo): Promise<JSONSchema> => {
1111+ const data = Buffer.isBuffer(file.data) ? file.data.toString() : file.data;
1212+1313+ if (typeof data !== 'string') {
1414+ // data is already a JavaScript value (object, array, number, null, NaN, etc.)
1515+ return data;
1616+ }
1717+1818+ try {
1919+ const yamlSchema = yaml.load(data, { schema: JSON_SCHEMA }) as JSONSchema;
2020+ return yamlSchema;
2121+ } catch (error: any) {
2222+ throw new ParserError(error?.message || 'Parser Error', file.url);
2323+ }
2424+ },
2525+ name: 'yaml',
2626+};
+352
packages/json-schema-ref-parser/src/pointer.ts
···11+import type { ParserOptions } from './options';
22+import $Ref from './ref';
33+import type { JSONSchema } from './types';
44+import {
55+ InvalidPointerError,
66+ isHandledError,
77+ JSONParserError,
88+ MissingPointerError,
99+} from './util/errors';
1010+import * as url from './util/url';
1111+1212+const slashes = /\//g;
1313+const tildes = /~/g;
1414+const escapedSlash = /~1/g;
1515+const escapedTilde = /~0/g;
1616+1717+const safeDecodeURIComponent = (encodedURIComponent: string): string => {
1818+ try {
1919+ return decodeURIComponent(encodedURIComponent);
2020+ } catch {
2121+ return encodedURIComponent;
2222+ }
2323+};
2424+2525+/**
2626+ * This class represents a single JSON pointer and its resolved value.
2727+ *
2828+ * @param $ref
2929+ * @param path
3030+ * @param [friendlyPath] - The original user-specified path (used for error messages)
3131+ * @class
3232+ */
3333+class Pointer<S extends object = JSONSchema> {
3434+ /**
3535+ * The {@link $Ref} object that contains this {@link Pointer} object.
3636+ */
3737+ $ref: $Ref<S>;
3838+3939+ /**
4040+ * The file path or URL, containing the JSON pointer in the hash.
4141+ * This path is relative to the path of the main JSON schema file.
4242+ */
4343+ path: string;
4444+4545+ /**
4646+ * The original path or URL, used for error messages.
4747+ */
4848+ originalPath: string;
4949+5050+ /**
5151+ * The value of the JSON pointer.
5252+ * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
5353+ */
5454+5555+ value: any;
5656+ /**
5757+ * Indicates whether the pointer references itself.
5858+ */
5959+ circular: boolean;
6060+ /**
6161+ * The number of indirect references that were traversed to resolve the value.
6262+ * Resolving a single pointer may require resolving multiple $Refs.
6363+ */
6464+ indirections: number;
6565+6666+ constructor($ref: $Ref<S>, path: string, friendlyPath?: string) {
6767+ this.$ref = $ref;
6868+6969+ this.path = path;
7070+7171+ this.originalPath = friendlyPath || path;
7272+7373+ this.value = undefined;
7474+7575+ this.circular = false;
7676+7777+ this.indirections = 0;
7878+ }
7979+8080+ /**
8181+ * Resolves the value of a nested property within the given object.
8282+ *
8383+ * @param obj - The object that will be crawled
8484+ * @param options
8585+ * @param pathFromRoot - the path of place that initiated resolving
8686+ *
8787+ * @returns
8888+ * Returns a JSON pointer whose {@link Pointer#value} is the resolved value.
8989+ * If resolving this value required resolving other JSON references, then
9090+ * the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
9191+ * of the resolved value.
9292+ */
9393+ resolve(obj: S, options?: ParserOptions, pathFromRoot?: string) {
9494+ const tokens = Pointer.parse(this.path, this.originalPath);
9595+9696+ // Crawl the object, one token at a time
9797+ this.value = unwrapOrThrow(obj);
9898+9999+ const errors: MissingPointerError[] = [];
100100+101101+ for (let i = 0; i < tokens.length; i++) {
102102+ if (resolveIf$Ref(this, options, pathFromRoot)) {
103103+ // The $ref path has changed, so append the remaining tokens to the path
104104+ this.path = Pointer.join(this.path, tokens.slice(i));
105105+ }
106106+107107+ if (
108108+ typeof this.value === 'object' &&
109109+ this.value !== null &&
110110+ !isRootPath(pathFromRoot) &&
111111+ '$ref' in this.value
112112+ ) {
113113+ return this;
114114+ }
115115+116116+ const token = tokens[i]!;
117117+ if (
118118+ this.value[token] === undefined ||
119119+ (this.value[token] === null && i === tokens.length - 1)
120120+ ) {
121121+ // 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
122122+ let didFindSubstringSlashMatch = false;
123123+ for (let j = tokens.length - 1; j > i; j--) {
124124+ const joinedToken = tokens.slice(i, j + 1).join('/');
125125+ if (this.value[joinedToken] !== undefined) {
126126+ this.value = this.value[joinedToken];
127127+ i = j;
128128+ didFindSubstringSlashMatch = true;
129129+ break;
130130+ }
131131+ }
132132+ if (didFindSubstringSlashMatch) {
133133+ continue;
134134+ }
135135+136136+ this.value = null;
137137+ errors.push(new MissingPointerError(token, decodeURI(this.originalPath)));
138138+ } else {
139139+ this.value = this.value[token];
140140+ }
141141+ }
142142+143143+ if (errors.length > 0) {
144144+ throw errors.length === 1
145145+ ? errors[0]
146146+ : new AggregateError(errors, 'Multiple missing pointer errors');
147147+ }
148148+149149+ // Resolve the final value
150150+ if (
151151+ !this.value ||
152152+ (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)
153153+ ) {
154154+ resolveIf$Ref(this, options, pathFromRoot);
155155+ }
156156+157157+ return this;
158158+ }
159159+160160+ /**
161161+ * Sets the value of a nested property within the given object.
162162+ *
163163+ * @param obj - The object that will be crawled
164164+ * @param value - the value to assign
165165+ * @param options
166166+ *
167167+ * @returns
168168+ * Returns the modified object, or an entirely new object if the entire object is overwritten.
169169+ */
170170+ set(obj: S, value: any, options?: ParserOptions) {
171171+ const tokens = Pointer.parse(this.path);
172172+ let token;
173173+174174+ if (tokens.length === 0) {
175175+ // There are no tokens, replace the entire object with the new value
176176+ this.value = value;
177177+ return value;
178178+ }
179179+180180+ // Crawl the object, one token at a time
181181+ this.value = unwrapOrThrow(obj);
182182+183183+ for (let i = 0; i < tokens.length - 1; i++) {
184184+ resolveIf$Ref(this, options);
185185+186186+ token = tokens[i]!;
187187+ if (this.value && this.value[token] !== undefined) {
188188+ // The token exists
189189+ this.value = this.value[token];
190190+ } else {
191191+ // The token doesn't exist, so create it
192192+ this.value = setValue(this, token, {});
193193+ }
194194+ }
195195+196196+ // Set the value of the final token
197197+ resolveIf$Ref(this, options);
198198+ token = tokens[tokens.length - 1];
199199+ setValue(this, token, value);
200200+201201+ // Return the updated object
202202+ return obj;
203203+ }
204204+205205+ /**
206206+ * Parses a JSON pointer (or a path containing a JSON pointer in the hash)
207207+ * and returns an array of the pointer's tokens.
208208+ * (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
209209+ *
210210+ * The pointer is parsed according to RFC 6901
211211+ * {@link https://tools.ietf.org/html/rfc6901#section-3}
212212+ *
213213+ * @param path
214214+ * @param [originalPath]
215215+ * @returns
216216+ */
217217+ static parse(path: string, originalPath?: string): string[] {
218218+ // Get the JSON pointer from the path's hash
219219+ const pointer = url.getHash(path).substring(1);
220220+221221+ // If there's no pointer, then there are no tokens,
222222+ // so return an empty array
223223+ if (!pointer) {
224224+ return [];
225225+ }
226226+227227+ // Split into an array
228228+ const split = pointer.split('/');
229229+230230+ // Decode each part, according to RFC 6901
231231+ for (let i = 0; i < split.length; i++) {
232232+ split[i] = safeDecodeURIComponent(
233233+ split[i]!.replace(escapedSlash, '/').replace(escapedTilde, '~'),
234234+ );
235235+ }
236236+237237+ if (split[0] !== '') {
238238+ throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath);
239239+ }
240240+241241+ return split.slice(1);
242242+ }
243243+244244+ /**
245245+ * Creates a JSON pointer path, by joining one or more tokens to a base path.
246246+ *
247247+ * @param base - The base path (e.g. "schema.json#/definitions/person")
248248+ * @param tokens - The token(s) to append (e.g. ["name", "first"])
249249+ * @returns
250250+ */
251251+ static join(base: string, tokens: string | string[]) {
252252+ // Ensure that the base path contains a hash
253253+ if (base.indexOf('#') === -1) {
254254+ base += '#';
255255+ }
256256+257257+ // Append each token to the base path
258258+ tokens = Array.isArray(tokens) ? tokens : [tokens];
259259+ for (let i = 0; i < tokens.length; i++) {
260260+ const token = tokens[i]!;
261261+ // Encode the token, according to RFC 6901
262262+ base += '/' + encodeURIComponent(token.replace(tildes, '~0').replace(slashes, '~1'));
263263+ }
264264+265265+ return base;
266266+ }
267267+}
268268+269269+/**
270270+ * If the given pointer's {@link Pointer#value} is a JSON reference,
271271+ * then the reference is resolved and {@link Pointer#value} is replaced with the resolved value.
272272+ * In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the
273273+ * resolution path of the new value.
274274+ *
275275+ * @param pointer
276276+ * @param options
277277+ * @param [pathFromRoot] - the path of place that initiated resolving
278278+ * @returns - Returns `true` if the resolution path changed
279279+ */
280280+function resolveIf$Ref(pointer: any, options: any, pathFromRoot?: any) {
281281+ // Is the value a JSON reference? (and allowed?)
282282+283283+ if ($Ref.isAllowed$Ref(pointer.value)) {
284284+ const $refPath = url.resolve(pointer.path, pointer.value.$ref);
285285+286286+ if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
287287+ // The value is a reference to itself, so there's nothing to do.
288288+ pointer.circular = true;
289289+ } else {
290290+ const resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options);
291291+ if (resolved === null) {
292292+ return false;
293293+ }
294294+295295+ pointer.indirections += resolved.indirections + 1;
296296+297297+ if ($Ref.isExtended$Ref(pointer.value)) {
298298+ // This JSON reference "extends" the resolved value, rather than simply pointing to it.
299299+ // So the resolved path does NOT change. Just the value does.
300300+ pointer.value = $Ref.dereference(pointer.value, resolved.value);
301301+ return false;
302302+ } else {
303303+ // Resolve the reference
304304+ pointer.$ref = resolved.$ref;
305305+ pointer.path = resolved.path;
306306+ pointer.value = resolved.value;
307307+ }
308308+309309+ return true;
310310+ }
311311+ }
312312+ return undefined;
313313+}
314314+export default Pointer;
315315+316316+/**
317317+ * Sets the specified token value of the {@link Pointer#value}.
318318+ *
319319+ * The token is evaluated according to RFC 6901.
320320+ * {@link https://tools.ietf.org/html/rfc6901#section-4}
321321+ *
322322+ * @param pointer - The JSON Pointer whose value will be modified
323323+ * @param token - A JSON Pointer token that indicates how to modify `obj`
324324+ * @param value - The value to assign
325325+ * @returns - Returns the assigned value
326326+ */
327327+function setValue(pointer: any, token: any, value: any) {
328328+ if (pointer.value && typeof pointer.value === 'object') {
329329+ if (token === '-' && Array.isArray(pointer.value)) {
330330+ pointer.value.push(value);
331331+ } else {
332332+ pointer.value[token] = value;
333333+ }
334334+ } else {
335335+ throw new JSONParserError(
336336+ `Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`,
337337+ );
338338+ }
339339+ return value;
340340+}
341341+342342+function unwrapOrThrow(value: any) {
343343+ if (isHandledError(value)) {
344344+ throw value;
345345+ }
346346+347347+ return value;
348348+}
349349+350350+function isRootPath(pathFromRoot: any): boolean {
351351+ return typeof pathFromRoot == 'string' && Pointer.parse(pathFromRoot).length == 0;
352352+}
+283
packages/json-schema-ref-parser/src/ref.ts
···11+import type { ParserOptions } from './options';
22+import Pointer from './pointer';
33+import type $Refs from './refs';
44+import type { JSONSchema } from './types';
55+import type {
66+ JSONParserError,
77+ MissingPointerError,
88+ ParserError,
99+ ResolverError,
1010+} from './util/errors';
1111+import { normalizeError } from './util/errors';
1212+1313+export type $RefError = JSONParserError | ResolverError | ParserError | MissingPointerError;
1414+1515+/**
1616+ * This class represents a single JSON reference and its resolved value.
1717+ *
1818+ * @class
1919+ */
2020+class $Ref<S extends object = JSONSchema> {
2121+ /**
2222+ * The file path or URL of the referenced file.
2323+ * This path is relative to the path of the main JSON schema file.
2424+ *
2525+ * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file.
2626+ * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get
2727+ * specific JSON pointers within the file.
2828+ *
2929+ * @type {string}
3030+ */
3131+ path: undefined | string;
3232+3333+ /**
3434+ * The resolved value of the JSON reference.
3535+ * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
3636+ *
3737+ * @type {?*}
3838+ */
3939+ value: any;
4040+4141+ /**
4242+ * The {@link $Refs} object that contains this {@link $Ref} object.
4343+ *
4444+ * @type {$Refs}
4545+ */
4646+ $refs: $Refs<S>;
4747+4848+ /**
4949+ * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
5050+ */
5151+ pathType: string | unknown;
5252+5353+ /**
5454+ * List of all errors. Undefined if no errors.
5555+ */
5656+ errors: Array<$RefError> = [];
5757+5858+ constructor($refs: $Refs<S>) {
5959+ this.$refs = $refs;
6060+ }
6161+6262+ /**
6363+ * Pushes an error to errors array.
6464+ *
6565+ * @param err - The error to be pushed
6666+ * @returns
6767+ */
6868+ addError(err: $RefError) {
6969+ if (this.errors === undefined) {
7070+ this.errors = [];
7171+ }
7272+7373+ const existingErrors = this.errors.map(({ footprint }: any) => footprint);
7474+7575+ // the path has been almost certainly set at this point,
7676+ // but just in case something went wrong, normalizeError injects path if necessary
7777+ // moreover, certain errors might point at the same spot, so filter them out to reduce noise
7878+ if ('errors' in err && Array.isArray(err.errors)) {
7979+ this.errors.push(
8080+ ...err.errors
8181+ .map(normalizeError)
8282+ .filter(({ footprint }: any) => !existingErrors.includes(footprint)),
8383+ );
8484+ } else if (!('footprint' in err) || !existingErrors.includes(err.footprint)) {
8585+ this.errors.push(normalizeError(err));
8686+ }
8787+ }
8888+8989+ /**
9090+ * Determines whether the given JSON reference exists within this {@link $Ref#value}.
9191+ *
9292+ * @param path - The full path being resolved, optionally with a JSON pointer in the hash
9393+ * @param options
9494+ * @returns
9595+ */
9696+ exists(path: string, options?: ParserOptions) {
9797+ try {
9898+ this.resolve(path, options);
9999+ return true;
100100+ } catch {
101101+ return false;
102102+ }
103103+ }
104104+105105+ /**
106106+ * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value.
107107+ *
108108+ * @param path - The full path being resolved, optionally with a JSON pointer in the hash
109109+ * @param options
110110+ * @returns - Returns the resolved value
111111+ */
112112+ get(path: string, options?: ParserOptions) {
113113+ return this.resolve(path, options)?.value;
114114+ }
115115+116116+ /**
117117+ * Resolves the given JSON reference within this {@link $Ref#value}.
118118+ *
119119+ * @param path - The full path being resolved, optionally with a JSON pointer in the hash
120120+ * @param options
121121+ * @param friendlyPath - The original user-specified path (used for error messages)
122122+ * @param pathFromRoot - The path of `obj` from the schema root
123123+ * @returns
124124+ */
125125+ resolve(path: string, options?: ParserOptions, friendlyPath?: string, pathFromRoot?: string) {
126126+ const pointer = new Pointer<S>(this, path, friendlyPath);
127127+ return pointer.resolve(this.value, options, pathFromRoot);
128128+ }
129129+130130+ /**
131131+ * Sets the value of a nested property within this {@link $Ref#value}.
132132+ * If the property, or any of its parents don't exist, they will be created.
133133+ *
134134+ * @param path - The full path of the property to set, optionally with a JSON pointer in the hash
135135+ * @param value - The value to assign
136136+ */
137137+ set(path: string, value: any) {
138138+ const pointer = new Pointer(this, path);
139139+ this.value = pointer.set(this.value, value);
140140+ }
141141+142142+ /**
143143+ * Determines whether the given value is a JSON reference.
144144+ *
145145+ * @param value - The value to inspect
146146+ * @returns
147147+ */
148148+ static is$Ref(value: unknown): value is { $ref: string; length?: number } {
149149+ return (
150150+ Boolean(value) &&
151151+ typeof value === 'object' &&
152152+ value !== null &&
153153+ '$ref' in value &&
154154+ typeof value.$ref === 'string' &&
155155+ value.$ref.length > 0
156156+ );
157157+ }
158158+159159+ /**
160160+ * Determines whether the given value is an external JSON reference.
161161+ *
162162+ * @param value - The value to inspect
163163+ * @returns
164164+ */
165165+ static isExternal$Ref(value: unknown): boolean {
166166+ return $Ref.is$Ref(value) && value.$ref![0] !== '#';
167167+ }
168168+169169+ /**
170170+ * Determines whether the given value is a JSON reference, and whether it is allowed by the options.
171171+ *
172172+ * @param value - The value to inspect
173173+ * @param options
174174+ * @returns
175175+ */
176176+ static isAllowed$Ref(value: unknown) {
177177+ if (this.is$Ref(value)) {
178178+ if (value.$ref.substring(0, 2) === '#/' || value.$ref === '#') {
179179+ // It's a JSON Pointer reference, which is always allowed
180180+ return true;
181181+ } else if (value.$ref[0] !== '#') {
182182+ // It's an external reference, which is allowed by the options
183183+ return true;
184184+ }
185185+ }
186186+ return undefined;
187187+ }
188188+189189+ /**
190190+ * Determines whether the given value is a JSON reference that "extends" its resolved value.
191191+ * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
192192+ * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved
193193+ * value, plus the extra properties.
194194+ *
195195+ * @example: {
196196+ person: {
197197+ properties: {
198198+ firstName: { type: string }
199199+ lastName: { type: string }
200200+ }
201201+ }
202202+ employee: {
203203+ properties: {
204204+ $ref: #/person/properties
205205+ salary: { type: number }
206206+ }
207207+ }
208208+ }
209209+ * In this example, "employee" is an extended $ref, since it extends "person" with an additional
210210+ * property (salary). The result is a NEW value that looks like this:
211211+ *
212212+ * {
213213+ * properties: {
214214+ * firstName: { type: string }
215215+ * lastName: { type: string }
216216+ * salary: { type: number }
217217+ * }
218218+ * }
219219+ *
220220+ * @param value - The value to inspect
221221+ * @returns
222222+ */
223223+ static isExtended$Ref(value: unknown) {
224224+ return $Ref.is$Ref(value) && Object.keys(value).length > 1;
225225+ }
226226+227227+ /**
228228+ * Returns the resolved value of a JSON Reference.
229229+ * If necessary, the resolved value is merged with the JSON Reference to create a new object
230230+ *
231231+ * @example: {
232232+ person: {
233233+ properties: {
234234+ firstName: { type: string }
235235+ lastName: { type: string }
236236+ }
237237+ }
238238+ employee: {
239239+ properties: {
240240+ $ref: #/person/properties
241241+ salary: { type: number }
242242+ }
243243+ }
244244+ } When "person" and "employee" are merged, you end up with the following object:
245245+ *
246246+ * {
247247+ * properties: {
248248+ * firstName: { type: string }
249249+ * lastName: { type: string }
250250+ * salary: { type: number }
251251+ * }
252252+ * }
253253+ *
254254+ * @param $ref - The JSON reference object (the one with the "$ref" property)
255255+ * @param resolvedValue - The resolved value, which can be any type
256256+ * @returns - Returns the dereferenced value
257257+ */
258258+ static dereference<S extends object = JSONSchema>($ref: $Ref<S>, resolvedValue: S): S {
259259+ if (resolvedValue && typeof resolvedValue === 'object' && $Ref.isExtended$Ref($ref)) {
260260+ const merged = {};
261261+ for (const key of Object.keys($ref)) {
262262+ if (key !== '$ref') {
263263+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
264264+ merged[key] = $ref[key];
265265+ }
266266+ }
267267+268268+ for (const key of Object.keys(resolvedValue)) {
269269+ if (!(key in merged)) {
270270+ // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
271271+ merged[key] = resolvedValue[key];
272272+ }
273273+ }
274274+275275+ return merged as S;
276276+ } else {
277277+ // Completely replace the original reference with the resolved value
278278+ return resolvedValue;
279279+ }
280280+ }
281281+}
282282+283283+export default $Ref;
+231
packages/json-schema-ref-parser/src/refs.ts
···11+import { ono } from '@jsdevtools/ono';
22+import type { JSONSchema4Type, JSONSchema6Type, JSONSchema7Type } from 'json-schema';
33+44+import type { ParserOptions } from './options';
55+import $Ref from './ref';
66+import type { JSONSchema } from './types';
77+import convertPathToPosix from './util/convert-path-to-posix';
88+import * as url from './util/url';
99+1010+interface $RefsMap<S extends object = JSONSchema> {
1111+ [url: string]: $Ref<S>;
1212+}
1313+/**
1414+ * 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.
1515+ *
1616+ * 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.
1717+ *
1818+ * See https://apitools.dev/json-schema-ref-parser/docs/refs.html
1919+ */
2020+export default class $Refs<S extends object = JSONSchema> {
2121+ /**
2222+ * 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.
2323+ *
2424+ * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#circular
2525+ */
2626+ public circular: boolean;
2727+2828+ /**
2929+ * Returns the paths/URLs of all the files in your schema (including the main schema file).
3030+ *
3131+ * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#pathstypes
3232+ *
3333+ * @param types (optional) Optionally only return certain types of paths ("file", "http", etc.)
3434+ */
3535+ paths(...types: (string | string[])[]): string[] {
3636+ const paths = getPaths(this._$refs, types.flat());
3737+ return paths.map((path) => convertPathToPosix(path.decoded));
3838+ }
3939+4040+ /**
4141+ * Returns a map of paths/URLs and their correspond values.
4242+ *
4343+ * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#valuestypes
4444+ *
4545+ * @param types (optional) Optionally only return values from certain locations ("file", "http", etc.)
4646+ */
4747+ values(...types: (string | string[])[]): S {
4848+ const $refs = this._$refs;
4949+ const paths = getPaths($refs, types.flat());
5050+ return paths.reduce<Record<string, any>>((obj, path) => {
5151+ obj[convertPathToPosix(path.decoded)] = $refs[path.encoded]!.value;
5252+ return obj;
5353+ }, {}) as S;
5454+ }
5555+5656+ /**
5757+ * Returns `true` if the given path exists in the schema; otherwise, returns `false`
5858+ *
5959+ * See https://apitools.dev/json-schema-ref-parser/docs/refs.html#existsref
6060+ *
6161+ * @param $ref The JSON Reference path, optionally with a JSON Pointer in the hash
6262+ */
6363+ /**
6464+ * Determines whether the given JSON reference exists.
6565+ *
6666+ * @param path - The path being resolved, optionally with a JSON pointer in the hash
6767+ * @param [options]
6868+ * @returns
6969+ */
7070+ exists(path: string, options: any) {
7171+ try {
7272+ this._resolve(path, '', options);
7373+ return true;
7474+ } catch {
7575+ return false;
7676+ }
7777+ }
7878+7979+ /**
8080+ * Resolves the given JSON reference and returns the resolved value.
8181+ *
8282+ * @param path - The path being resolved, with a JSON pointer in the hash
8383+ * @param [options]
8484+ * @returns - Returns the resolved value
8585+ */
8686+ get(path: string, options?: ParserOptions): JSONSchema4Type | JSONSchema6Type | JSONSchema7Type {
8787+ return this._resolve(path, '', options)!.value;
8888+ }
8989+9090+ /**
9191+ * 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.
9292+ *
9393+ * @param path The JSON Reference path, optionally with a JSON Pointer in the hash
9494+ * @param value The value to assign. Can be anything (object, string, number, etc.)
9595+ */
9696+ set(path: string, value: JSONSchema4Type | JSONSchema6Type | JSONSchema7Type) {
9797+ const absPath = url.resolve(this._root$Ref.path!, path);
9898+ const withoutHash = url.stripHash(absPath);
9999+ const $ref = this._$refs[withoutHash];
100100+101101+ if (!$ref) {
102102+ throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
103103+ }
104104+105105+ $ref.set(absPath, value);
106106+ }
107107+ /**
108108+ * Returns the specified {@link $Ref} object, or undefined.
109109+ *
110110+ * @param path - The path being resolved, optionally with a JSON pointer in the hash
111111+ * @returns
112112+ * @protected
113113+ */
114114+ _get$Ref(path: string) {
115115+ path = url.resolve(this._root$Ref.path!, path);
116116+ const withoutHash = url.stripHash(path);
117117+ return this._$refs[withoutHash];
118118+ }
119119+120120+ /**
121121+ * Creates a new {@link $Ref} object and adds it to this {@link $Refs} object.
122122+ *
123123+ * @param path - The file path or URL of the referenced file
124124+ */
125125+ _add(path: string) {
126126+ const withoutHash = url.stripHash(path);
127127+128128+ const $ref = new $Ref<S>(this);
129129+ $ref.path = withoutHash;
130130+131131+ this._$refs[withoutHash] = $ref;
132132+ this._root$Ref = this._root$Ref || $ref;
133133+134134+ return $ref;
135135+ }
136136+137137+ /**
138138+ * Resolves the given JSON reference.
139139+ *
140140+ * @param path - The path being resolved, optionally with a JSON pointer in the hash
141141+ * @param pathFromRoot - The path of `obj` from the schema root
142142+ * @param [options]
143143+ * @returns
144144+ * @protected
145145+ */
146146+ _resolve(path: string, pathFromRoot: string, options?: ParserOptions) {
147147+ const absPath = url.resolve(this._root$Ref.path!, path);
148148+ const withoutHash = url.stripHash(absPath);
149149+ const $ref = this._$refs[withoutHash];
150150+151151+ if (!$ref) {
152152+ throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
153153+ }
154154+155155+ return $ref.resolve(absPath, options, path, pathFromRoot);
156156+ }
157157+158158+ /**
159159+ * A map of paths/urls to {@link $Ref} objects
160160+ *
161161+ * @type {object}
162162+ * @protected
163163+ */
164164+ _$refs: $RefsMap<S> = {};
165165+166166+ /**
167167+ * The {@link $Ref} object that is the root of the JSON schema.
168168+ *
169169+ * @type {$Ref}
170170+ * @protected
171171+ */
172172+ _root$Ref: $Ref<S>;
173173+174174+ constructor() {
175175+ /**
176176+ * Indicates whether the schema contains any circular references.
177177+ *
178178+ * @type {boolean}
179179+ */
180180+ this.circular = false;
181181+182182+ this._$refs = {};
183183+184184+ // @ts-ignore
185185+ this._root$Ref = null;
186186+ }
187187+188188+ /**
189189+ * Returns the paths of all the files/URLs that are referenced by the JSON schema,
190190+ * including the schema itself.
191191+ *
192192+ * @param [types] - Only return paths of the given types ("file", "http", etc.)
193193+ * @returns
194194+ */
195195+ /**
196196+ * Returns the map of JSON references and their resolved values.
197197+ *
198198+ * @param [types] - Only return references of the given types ("file", "http", etc.)
199199+ * @returns
200200+ */
201201+202202+ /**
203203+ * Returns a POJO (plain old JavaScript object) for serialization as JSON.
204204+ *
205205+ * @returns {object}
206206+ */
207207+ toJSON = this.values;
208208+}
209209+210210+/**
211211+ * Returns the encoded and decoded paths keys of the given object.
212212+ *
213213+ * @param $refs - The object whose keys are URL-encoded paths
214214+ * @param [types] - Only return paths of the given types ("file", "http", etc.)
215215+ * @returns
216216+ */
217217+function getPaths<S extends object = JSONSchema>($refs: $RefsMap<S>, types: string[]) {
218218+ let paths = Object.keys($refs);
219219+220220+ // Filter the paths by type
221221+ types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types);
222222+ if (types.length > 0 && types[0]) {
223223+ paths = paths.filter((key) => types.includes($refs[key]!.pathType as string));
224224+ }
225225+226226+ // Decode local filesystem paths
227227+ return paths.map((path) => ({
228228+ decoded: $refs[path]!.pathType === 'file' ? url.toFileSystemPath(path, true) : path,
229229+ encoded: path,
230230+ }));
231231+}
···11+import type { $RefParser } from '.';
22+import { getResolvedInput } from '.';
33+import type { $RefParserOptions } from './options';
44+import { newFile, parseFile } from './parse';
55+import Pointer from './pointer';
66+import $Ref from './ref';
77+import type $Refs from './refs';
88+import { fileResolver } from './resolvers/file';
99+import { urlResolver } from './resolvers/url';
1010+import type { JSONSchema } from './types';
1111+import { isHandledError } from './util/errors';
1212+import * as url from './util/url';
1313+1414+/**
1515+ * Crawls the JSON schema, finds all external JSON references, and resolves their values.
1616+ * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
1717+ *
1818+ * NOTE: We only care about EXTERNAL references here. INTERNAL references are only relevant when dereferencing.
1919+ *
2020+ * @returns
2121+ * The promise resolves once all JSON references in the schema have been resolved,
2222+ * including nested references that are contained in externally-referenced files.
2323+ */
2424+export function resolveExternal(parser: $RefParser, options: $RefParserOptions) {
2525+ try {
2626+ // console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
2727+ const promises = crawl(parser.schema, parser.$refs._root$Ref.path + '#', parser.$refs, options);
2828+ return Promise.all(promises);
2929+ } catch (e) {
3030+ return Promise.reject(e);
3131+ }
3232+}
3333+3434+/**
3535+ * Recursively crawls the given value, and resolves any external JSON references.
3636+ *
3737+ * @param obj - The value to crawl. If it's not an object or array, it will be ignored.
3838+ * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash
3939+ * @param {boolean} external - Whether `obj` was found in an external document.
4040+ * @param $refs
4141+ * @param options
4242+ * @param seen - Internal.
4343+ *
4444+ * @returns
4545+ * Returns an array of promises. There will be one promise for each JSON reference in `obj`.
4646+ * If `obj` does not contain any JSON references, then the array will be empty.
4747+ * If any of the JSON references point to files that contain additional JSON references,
4848+ * then the corresponding promise will internally reference an array of promises.
4949+ */
5050+function crawl<S extends object = JSONSchema>(
5151+ obj: string | Buffer | S | undefined | null,
5252+ path: string,
5353+ $refs: $Refs<S>,
5454+ options: $RefParserOptions,
5555+ seen?: Set<any>,
5656+ external?: boolean,
5757+) {
5858+ seen ||= new Set();
5959+ let promises: any = [];
6060+6161+ if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
6262+ seen.add(obj); // Track previously seen objects to avoid infinite recursion
6363+ if ($Ref.isExternal$Ref(obj)) {
6464+ promises.push(resolve$Ref<S>(obj, path, $refs, options));
6565+ }
6666+6767+ const keys = Object.keys(obj) as string[];
6868+ for (const key of keys) {
6969+ const keyPath = Pointer.join(path, key);
7070+ const value = obj[key as keyof typeof obj] as string | JSONSchema | Buffer | undefined;
7171+ promises = promises.concat(crawl(value, keyPath, $refs, options, seen, external));
7272+ }
7373+ }
7474+7575+ return promises;
7676+}
7777+7878+/**
7979+ * Resolves the given JSON Reference, and then crawls the resulting value.
8080+ *
8181+ * @param $ref - The JSON Reference to resolve
8282+ * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash
8383+ * @param $refs
8484+ * @param options
8585+ *
8686+ * @returns
8787+ * The promise resolves once all JSON references in the object have been resolved,
8888+ * including nested references that are contained in externally-referenced files.
8989+ */
9090+async function resolve$Ref<S extends object = JSONSchema>(
9191+ $ref: S,
9292+ path: string,
9393+ $refs: $Refs<S>,
9494+ options: $RefParserOptions,
9595+) {
9696+ const resolvedPath = url.resolve(path, ($ref as JSONSchema).$ref!);
9797+ const withoutHash = url.stripHash(resolvedPath);
9898+9999+ // $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
100100+101101+ // If this ref points back to an input source we've already merged, avoid re-importing
102102+ // by checking if the path (without hash) matches a known source in parser and we can serve it internally later.
103103+ // We keep normal flow but ensure cache hit if already added.
104104+ // Do we already have this $ref?
105105+ const ref = $refs._$refs[withoutHash];
106106+ if (ref) {
107107+ // We've already parsed this $ref, so crawl it to resolve its own externals
108108+ const promises = crawl(ref.value as S, `${withoutHash}#`, $refs, options, new Set(), true);
109109+ return Promise.all(promises);
110110+ }
111111+112112+ // Parse the $referenced file/url
113113+ const file = newFile(resolvedPath);
114114+115115+ // Add a new $Ref for this file, even though we don't have the value yet.
116116+ // This ensures that we don't simultaneously read & parse the same file multiple times
117117+ const $refAdded = $refs._add(file.url);
118118+119119+ try {
120120+ const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: resolvedPath });
121121+122122+ $refAdded.pathType = resolvedInput.type;
123123+124124+ let promises: any = [];
125125+126126+ if (resolvedInput.type !== 'json') {
127127+ const resolver = resolvedInput.type === 'file' ? fileResolver : urlResolver;
128128+ await resolver.handler({ file });
129129+ const parseResult = await parseFile(file, options);
130130+ $refAdded.value = parseResult.result;
131131+ promises = crawl(parseResult.result, `${withoutHash}#`, $refs, options, new Set(), true);
132132+ }
133133+134134+ return Promise.all(promises);
135135+ } catch (err) {
136136+ if (isHandledError(err)) {
137137+ $refAdded.value = err;
138138+ }
139139+140140+ throw err;
141141+ }
142142+}
···11+import type {
22+ JSONSchema4,
33+ JSONSchema4Object,
44+ JSONSchema6,
55+ JSONSchema6Object,
66+ JSONSchema7,
77+ JSONSchema7Object,
88+} from 'json-schema';
99+1010+export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7;
1111+export type JSONSchemaObject = JSONSchema4Object | JSONSchema6Object | JSONSchema7Object;
1212+1313+export interface Plugin {
1414+ /**
1515+ * Can this parser be used to process this file?
1616+ */
1717+ canHandle: (file: FileInfo) => boolean;
1818+ /**
1919+ * 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.
2020+ *
2121+ * 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:
2222+ */
2323+ handler: (
2424+ file: FileInfo,
2525+ ) =>
2626+ | string
2727+ | Buffer
2828+ | JSONSchema
2929+ | Promise<{ data: Buffer }>
3030+ | Promise<string | Buffer | JSONSchema>;
3131+ name: 'binary' | 'file' | 'http' | 'json' | 'text' | 'yaml';
3232+}
3333+3434+/**
3535+ * 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.
3636+ *
3737+ * 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.
3838+ *
3939+ * See https://apitools.dev/json-schema-ref-parser/docs/plugins/file-info-object.html
4040+ */
4141+export interface FileInfo {
4242+ /**
4343+ * The raw file contents, in whatever form they were returned by the resolver that read the file.
4444+ */
4545+ data: string | Buffer;
4646+ /**
4747+ * The lowercase file extension, such as ".json", ".yaml", ".txt", etc.
4848+ */
4949+ extension: string;
5050+ /**
5151+ * 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.
5252+ */
5353+ hash: string;
5454+ /**
5555+ * 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).
5656+ */
5757+ url: string;
5858+}
···11+export default function convertPathToPosix(filePath: string): string {
22+ // Extended-length paths on Windows should not be converted
33+ if (filePath.startsWith('\\\\?\\')) {
44+ return filePath;
55+ }
66+77+ return filePath.replaceAll('\\', '/');
88+}
···11+import type { FileInfo, JSONSchema, Plugin } from '../types';
22+33+export interface PluginResult {
44+ error?: any;
55+ plugin: Pick<Plugin, 'handler'>;
66+ result?: string | Buffer | JSONSchema;
77+}
88+99+/**
1010+ * Runs the specified method of the given plugins, in order, until one of them returns a successful result.
1111+ * Each method can return a synchronous value, a Promise, or call an error-first callback.
1212+ * If the promise resolves successfully, or the callback is called without an error, then the result
1313+ * is immediately returned and no further plugins are called.
1414+ * If the promise rejects, or the callback is called with an error, then the next plugin is called.
1515+ * If ALL plugins fail, then the last error is thrown.
1616+ */
1717+export async function run(plugins: Pick<Plugin, 'handler'>[], file: FileInfo) {
1818+ let index = 0;
1919+ let lastError: PluginResult;
2020+ let plugin: Pick<Plugin, 'handler'>;
2121+2222+ return new Promise<PluginResult>((resolve, reject) => {
2323+ const runNextPlugin = async () => {
2424+ plugin = plugins[index++]!;
2525+2626+ if (!plugin) {
2727+ // there are no more functions, re-throw the last error
2828+ return reject(lastError);
2929+ }
3030+3131+ try {
3232+ const result = await plugin.handler(file);
3333+3434+ if (result !== undefined) {
3535+ return resolve({
3636+ plugin,
3737+ result,
3838+ });
3939+ }
4040+4141+ if (index === plugins.length) {
4242+ throw new Error('No promise has been returned.');
4343+ }
4444+ } catch (e) {
4545+ lastError = {
4646+ error: e,
4747+ plugin,
4848+ };
4949+ runNextPlugin();
5050+ }
5151+ };
5252+5353+ runNextPlugin();
5454+ });
5555+}
+265
packages/json-schema-ref-parser/src/util/url.ts
···11+import path, { join, win32 } from 'node:path';
22+33+import convertPathToPosix from './convert-path-to-posix';
44+import { isWindows } from './is-windows';
55+66+const forwardSlashPattern = /\//g;
77+const protocolPattern = /^(\w{2,}):\/\//i;
88+99+// RegExp patterns to URL-encode special characters in local filesystem paths
1010+const urlEncodePatterns = [
1111+ [/\?/g, '%3F'],
1212+ [/#/g, '%23'],
1313+] as [RegExp, string][];
1414+1515+// RegExp patterns to URL-decode special characters for local filesystem paths
1616+const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'];
1717+1818+/**
1919+ * Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
2020+ *
2121+ * @returns
2222+ */
2323+export function resolve(from: string, to: string) {
2424+ const fromUrl = new URL(convertPathToPosix(from), 'resolve://');
2525+ const resolvedUrl = new URL(convertPathToPosix(to), fromUrl);
2626+ const endSpaces = to.match(/(\s*)$/)?.[1] || '';
2727+ if (resolvedUrl.protocol === 'resolve:') {
2828+ // `from` is a relative URL.
2929+ const { hash, pathname, search } = resolvedUrl;
3030+ return pathname + search + hash + endSpaces;
3131+ }
3232+ return resolvedUrl.toString() + endSpaces;
3333+}
3434+3535+/**
3636+ * Returns the current working directory (in Node) or the current page URL (in browsers).
3737+ *
3838+ * @returns
3939+ */
4040+export function cwd() {
4141+ if (typeof window !== 'undefined') {
4242+ return location.href;
4343+ }
4444+4545+ const path = process.cwd();
4646+4747+ const lastChar = path.slice(-1);
4848+ if (lastChar === '/' || lastChar === '\\') {
4949+ return path;
5050+ } else {
5151+ return path + '/';
5252+ }
5353+}
5454+5555+/**
5656+ * Returns the protocol of the given URL, or `undefined` if it has no protocol.
5757+ *
5858+ * @param path
5959+ * @returns
6060+ */
6161+export function getProtocol(path: string | undefined) {
6262+ const match = protocolPattern.exec(path || '');
6363+ if (match) {
6464+ return match[1]!.toLowerCase();
6565+ }
6666+ return undefined;
6767+}
6868+6969+/**
7070+ * Returns the lowercased file extension of the given URL,
7171+ * or an empty string if it has no extension.
7272+ *
7373+ * @param path
7474+ * @returns
7575+ */
7676+export function getExtension(path: any) {
7777+ const lastDot = path.lastIndexOf('.');
7878+ if (lastDot > -1) {
7979+ return stripQuery(path.substr(lastDot).toLowerCase());
8080+ }
8181+ return '';
8282+}
8383+8484+/**
8585+ * Removes the query, if any, from the given path.
8686+ *
8787+ * @param path
8888+ * @returns
8989+ */
9090+export function stripQuery(path: any) {
9191+ const queryIndex = path.indexOf('?');
9292+ if (queryIndex > -1) {
9393+ path = path.substr(0, queryIndex);
9494+ }
9595+ return path;
9696+}
9797+9898+/**
9999+ * Returns the hash (URL fragment), of the given path.
100100+ * If there is no hash, then the root hash ("#") is returned.
101101+ *
102102+ * @param path
103103+ * @returns
104104+ */
105105+export function getHash(path: undefined | string) {
106106+ if (!path) {
107107+ return '#';
108108+ }
109109+ const hashIndex = path.indexOf('#');
110110+ if (hashIndex > -1) {
111111+ return path.substring(hashIndex);
112112+ }
113113+ return '#';
114114+}
115115+116116+/**
117117+ * Removes the hash (URL fragment), if any, from the given path.
118118+ *
119119+ * @param path
120120+ * @returns
121121+ */
122122+export function stripHash(path?: string | undefined) {
123123+ if (!path) {
124124+ return '';
125125+ }
126126+ const hashIndex = path.indexOf('#');
127127+ if (hashIndex > -1) {
128128+ path = path.substring(0, hashIndex);
129129+ }
130130+ return path;
131131+}
132132+133133+/**
134134+ * Determines whether the given path is a filesystem path.
135135+ * This includes "file://" URLs.
136136+ *
137137+ * @param path
138138+ * @returns
139139+ */
140140+export function isFileSystemPath(path: string | undefined) {
141141+ // @ts-ignore
142142+ if (typeof window !== 'undefined' || (typeof process !== 'undefined' && process.browser)) {
143143+ // We're running in a browser, so assume that all paths are URLs.
144144+ // This way, even relative paths will be treated as URLs rather than as filesystem paths
145145+ return false;
146146+ }
147147+148148+ const protocol = getProtocol(path);
149149+ return protocol === undefined || protocol === 'file';
150150+}
151151+152152+/**
153153+ * Converts a filesystem path to a properly-encoded URL.
154154+ *
155155+ * This is intended to handle situations where JSON Schema $Ref Parser is called
156156+ * with a filesystem path that contains characters which are not allowed in URLs.
157157+ *
158158+ * @example
159159+ * The following filesystem paths would be converted to the following URLs:
160160+ *
161161+ * <"!@#$%^&*+=?'>.json ==> %3C%22!@%23$%25%5E&*+=%3F\'%3E.json
162162+ * C:\\My Documents\\File (1).json ==> C:/My%20Documents/File%20(1).json
163163+ * file://Project #42/file.json ==> file://Project%20%2342/file.json
164164+ *
165165+ * @param path
166166+ * @returns
167167+ */
168168+export function fromFileSystemPath(path: string) {
169169+ // Step 1: On Windows, replace backslashes with forward slashes,
170170+ // rather than encoding them as "%5C"
171171+ if (isWindows()) {
172172+ const projectDir = cwd();
173173+ const upperPath = path.toUpperCase();
174174+ const projectDirPosixPath = convertPathToPosix(projectDir);
175175+ const posixUpper = projectDirPosixPath.toUpperCase();
176176+ const hasProjectDir = upperPath.includes(posixUpper);
177177+ const hasProjectUri = upperPath.includes(posixUpper);
178178+ const isAbsolutePath =
179179+ win32.isAbsolute(path) ||
180180+ path.startsWith('http://') ||
181181+ path.startsWith('https://') ||
182182+ path.startsWith('file://');
183183+184184+ if (!(hasProjectDir || hasProjectUri || isAbsolutePath) && !projectDir.startsWith('http')) {
185185+ path = join(projectDir, path);
186186+ }
187187+ path = convertPathToPosix(path);
188188+ }
189189+190190+ // Step 2: `encodeURI` will take care of MOST characters
191191+ path = encodeURI(path);
192192+193193+ // Step 3: Manually encode characters that are not encoded by `encodeURI`.
194194+ // This includes characters such as "#" and "?", which have special meaning in URLs,
195195+ // but are just normal characters in a filesystem path.
196196+ for (const pattern of urlEncodePatterns) {
197197+ path = path.replace(pattern[0], pattern[1]);
198198+ }
199199+200200+ return path;
201201+}
202202+203203+/**
204204+ * Converts a URL to a local filesystem path.
205205+ */
206206+export function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string {
207207+ // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
208208+ path = decodeURI(path!);
209209+210210+ // Step 2: Manually decode characters that are not decoded by `decodeURI`.
211211+ // This includes characters such as "#" and "?", which have special meaning in URLs,
212212+ // but are just normal characters in a filesystem path.
213213+ for (let i = 0; i < urlDecodePatterns.length; i += 2) {
214214+ path = path.replace(urlDecodePatterns[i]!, urlDecodePatterns[i + 1] as string);
215215+ }
216216+217217+ // Step 3: If it's a "file://" URL, then format it consistently
218218+ // or convert it to a local filesystem path
219219+ let isFileUrl = path.substr(0, 7).toLowerCase() === 'file://';
220220+ if (isFileUrl) {
221221+ // Strip-off the protocol, and the initial "/", if there is one
222222+ path = path[7] === '/' ? path.substr(8) : path.substr(7);
223223+224224+ // insert a colon (":") after the drive letter on Windows
225225+ if (isWindows() && path[1] === '/') {
226226+ path = path[0] + ':' + path.substr(1);
227227+ }
228228+229229+ if (keepFileProtocol) {
230230+ // Return the consistently-formatted "file://" URL
231231+ path = 'file:///' + path;
232232+ } else {
233233+ // Convert the "file://" URL to a local filesystem path.
234234+ // On Windows, it will start with something like "C:/".
235235+ // On Posix, it will start with "/"
236236+ isFileUrl = false;
237237+ path = isWindows() ? path : '/' + path;
238238+ }
239239+ }
240240+241241+ // Step 4: Normalize Windows paths (unless it's a "file://" URL)
242242+ if (isWindows() && !isFileUrl) {
243243+ // Replace forward slashes with backslashes
244244+ path = path.replace(forwardSlashPattern, '\\');
245245+246246+ // Capitalize the drive letter
247247+ if (path.substr(1, 2) === ':\\') {
248248+ path = path[0]!.toUpperCase() + path.substr(1);
249249+ }
250250+ }
251251+252252+ return path;
253253+}
254254+255255+export function relative(from: string, to: string) {
256256+ if (!isFileSystemPath(from) || !isFileSystemPath(to)) {
257257+ return resolve(from, to);
258258+ }
259259+260260+ const fromDir = path.dirname(stripHash(from));
261261+ const toPath = stripHash(to);
262262+263263+ const result = path.relative(fromDir, toPath);
264264+ return result + getHash(to);
265265+}