diff --git a/.changeset/warm-ducks-check.md b/.changeset/warm-ducks-check.md new file mode 100644 index 000000000..fe20bed7f --- /dev/null +++ b/.changeset/warm-ducks-check.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Replace $ref with $reference in json-decycle diff --git a/bun.lock b/bun.lock index 0e59b1dbd..60e9df304 100644 --- a/bun.lock +++ b/bun.lock @@ -241,7 +241,6 @@ "@scalar/oas-utils": "^0.2.120", "clsx": "^2.1.1", "flatted": "^3.2.9", - "json-decycle": "^4.0.0", "json-xml-parse": "^1.3.0", "react-aria": "^3.37.0", "react-aria-components": "^1.6.0", @@ -2070,8 +2069,6 @@ "json-buffer": ["json-buffer@3.0.0", "", {}, "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ=="], - "json-decycle": ["json-decycle@4.0.0", "", {}, "sha512-3GFL/vWazCbMu1kw+NdIfAHh6Ugq5pxkKcSUnK1f/Fw1nDtt1i+BiBfRJs0iPEKscYAz4k4+osvgjY95hmuJXQ=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index d5f0ddf37..15ede1ffd 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -16,7 +16,6 @@ "@scalar/oas-utils": "^0.2.120", "clsx": "^2.1.1", "flatted": "^3.2.9", - "json-decycle": "^4.0.0", "json-xml-parse": "^1.3.0", "react-aria-components": "^1.6.0", "react-aria": "^3.37.0", diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 65ee7c546..a012a1ec5 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -6,10 +6,10 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { useId } from 'react'; import clsx from 'clsx'; -import { retrocycle } from 'json-decycle'; import { Markdown } from './Markdown'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; import { OpenAPISchemaName } from './OpenAPISchemaName'; +import { retrocycle } from './decycle'; import type { OpenAPIClientContext } from './types'; import { checkIsReference, resolveDescription, resolveFirstExample } from './utils'; @@ -122,7 +122,7 @@ export function OpenAPISchemaPropertiesFromServer(props: { return ( ); @@ -172,7 +172,12 @@ export function OpenAPIRootSchemaFromServer(props: { schema: string; context: OpenAPIClientContext; }) { - return ; + return ( + + ); } /** @@ -460,19 +465,3 @@ function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string { return schema.title || 'child attributes'; } - -/** - * Safely parse a JSON string using retrocycle. - * If parsing fails, it falls back to standard JSON.parse. - */ -function safeJSONParse(jsonString: string) { - try { - return JSON.parse(jsonString, retrocycle()); - } catch { - try { - return JSON.parse(jsonString); - } catch (fallbackError) { - throw new Error(`Failed to parse JSON string: ${fallbackError}`); - } - } -} diff --git a/packages/react-openapi/src/OpenAPISchemaServer.tsx b/packages/react-openapi/src/OpenAPISchemaServer.tsx index 877d378d6..d2396c705 100644 --- a/packages/react-openapi/src/OpenAPISchemaServer.tsx +++ b/packages/react-openapi/src/OpenAPISchemaServer.tsx @@ -1,10 +1,10 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; -import { decycle } from 'json-decycle'; import { OpenAPIRootSchemaFromServer, OpenAPISchemaPropertiesFromServer, type OpenAPISchemaPropertyEntry, } from './OpenAPISchema'; +import { decycle } from './decycle'; import type { OpenAPIClientContext } from './types'; export function OpenAPISchemaProperties(props: { diff --git a/packages/react-openapi/src/decycle.ts b/packages/react-openapi/src/decycle.ts new file mode 100644 index 000000000..bb2da6a8f --- /dev/null +++ b/packages/react-openapi/src/decycle.ts @@ -0,0 +1,68 @@ +// Forked from: https://github.com/YChebotaev/json-decycle/blob/master/src/index.ts +// Replaced `$ref` with `$reference` to avoid conflicts with OpenAPI + +const isObject = (value: any): value is object => + typeof value === 'object' && + value != null && + !(value instanceof Boolean) && + !(value instanceof Date) && + !(value instanceof Number) && + !(value instanceof RegExp) && + !(value instanceof String); + +const toPointer = (parts: string[]) => + `#${parts.map((part) => String(part).replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`; + +export const decycle = () => { + const paths = new WeakMap(); + + return function replacer(this: any, key: string | symbol, value: any) { + if (key !== '$reference' && isObject(value)) { + const seen = paths.has(value); + + if (seen) { + return { $reference: toPointer(paths.get(value)) }; + } + + paths.set(value, [...(paths.get(this) ?? []), key]); + } + + return value; + }; +}; + +export function retrocycle() { + const parents = new WeakMap(); + const keys = new WeakMap(); + const refs = new Set(); + + function dereference(this: { [k: string]: any }, ref: { $reference: string }) { + const parts = ref.$reference.slice(1).split('/'); + let key: any; + let value = this; + + for (let i = 0; i < parts.length; i++) { + key = parts[i]?.replace(/~1/g, '/').replace(/~0/g, '~'); + value = value[key]; + } + + const parent = parents.get(ref); + parent[keys.get(ref)] = value; + } + + return function reviver(this: object, key: string | symbol, value: any) { + if (key === '$reference') { + refs.add(this); + } else if (isObject(value)) { + const isRoot = key === '' && Object.keys(this).length === 1; + if (isRoot) { + refs.forEach(dereference as any, this); + } else { + parents.set(value, this); + keys.set(value, key); + } + } + + return value; + }; +}