mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
breaking(openapi): unify client API
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': major
|
||||
---
|
||||
|
||||
`renderHeading`, `renderMarkdown`, `renderCodeBlock` -> `components.Heading`, `components.Markdown`, `components.CodeBlock`
|
||||
@@ -30,9 +30,10 @@
|
||||
"./scalar": "./dist/scalar/index.js",
|
||||
"./server": "./dist/server/index.js",
|
||||
"./ui": "./dist/ui/index.js",
|
||||
"./ui/asyncapi": "./dist/ui/asyncapi/index.js",
|
||||
"./ui/asyncapi-base": "./dist/ui/asyncapi-base.js",
|
||||
"./ui/base": "./dist/ui/base.js",
|
||||
"./ui/client": "./dist/ui/client/index.js",
|
||||
"./ui/create-client": "./dist/ui/create-client.js",
|
||||
"./package.json": "./package.json",
|
||||
"./css/*": "./css/*"
|
||||
},
|
||||
@@ -105,7 +106,7 @@
|
||||
"inlinedDependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "15.3.5",
|
||||
"@fastify/deepmerge": "3.2.1",
|
||||
"@scalar/openapi-upgrader": "0.2.8",
|
||||
"@scalar/openapi-upgrader": "0.2.9",
|
||||
"ajv": "8.20.0",
|
||||
"fast-content-type-parse": "3.0.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useApiContext } from '@/ui/contexts/api';
|
||||
import { useRenderContext } from '@/ui/contexts/api';
|
||||
import { useQuery } from '@/utils/use-query';
|
||||
import { createContext, type ReactNode, use, useEffect, useMemo, useState } from 'react';
|
||||
import type { AuthCodeState, ImplicitState } from './components/oauth-dialog';
|
||||
@@ -36,11 +36,11 @@ export function useAuth() {
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { schemes } = useApiContext();
|
||||
const schemes = useRenderContext().schema.dereferenced.components?.securitySchemes;
|
||||
const [store, setStore] = useState<TokenStore>({});
|
||||
|
||||
const authCodeQuery = useQuery(async (code: string, state: AuthCodeState) => {
|
||||
const scheme = schemes[state.scheme];
|
||||
const scheme = schemes?.[state.scheme];
|
||||
if (!scheme || scheme.type !== 'oauth2') return;
|
||||
const value = scheme.flows?.authorizationCode;
|
||||
if (!value) return;
|
||||
@@ -98,7 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
if (!state || !token) return;
|
||||
|
||||
const parsedState = JSON.parse(state) as ImplicitState;
|
||||
const scheme = schemes[parsedState.scheme];
|
||||
const scheme = schemes?.[parsedState.scheme];
|
||||
if (!scheme || scheme.type !== 'oauth2' || !scheme.flows?.implicit) return;
|
||||
|
||||
const info: TokenInfo = {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useApiContext, useServerContext } from '@/ui/contexts/api';
|
||||
import { useRenderContext, useServerContext } from '@/ui/contexts/api';
|
||||
import type { BrowserFetcherOptions } from '@/playground/fetcher';
|
||||
import { DefaultResultDisplay, type ResultDisplayProps } from './components/result-display';
|
||||
import { joinURL, resolveRequestData, resolveServerUrl, withBase } from '@/utils/url';
|
||||
@@ -47,11 +47,11 @@ import {
|
||||
} from '@fumari/stf';
|
||||
import { arrayStartsWith, objectGet, objectSet, stringifyFieldKey } from '@fumari/stf/lib/utils';
|
||||
import { FieldInput, FieldSet, JsonInput, ObjectInput } from './components/inputs';
|
||||
import type { Document, HttpMethods, ParameterObject } from '@/types';
|
||||
import type { HttpMethods, ParameterObject } from '@/types';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
import { useOperationContext } from '@/ui/operation/client';
|
||||
import { OAuthDialog, OAuthDialogContent, OAuthDialogTrigger } from './components/oauth-dialog';
|
||||
import { dereferenceSwallow } from '@/utils/schema/dereference';
|
||||
import { dereferenceShallow } from '@/utils/schema/dereference';
|
||||
import { useAuth } from './auth';
|
||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||
import { Spinner } from './components/spinner';
|
||||
@@ -68,8 +68,6 @@ export interface PlaygroundClientProps extends ComponentProps<'form'>, SchemaSco
|
||||
route: string;
|
||||
method: HttpMethods;
|
||||
securities: SecurityEntry[][];
|
||||
/** the OpenAPI document (not dereferenced) */
|
||||
doc: Document;
|
||||
proxyUrl?: string;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
@@ -128,7 +126,6 @@ export default function PlaygroundClient({
|
||||
route,
|
||||
method,
|
||||
securities,
|
||||
doc,
|
||||
proxyUrl,
|
||||
writeOnly,
|
||||
readOnly,
|
||||
@@ -136,20 +133,22 @@ export default function PlaygroundClient({
|
||||
...rest
|
||||
}: PlaygroundClientProps) {
|
||||
const t = useTranslations();
|
||||
const ctx = useRenderContext();
|
||||
const doc = ctx.schema.bundled;
|
||||
const { parameters, body } = useMemo(() => {
|
||||
const operation = doc.paths![route]![method]!;
|
||||
const parameters: ParameterObject[] =
|
||||
operation.parameters?.map((param) => dereferenceSwallow(param, doc)) ?? [];
|
||||
operation.parameters?.map((param) => dereferenceShallow(param, doc)) ?? [];
|
||||
let body: RequestBodyInfo | undefined;
|
||||
|
||||
if (operation.requestBody) {
|
||||
const content = dereferenceSwallow(operation.requestBody, doc).content;
|
||||
const content = dereferenceShallow(operation.requestBody, doc).content;
|
||||
const mediaType = content ? getPreferredType(content) : undefined;
|
||||
|
||||
if (content && mediaType) {
|
||||
body = {
|
||||
mediaType,
|
||||
schema: dereferenceSwallow(content[mediaType], doc).schema ?? true,
|
||||
schema: dereferenceShallow(content[mediaType], doc).schema ?? true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -164,18 +163,16 @@ export default function PlaygroundClient({
|
||||
const { server } = useServerContext();
|
||||
const {
|
||||
mediaAdapters,
|
||||
client: {
|
||||
playground: {
|
||||
components: {
|
||||
ResultDisplay = DefaultResultDisplay,
|
||||
CollapsiblePanel = DefaultCollapsiblePanel,
|
||||
} = {},
|
||||
requestTimeout,
|
||||
fetchOptions = { requestTimeout },
|
||||
renderBodyField,
|
||||
playground: {
|
||||
components: {
|
||||
ResultDisplay = DefaultResultDisplay,
|
||||
CollapsiblePanel = DefaultCollapsiblePanel,
|
||||
} = {},
|
||||
},
|
||||
} = useApiContext();
|
||||
requestTimeout,
|
||||
fetchOptions = { requestTimeout },
|
||||
renderBodyField,
|
||||
} = {},
|
||||
} = useRenderContext();
|
||||
|
||||
const defaultValues: FormValues = useMemo(() => {
|
||||
const requestData = examples.find((example) => example.id === exampleId)?.data;
|
||||
@@ -266,7 +263,7 @@ export default function PlaygroundClient({
|
||||
|
||||
return (
|
||||
<StfProvider value={stf}>
|
||||
<SchemaProvider doc={doc} writeOnly={writeOnly} readOnly={readOnly}>
|
||||
<SchemaProvider writeOnly={writeOnly} readOnly={readOnly}>
|
||||
<form
|
||||
{...rest}
|
||||
className={cn(
|
||||
@@ -331,7 +328,7 @@ function SecurityRequirements({
|
||||
const defaultOpen = isLoading || error != null;
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const { CollapsiblePanel = DefaultCollapsiblePanel } =
|
||||
useApiContext().client.playground?.components ?? {};
|
||||
useRenderContext().playground?.components ?? {};
|
||||
|
||||
useOnChange(defaultOpen, () => {
|
||||
if (defaultOpen) setOpen(true);
|
||||
@@ -379,12 +376,13 @@ function SecurityRequirements({
|
||||
}
|
||||
|
||||
function SecurityRequirement({ requirement }: { requirement: SecurityEntry[] }) {
|
||||
const { schemes } = useApiContext();
|
||||
const schemes = useRenderContext().schema.dereferenced.components?.securitySchemes;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 max-w-[600px]">
|
||||
{requirement.map((item) => {
|
||||
const scheme = schemes[item.id];
|
||||
const scheme = schemes?.[item.id];
|
||||
if (!scheme) return;
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
@@ -408,7 +406,7 @@ const ParamTypes = ['path', 'header', 'cookie', 'query'] as const;
|
||||
type ParamType = (typeof ParamTypes)[number];
|
||||
|
||||
function ParameterItem({ type, parameters }: { type: ParamType; parameters: ParameterObject[] }) {
|
||||
const { renderParameterField } = useApiContext().client.playground ?? {};
|
||||
const { renderParameterField } = useRenderContext().playground ?? {};
|
||||
|
||||
return parameters.map((field) => {
|
||||
const fieldName: FieldKey = [type, field.name!];
|
||||
@@ -436,7 +434,7 @@ function ParameterItem({ type, parameters }: { type: ParamType; parameters: Para
|
||||
|
||||
function ParametersForm({ parameters }: { parameters: ParameterObject[] }) {
|
||||
const { components: { CollapsiblePanel = DefaultCollapsiblePanel } = {} } =
|
||||
useApiContext().client.playground ?? {};
|
||||
useRenderContext().playground ?? {};
|
||||
const t = useTranslations();
|
||||
const displayNames = {
|
||||
header: t.header,
|
||||
@@ -523,12 +521,12 @@ function useAuthInputs(engine: DataEngine, requirements: SecurityEntry[][]) {
|
||||
const authCtx = useAuth();
|
||||
const storageKeys = useStorageKey();
|
||||
const t = useTranslations();
|
||||
const {
|
||||
schemes,
|
||||
client: { playground: { transformAuthInputs } = {} },
|
||||
} = useApiContext();
|
||||
const ctx = useRenderContext();
|
||||
const schemes = ctx.schema.dereferenced.components?.securitySchemes;
|
||||
const { transformAuthInputs } = ctx.playground ?? {};
|
||||
|
||||
const [requirementId, setRequirementId] = useState(() => {
|
||||
if (requirements.length === 0) return -1;
|
||||
if (!schemes || requirements.length === 0) return -1;
|
||||
|
||||
const idx = requirements.findIndex((s) => s.every((item) => !schemes[item.id].deprecated));
|
||||
return idx !== -1 ? idx : 0;
|
||||
@@ -536,11 +534,10 @@ function useAuthInputs(engine: DataEngine, requirements: SecurityEntry[][]) {
|
||||
const requirement = requirementId === -1 ? null : requirements[requirementId];
|
||||
|
||||
let inputs = useMemo<AuthField[]>(() => {
|
||||
if (!requirement) return [];
|
||||
if (!requirement || !schemes) return [];
|
||||
|
||||
return requirement.map((item) => {
|
||||
const scheme = schemes[item.id];
|
||||
|
||||
const scheme = schemes?.[item.id];
|
||||
if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
||||
const fieldName: FieldKey = ['header', 'Authorization'];
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
anyFields,
|
||||
useFieldInfo,
|
||||
useSchemaUtils,
|
||||
useSchemaScope,
|
||||
useSchemaContext,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
@@ -307,7 +307,7 @@ export function FieldSet({
|
||||
toolbar?: ReactNode;
|
||||
collapsible?: boolean;
|
||||
}) {
|
||||
const { readOnly, writeOnly } = useSchemaScope();
|
||||
const { readOnly, writeOnly } = useSchemaContext();
|
||||
const { generateDefault, schemaToString } = useSchemaUtils();
|
||||
const field = useResolvedSchema(_field);
|
||||
const [show, setShow] = useState(!collapsible);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import type { OAuth2SecurityScheme } from '@/types';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
import { useAuth } from '../auth';
|
||||
import { useApiContext } from '@/ui/contexts/api';
|
||||
import { useRenderContext } from '@/ui/contexts/api';
|
||||
|
||||
type FlowType = keyof NonNullable<OAuth2SecurityScheme['flows']>;
|
||||
|
||||
@@ -78,9 +78,9 @@ export function OAuthDialogContent(props: AuthDialogContentProps) {
|
||||
}
|
||||
|
||||
function Content({ schemeId, scopes, setToken, setOpen }: AuthDialogContentProps) {
|
||||
const { schemes } = useApiContext();
|
||||
const schemes = useRenderContext().schema.dereferenced.components?.securitySchemes;
|
||||
const tokenInfo = useAuth().store[schemeId];
|
||||
const scheme = schemes[schemeId];
|
||||
const scheme = schemes?.[schemeId];
|
||||
if (!scheme || scheme.type !== 'oauth2')
|
||||
throw new Error('unexpected schemaId: must be type oauth2');
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@ import { FieldKey, useDataEngine, useFieldValue, useNamespace } from '@fumari/st
|
||||
import { stringifyFieldKey } from '@fumari/stf/lib/utils';
|
||||
import { sample } from '@/utils/schema/sample';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema/to-string';
|
||||
import { dereferenceSwallow } from '@/utils/schema/dereference';
|
||||
import type { Document } from '@/types';
|
||||
import { dereferenceShallow } from '@/utils/schema/dereference';
|
||||
import { useRenderContext } from '@/ui/contexts/api';
|
||||
|
||||
interface SchemaContextType extends SchemaScope {
|
||||
/** (not dereferenced) */
|
||||
doc: Document;
|
||||
ajv: Ajv2020;
|
||||
}
|
||||
|
||||
@@ -47,7 +45,6 @@ export const anyFields = {
|
||||
} satisfies ParsedSchema;
|
||||
|
||||
export function SchemaProvider({
|
||||
doc,
|
||||
readOnly,
|
||||
writeOnly,
|
||||
children,
|
||||
@@ -64,14 +61,14 @@ export function SchemaProvider({
|
||||
|
||||
return (
|
||||
<SchemaContext.Provider
|
||||
value={useMemo(() => ({ doc, ajv, readOnly, writeOnly }), [doc, ajv, readOnly, writeOnly])}
|
||||
value={useMemo(() => ({ ajv, readOnly, writeOnly }), [ajv, readOnly, writeOnly])}
|
||||
>
|
||||
{children}
|
||||
</SchemaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSchemaScope(): SchemaScope {
|
||||
export function useSchemaContext() {
|
||||
return use(SchemaContext)!;
|
||||
}
|
||||
|
||||
@@ -91,7 +88,8 @@ export function useFieldInfo(
|
||||
schema: Exclude<ParsedSchema, boolean>;
|
||||
updateInfo: (value: Partial<FieldInfo>) => void;
|
||||
} {
|
||||
const { ajv, doc } = use(SchemaContext)!;
|
||||
const { ajv } = useSchemaContext();
|
||||
const doc = useRenderContext().schema.bundled;
|
||||
const engine = useDataEngine();
|
||||
const { generateDefault } = useSchemaUtils();
|
||||
const fieldData = useNamespace({
|
||||
@@ -155,7 +153,8 @@ export function useFieldInfo(
|
||||
}
|
||||
|
||||
export function useSchemaUtils() {
|
||||
const { doc, readOnly } = use(SchemaContext)!;
|
||||
const doc = useRenderContext().schema.bundled;
|
||||
const { readOnly } = useSchemaContext();
|
||||
|
||||
return {
|
||||
generateDefault(schema: ParsedSchema): unknown {
|
||||
@@ -174,7 +173,7 @@ export function useSchemaUtils() {
|
||||
value,
|
||||
(raw) => ({
|
||||
raw,
|
||||
dereferenced: dereferenceSwallow(raw, doc),
|
||||
dereferenced: dereferenceShallow(raw, doc),
|
||||
}),
|
||||
flags,
|
||||
);
|
||||
@@ -186,14 +185,14 @@ export function useSchemaUtils() {
|
||||
* dereference & merge `allOf`.
|
||||
*/
|
||||
export function useResolvedSchema(raw: ParsedSchema): Exclude<ParsedSchema, boolean> {
|
||||
const { doc } = use(SchemaContext)!;
|
||||
const doc = useRenderContext().schema.bundled;
|
||||
return useMemo(() => {
|
||||
let out = dereferenceSwallow(raw, doc);
|
||||
let out = dereferenceShallow(raw, doc);
|
||||
|
||||
if (typeof out === 'object' && out.allOf) {
|
||||
out = mergeAllOf(out, {
|
||||
dereference(schema) {
|
||||
return dereferenceSwallow(schema, doc);
|
||||
return dereferenceShallow(schema, doc);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { OpenAPIV3_2, OpenAPIV3 } from './types/openapi';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type { DereferencedDocument } from '@/utils/document/dereference';
|
||||
import type { OpenAPIOptions } from '@/server';
|
||||
import type { CreateAPIPageOptions } from './ui/base';
|
||||
import type { InlineCodeUsageGenerator } from './requests/generators';
|
||||
import type { CreateOpenAPIPageOptions, createMarkdownProcessor } from './ui/create-client';
|
||||
|
||||
export type Document = OpenAPIV3_2.Document;
|
||||
export type OperationObject = OpenAPIV3_2.OperationObject;
|
||||
@@ -34,21 +34,15 @@ type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
||||
export interface RenderContext
|
||||
extends
|
||||
Pick<OpenAPIOptions, 'proxyUrl'>,
|
||||
Omit<
|
||||
RequireKeys<
|
||||
CreateAPIPageOptions,
|
||||
| 'renderMarkdown'
|
||||
| 'generateTypeScriptDefinitions'
|
||||
| 'renderCodeBlock'
|
||||
| 'mediaAdapters'
|
||||
| 'codeUsages'
|
||||
>,
|
||||
'renderHeading' | 'generateTypeScriptSchema'
|
||||
RequireKeys<
|
||||
CreateOpenAPIPageOptions,
|
||||
'generateTypeScriptDefinitions' | 'mediaAdapters' | 'codeUsages' | 'shikiOptions' | 'shiki'
|
||||
> {
|
||||
/**
|
||||
* dereferenced schema
|
||||
*/
|
||||
schema: DereferencedDocument;
|
||||
_getMarkdownProcessor: () => ReturnType<typeof createMarkdownProcessor>;
|
||||
clientBoundary: typeof import('@/ui/client/boundary');
|
||||
}
|
||||
|
||||
|
||||
@@ -116,11 +116,7 @@ export function APIPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<ctx.clientBoundary.ApiProvider
|
||||
schemes={dereferenced.components?.securitySchemes ?? {}}
|
||||
shikiOptions={ctx.shikiOptions}
|
||||
client={ctx.client ?? {}}
|
||||
>
|
||||
<ctx.clientBoundary.ApiProvider ctx={ctx}>
|
||||
<ctx.clientBoundary.ServerProvider servers={dereferenced.servers as ServerObject[]}>
|
||||
{content}
|
||||
</ctx.clientBoundary.ServerProvider>
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
|
||||
import type { Awaitable, MethodInformation, RenderContext } from '@/types';
|
||||
import { parseSecurities, type NoReference } from '@/utils/schema';
|
||||
import type { DereferencedDocument } from '@/utils/document/dereference';
|
||||
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { FC, HTMLAttributes, ReactNode } from 'react';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import { createRehypeCode } from 'fumadocs-core/mdx-plugins/rehype-code.core';
|
||||
import { remarkGfm } from 'fumadocs-core/mdx-plugins/remark-gfm';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { remark } from 'remark';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
||||
import * as JsxRuntime from 'react/jsx-runtime';
|
||||
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
|
||||
import type { SchemaUIOptions } from './schema';
|
||||
import type { ResponseTab } from './operation/response-tabs';
|
||||
import { APIPage, type ApiPageProps, type OperationItem, type WebhookItem } from './api-page';
|
||||
import {
|
||||
createCodeUsageGeneratorRegistry,
|
||||
type CodeUsageGeneratorRegistry,
|
||||
type InlineCodeUsageGenerator,
|
||||
} from '@/requests/generators';
|
||||
import type { JSONSchema } from 'json-schema-typed';
|
||||
import type { BundledTheme, CodeOptionsThemes, CodeToHastOptionsCommon } from 'shiki';
|
||||
import { highlightHast, type ShikiFactory } from 'fumadocs-core/highlight/shiki';
|
||||
import type { ExampleRequestItem } from './operation/get-example-requests';
|
||||
import { compile } from '@fumari/json-schema-ts';
|
||||
import { pickSchema } from '@/utils/schema/pick';
|
||||
import { encodeInternalRef } from '@/utils/schema/ref';
|
||||
import type { RequestTabsRenderContext } from './operation/request-tabs';
|
||||
import { PlaygroundAuthProvider } from '@/ui/client/boundary.lazy';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
|
||||
export interface GenerateTypeScriptDefinitionsContext extends RenderContext {
|
||||
operation: NoReference<MethodInformation>;
|
||||
readOnly: boolean;
|
||||
writeOnly: boolean;
|
||||
/** @deprecated */
|
||||
_internal_legacy?: {
|
||||
statusCode: string;
|
||||
contentType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface APIPlaygroundProps {
|
||||
path: string;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}
|
||||
|
||||
export interface CreateAPIPageOptions {
|
||||
/**
|
||||
* Generate TypeScript definitions from response schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*
|
||||
* @param method - the operation object
|
||||
* @param statusCode - status code
|
||||
* @deprecated use `generateTypeScriptDefinitions` instead.
|
||||
*/
|
||||
generateTypeScriptSchema?:
|
||||
| ((
|
||||
method: NoReference<MethodInformation>,
|
||||
statusCode: string,
|
||||
contentType: string,
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<string | undefined>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate TypeScript definitions from JSON schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*/
|
||||
generateTypeScriptDefinitions?:
|
||||
| ((
|
||||
schema: JSONSchema,
|
||||
ctx: GenerateTypeScriptDefinitionsContext,
|
||||
) => Awaitable<string | undefined>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate example code usage for all endpoints.
|
||||
*/
|
||||
codeUsages?: CodeUsageGeneratorRegistry;
|
||||
|
||||
/**
|
||||
* Generate example code usage for each endpoint.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => InlineCodeUsageGenerator[];
|
||||
|
||||
shiki: ShikiFactory;
|
||||
renderMarkdown?: (md: string) => ReactNode;
|
||||
shikiOptions: Omit<CodeToHastOptionsCommon, 'lang'> & CodeOptionsThemes<BundledTheme>;
|
||||
|
||||
/**
|
||||
* Show full response schema instead of only example response & Typescript definitions.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showResponseSchema?: boolean;
|
||||
|
||||
/**
|
||||
* Support other media types (for server-side generation).
|
||||
*/
|
||||
mediaAdapters?: Record<string, MediaAdapter>;
|
||||
|
||||
/**
|
||||
* Customize page content
|
||||
*/
|
||||
content?: {
|
||||
renderResponseTabs?: (tabs: ResponseTab[], ctx: RenderContext) => ReactNode;
|
||||
|
||||
renderRequestTabs?: (items: ExampleRequestItem[], ctx: RequestTabsRenderContext) => ReactNode;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
selector: ReactNode;
|
||||
usageTabs: ReactNode;
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGeneratorRegistry,
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* renderer of the entire page's layout (containing all operations & webhooks UI)
|
||||
*/
|
||||
renderPageLayout?: (
|
||||
slots: {
|
||||
operations?: {
|
||||
item: OperationItem;
|
||||
children: ReactNode;
|
||||
}[];
|
||||
webhooks?: {
|
||||
item: WebhookItem;
|
||||
children: ReactNode;
|
||||
}[];
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
renderOperationLayout?: (
|
||||
slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
apiExample: ReactNode;
|
||||
apiPlayground: ReactNode;
|
||||
|
||||
authSchemes: ReactNode;
|
||||
parameters: ReactNode;
|
||||
body: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
method: NoReference<MethodInformation>,
|
||||
) => ReactNode;
|
||||
|
||||
renderWebhookLayout?: (slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
authSchemes: ReactNode;
|
||||
parameters: ReactNode;
|
||||
body: ReactNode;
|
||||
requests: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
}) => ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Info UI for JSON schemas
|
||||
*/
|
||||
schemaUI?: {
|
||||
render?: (options: SchemaUIOptions, ctx: RenderContext) => ReactNode;
|
||||
|
||||
/**
|
||||
* Show examples under the generated content of JSON schemas.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
showExample?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Customize API playground
|
||||
*/
|
||||
playground?: {
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* render a page-level provider (useful for handling auth)
|
||||
*/
|
||||
provider?: (props: { children: ReactNode }) => ReactNode;
|
||||
/**
|
||||
* replace the renderer
|
||||
*/
|
||||
render?: (props: APIPlaygroundProps) => ReactNode;
|
||||
};
|
||||
|
||||
/** @deprecated no longer used */
|
||||
renderHeading?: (props: HTMLAttributes<HTMLHeadingElement>, depth: number) => ReactNode;
|
||||
renderCodeBlock?: (props: { lang: string; code: string }) => ReactNode;
|
||||
|
||||
client?: APIPageClientOptions;
|
||||
}
|
||||
|
||||
export interface ServerApiPageProps extends Omit<ApiPageProps, 'document'> {
|
||||
document: string | DereferencedDocument;
|
||||
}
|
||||
|
||||
export function createAPIPage(
|
||||
server: OpenAPIServer,
|
||||
options: CreateAPIPageOptions,
|
||||
): FC<ServerApiPageProps> {
|
||||
let processor: ReturnType<typeof createMarkdownProcessor>;
|
||||
|
||||
function createMarkdownProcessor() {
|
||||
function rehypeReact(this: any) {
|
||||
this.compiler = (tree: any, file: any) => {
|
||||
return toJsxRuntime(tree, {
|
||||
development: false,
|
||||
filePath: file.path,
|
||||
...JsxRuntime,
|
||||
components: defaultMdxComponents,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return remark()
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(createRehypeCode(options.shiki), {
|
||||
langs: [],
|
||||
lazy: true,
|
||||
defaultColor: false,
|
||||
...options.shikiOptions,
|
||||
})
|
||||
.use(rehypeReact);
|
||||
}
|
||||
|
||||
function renderPlaygroundProviderDefault({ children }: { children: ReactNode }) {
|
||||
return <PlaygroundAuthProvider>{children}</PlaygroundAuthProvider>;
|
||||
}
|
||||
|
||||
function renderPlaygroundDefault({ path, method, ctx }: APIPlaygroundProps) {
|
||||
return (
|
||||
<ctx.clientBoundary.PlaygroundClient
|
||||
route={path}
|
||||
securities={parseSecurities(method, ctx.schema.dereferenced)}
|
||||
method={method.method}
|
||||
doc={pickSchema(ctx.schema.bundled, encodeInternalRef(['paths', path, method.method]))}
|
||||
proxyUrl={ctx.proxyUrl}
|
||||
writeOnly
|
||||
readOnly={false}
|
||||
deprecated={method.deprecated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return async function APIPageWrapper({ document, ...props }) {
|
||||
let processed: DereferencedDocument;
|
||||
if (typeof document === 'string') {
|
||||
processed = await server.getSchema(document);
|
||||
} else {
|
||||
processed = document;
|
||||
}
|
||||
|
||||
const { ApiProvider, PlaygroundClient, SchemaUI, ServerProvider, UsageTab, UsageTabsSelector } =
|
||||
await import('@/ui/client/boundary.lazy');
|
||||
|
||||
const ctx: RenderContext = {
|
||||
schema: processed,
|
||||
proxyUrl: server.options.proxyUrl,
|
||||
clientBoundary: {
|
||||
ApiProvider,
|
||||
PlaygroundClient,
|
||||
SchemaUI,
|
||||
ServerProvider,
|
||||
UsageTab,
|
||||
UsageTabsSelector,
|
||||
},
|
||||
...options,
|
||||
codeUsages: options.codeUsages ?? registerDefault(createCodeUsageGeneratorRegistry()),
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
playground: {
|
||||
...options.playground,
|
||||
provider: options.playground?.provider ?? renderPlaygroundProviderDefault,
|
||||
render: options.playground?.render ?? renderPlaygroundDefault,
|
||||
},
|
||||
generateTypeScriptDefinitions:
|
||||
options.generateTypeScriptDefinitions ??
|
||||
((schema, ctx) => {
|
||||
if (options.generateTypeScriptSchema && ctx._internal_legacy) {
|
||||
const { statusCode, contentType } = ctx._internal_legacy;
|
||||
return options.generateTypeScriptSchema(ctx.operation, statusCode, contentType, ctx);
|
||||
}
|
||||
|
||||
if (typeof schema !== 'object') return;
|
||||
try {
|
||||
return compile(schema, {
|
||||
name: 'Response',
|
||||
readOnly: ctx.readOnly,
|
||||
writeOnly: ctx.writeOnly,
|
||||
getSchemaId: ctx.schema.getRawRef,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to generate typescript schema:', e);
|
||||
}
|
||||
}),
|
||||
async renderMarkdown(text) {
|
||||
if (options.renderMarkdown) return options.renderMarkdown(text);
|
||||
processor ??= createMarkdownProcessor();
|
||||
|
||||
const out = await processor.process({
|
||||
value: text,
|
||||
});
|
||||
|
||||
return out.result as ReactNode;
|
||||
},
|
||||
async renderCodeBlock({ lang, code }) {
|
||||
if (options.renderCodeBlock) {
|
||||
return options.renderCodeBlock({ lang, code });
|
||||
}
|
||||
|
||||
const hast = await highlightHast(await options.shiki.getOrInit(), code, {
|
||||
lang,
|
||||
defaultColor: false,
|
||||
...options.shikiOptions,
|
||||
});
|
||||
const rendered = toJsxRuntime(hast, {
|
||||
...JsxRuntime,
|
||||
components: {
|
||||
pre: Pre,
|
||||
},
|
||||
});
|
||||
|
||||
return <CodeBlock className="my-0">{rendered}</CodeBlock>;
|
||||
},
|
||||
};
|
||||
|
||||
return <APIPage {...props} ctx={ctx} />;
|
||||
};
|
||||
}
|
||||
|
||||
export { ClientCodeBlockProvider } from './components/codeblock';
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { type ComponentType, lazy } from 'react';
|
||||
|
||||
// the boundary between server & lazy client components
|
||||
|
||||
export const ApiProvider = wrapLazy(() =>
|
||||
import('@/ui/contexts/api').then((mod) => ({ default: mod.ApiProvider })),
|
||||
);
|
||||
|
||||
export const ServerProvider = wrapLazy(() =>
|
||||
import('@/ui/contexts/api').then((mod) => ({ default: mod.ServerProvider })),
|
||||
);
|
||||
|
||||
export const UsageTabsSelector = wrapLazy(() =>
|
||||
import('@/ui/operation/usage-tabs/client').then((mod) => ({ default: mod.UsageTabsSelector })),
|
||||
);
|
||||
|
||||
export const UsageTab = wrapLazy(() =>
|
||||
import('@/ui/operation/usage-tabs/client').then((mod) => ({ default: mod.UsageTab })),
|
||||
);
|
||||
|
||||
export const SchemaUI = wrapLazy(() =>
|
||||
import('@/ui/schema/client').then((mod) => ({ default: mod.SchemaUI })),
|
||||
);
|
||||
|
||||
export const PlaygroundClient = wrapLazy(() => import('@/playground/client'));
|
||||
|
||||
export const PlaygroundAuthProvider = wrapLazy(() =>
|
||||
import('@/playground/auth').then((mod) => ({ default: mod.AuthProvider })),
|
||||
);
|
||||
|
||||
// Waku wraps all export functions of a "use client" file in `React.lazy`, but `lazy(lazy(() => ...))` causes error.
|
||||
// we wrap another layer of component such that it is valid
|
||||
// TODO: perhaps we can remove it once Waku migrated to vite-rsc
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- type infer
|
||||
function wrapLazy<T extends ComponentType<any>>(load: () => Promise<{ default: T }>): T {
|
||||
const V = lazy(load);
|
||||
|
||||
return function wrapper(props) {
|
||||
return <V {...props} />;
|
||||
} as T;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
export { ApiProvider, ServerProvider } from '@/ui/contexts/api';
|
||||
export { RenderContextProvider as ApiProvider, ServerProvider } from '@/ui/contexts/api';
|
||||
export { UsageTab, UsageTabsSelector } from '@/ui/operation/usage-tabs/client';
|
||||
export { SchemaUI } from '@/ui/schema/client';
|
||||
export { default as PlaygroundClient } from '@/playground/client';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client';
|
||||
import type { PlaygroundClientOptions } from '@/playground/client';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import type { CodeUsageGeneratorRegistry } from '@/requests/generators';
|
||||
import type { ExampleRequestItem } from '../operation/get-example-requests';
|
||||
|
||||
export interface APIPageClientOptions {
|
||||
playground?: PlaygroundClientOptions;
|
||||
operation?: {
|
||||
APIExampleSelector?: FC<{
|
||||
items: ExampleRequestItem[];
|
||||
|
||||
value: string | undefined;
|
||||
onValueChange: (id: string) => void;
|
||||
}>;
|
||||
};
|
||||
|
||||
components?: {
|
||||
Heading?: FC<ComponentProps<'h1'> & { id: string; depth: number }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a prefix for `localStorage` keys.
|
||||
*
|
||||
* Useful when using multiple OpenAPI instances to prevent state conflicts.
|
||||
*
|
||||
* @defaultValue `fumadocs-openapi-`
|
||||
*/
|
||||
storageKeyPrefix?: string;
|
||||
|
||||
/**
|
||||
* Support other media types (for client-side serialization)
|
||||
*/
|
||||
mediaAdapters?: Record<string, MediaAdapter>;
|
||||
|
||||
/**
|
||||
* generate code usage examples
|
||||
*/
|
||||
codeUsages?: CodeUsageGeneratorRegistry;
|
||||
}
|
||||
|
||||
export function defineClientConfig(options: APIPageClientOptions = {}): APIPageClientOptions {
|
||||
return options;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useApiContext } from '../contexts/api';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type KeyName = 'server-url' | `auth-${string}`;
|
||||
|
||||
export function useStorageKey() {
|
||||
const { storageKeyPrefix } = useApiContext().client;
|
||||
const { storageKeyPrefix } = useRenderContext();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -3,14 +3,16 @@ import {
|
||||
DynamicCodeBlock,
|
||||
type DynamicCodeblockProps,
|
||||
} from 'fumadocs-ui/components/dynamic-codeblock.core';
|
||||
import { useApiContext } from '../contexts/api';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
import { createContext, type ReactNode, use } from 'react';
|
||||
import type { ShikiFactory } from 'fumadocs-core/highlight/shiki';
|
||||
|
||||
const CodeBlockContext = createContext<ShikiFactory | null>(null);
|
||||
|
||||
export function ClientCodeBlock(props: Omit<DynamicCodeblockProps, 'highlighter' | 'options'>) {
|
||||
const { shikiOptions } = useApiContext();
|
||||
const { shikiOptions, components: { CodeBlock: Comp } = {} } = useRenderContext();
|
||||
if (Comp) return <Comp lang={props.lang} code={props.code} />;
|
||||
|
||||
const ctx = use(CodeBlockContext);
|
||||
if (!ctx)
|
||||
throw new Error(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useAnchorId } from '@/utils/auto-anchor.client';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Heading as BaseHeading } from 'fumadocs-ui/components/heading';
|
||||
import { useApiContext } from '../contexts/api';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
|
||||
export function Heading({
|
||||
id: _id,
|
||||
@@ -10,7 +10,7 @@ export function Heading({
|
||||
...props
|
||||
}: ComponentProps<'h1'> & { id: string; depth: number }) {
|
||||
const id = useAnchorId([_id]);
|
||||
const Component = useApiContext().client.components?.Heading;
|
||||
const Component = useRenderContext().components?.Heading;
|
||||
if (Component) return <Component id={id} depth={depth} {...props} />;
|
||||
|
||||
return <BaseHeading id={id} as={`h${depth}` as `h1`} {...props} />;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
|
||||
export function Markdown({ md }: { md: string }) {
|
||||
const processor = useRenderContext()._getMarkdownProcessor();
|
||||
|
||||
return useMemo(() => processor.processSync(md) as unknown as ReactNode, [processor, md]);
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
'use client';
|
||||
import { createContext, type ReactNode, use, useEffect, useMemo, useState } from 'react';
|
||||
import type { RenderContext, SecuritySchemeObject, ServerObject } from '@/types';
|
||||
import { defaultAdapters, type MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
import { useStorageKey } from '../client/storage-key';
|
||||
import type { APIPageClientOptions } from '../client';
|
||||
import {
|
||||
type CodeUsageGeneratorRegistry,
|
||||
createCodeUsageGeneratorRegistry,
|
||||
} from '@/requests/generators';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
|
||||
interface InheritFromContext extends Pick<RenderContext, 'shikiOptions'> {
|
||||
client: APIPageClientOptions;
|
||||
}
|
||||
|
||||
interface ServerContextType {
|
||||
servers?: ServerObject[];
|
||||
@@ -21,27 +10,17 @@ interface ServerContextType {
|
||||
setServerVariables: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
export interface ApiProviderProps extends InheritFromContext {
|
||||
schemes: Record<string, SecuritySchemeObject>;
|
||||
}
|
||||
|
||||
export interface SelectedServer {
|
||||
url: string;
|
||||
name?: string;
|
||||
variables: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ApiContextType extends InheritFromContext {
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
codeUsages: CodeUsageGeneratorRegistry;
|
||||
schemes: Record<string, SecuritySchemeObject>;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType | null>(null);
|
||||
const Context = createContext<RenderContext | null>(null);
|
||||
const ServerContext = createContext<ServerContextType | null>(null);
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
const ctx = use(ApiContext);
|
||||
export function useRenderContext(): RenderContext {
|
||||
const ctx = use(Context);
|
||||
if (!ctx) throw new Error('Component must be used under <ApiProvider />');
|
||||
|
||||
return ctx;
|
||||
@@ -54,30 +33,14 @@ export function useServerContext() {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function ApiProvider({
|
||||
export function RenderContextProvider({
|
||||
children,
|
||||
shikiOptions,
|
||||
client,
|
||||
schemes,
|
||||
}: ApiProviderProps & { children: ReactNode }) {
|
||||
return (
|
||||
<ApiContext
|
||||
value={useMemo(() => {
|
||||
return {
|
||||
shikiOptions,
|
||||
client,
|
||||
codeUsages: client.codeUsages ?? registerDefault(createCodeUsageGeneratorRegistry()),
|
||||
schemes,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...client.mediaAdapters,
|
||||
},
|
||||
};
|
||||
}, [client, schemes, shikiOptions])}
|
||||
>
|
||||
{children}
|
||||
</ApiContext>
|
||||
);
|
||||
ctx,
|
||||
}: {
|
||||
ctx: RenderContext;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <Context value={ctx}>{children}</Context>;
|
||||
}
|
||||
|
||||
export function ServerProvider({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
|
||||
import type { Document, RenderContext } from '@/types';
|
||||
import { defaultAdapters } from '@/requests/media/adapter';
|
||||
import type { Awaitable, Document, MethodInformation, RenderContext } from '@/types';
|
||||
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
|
||||
import {
|
||||
Children,
|
||||
type ComponentProps,
|
||||
@@ -16,37 +16,222 @@ import { remark } from 'remark';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
||||
import * as JsxRuntime from 'react/jsx-runtime';
|
||||
import { APIPage, type ApiPageProps } from './api-page';
|
||||
import type { APIPlaygroundProps, CreateAPIPageOptions } from './base';
|
||||
import { APIPage, OperationItem, WebhookItem, type ApiPageProps } from './api-page';
|
||||
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
|
||||
import { compile } from '@fumari/json-schema-ts';
|
||||
import { ClientCodeBlock, ClientCodeBlockProvider } from './components/codeblock';
|
||||
import * as ClientBoundary from '@/ui/client/boundary';
|
||||
import { dereferenceDocument } from '@/utils/document/dereference';
|
||||
import { parseSecurities } from '@/utils/schema';
|
||||
import { dereferenceOpenApiDocument } from '@/utils/document/dereference';
|
||||
import { type NoReference, parseSecurities } from '@/utils/schema';
|
||||
import { AuthProvider } from '@/playground/auth';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
import { createCodeUsageGeneratorRegistry } from '@/requests/generators';
|
||||
import {
|
||||
type CodeUsageGeneratorRegistry,
|
||||
createCodeUsageGeneratorRegistry,
|
||||
type InlineCodeUsageGenerator,
|
||||
} from '@/requests/generators';
|
||||
import type { ShikiFactory } from 'fumadocs-core/highlight/shiki';
|
||||
import type { JSONSchema } from 'json-schema-typed';
|
||||
import type { CodeToHastOptionsCommon, CodeOptionsThemes, BundledTheme } from 'shiki';
|
||||
import type { ExampleRequestItem } from './operation/get-example-requests';
|
||||
import type { RequestTabsRenderOptions } from './operation/request-tabs';
|
||||
import type { ResponseTabsRenderOptions } from './operation/response-tabs';
|
||||
import type { SchemaUIOptions } from './schema';
|
||||
import type { PlaygroundClientOptions } from '@/playground/client';
|
||||
|
||||
export interface ClientApiPageProps extends Omit<ApiPageProps, 'document'> {
|
||||
payload: ClientApiPagePayload;
|
||||
payload: OpenAPIPagePayload;
|
||||
}
|
||||
|
||||
export interface ClientApiPagePayload {
|
||||
export interface GenerateTypeScriptDefinitionsContext {
|
||||
operation: NoReference<MethodInformation>;
|
||||
readOnly: boolean;
|
||||
writeOnly: boolean;
|
||||
ctx: RenderContext;
|
||||
}
|
||||
|
||||
export interface APIPlaygroundProps {
|
||||
path: string;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}
|
||||
|
||||
export interface CreateOpenAPIPageOptions {
|
||||
/**
|
||||
* Generate TypeScript definitions from JSON schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*/
|
||||
generateTypeScriptDefinitions?:
|
||||
| ((
|
||||
schema: JSONSchema,
|
||||
ctx: GenerateTypeScriptDefinitionsContext,
|
||||
) => Awaitable<string | undefined>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate example code usage for all endpoints.
|
||||
*/
|
||||
codeUsages?: CodeUsageGeneratorRegistry;
|
||||
|
||||
/**
|
||||
* Generate example code usage for each endpoint.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => InlineCodeUsageGenerator[];
|
||||
|
||||
shiki?: ShikiFactory;
|
||||
shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> & CodeOptionsThemes<BundledTheme>;
|
||||
|
||||
/**
|
||||
* Show full response schema instead of only example response & Typescript definitions.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showResponseSchema?: boolean;
|
||||
|
||||
/**
|
||||
* Support other media types.
|
||||
*/
|
||||
mediaAdapters?: Record<string, MediaAdapter>;
|
||||
|
||||
/**
|
||||
* Customize page content
|
||||
*/
|
||||
content?: {
|
||||
renderResponseTabs?: (options: ResponseTabsRenderOptions, ctx: RenderContext) => ReactNode;
|
||||
|
||||
renderRequestTabs?: (options: RequestTabsRenderOptions, ctx: RenderContext) => ReactNode;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
selector: ReactNode;
|
||||
usageTabs: ReactNode;
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGeneratorRegistry,
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
/**
|
||||
* renderer of the entire page's layout (containing all operations & webhooks UI)
|
||||
*/
|
||||
renderPageLayout?: (
|
||||
slots: {
|
||||
operations?: {
|
||||
item: OperationItem;
|
||||
children: ReactNode;
|
||||
}[];
|
||||
webhooks?: {
|
||||
item: WebhookItem;
|
||||
children: ReactNode;
|
||||
}[];
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => ReactNode;
|
||||
|
||||
renderOperationLayout?: (
|
||||
slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
apiExample: ReactNode;
|
||||
apiPlayground: ReactNode;
|
||||
|
||||
authSchemes: ReactNode;
|
||||
parameters: ReactNode;
|
||||
body: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
method: NoReference<MethodInformation>,
|
||||
) => ReactNode;
|
||||
|
||||
renderWebhookLayout?: (slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
authSchemes: ReactNode;
|
||||
parameters: ReactNode;
|
||||
body: ReactNode;
|
||||
requests: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
}) => ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Info UI for JSON schemas
|
||||
*/
|
||||
schemaUI?: {
|
||||
render?: (options: SchemaUIOptions, ctx: RenderContext) => ReactNode;
|
||||
|
||||
/**
|
||||
* Show examples under the generated content of JSON schemas.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
showExample?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Customize API playground
|
||||
*/
|
||||
playground?: PlaygroundClientOptions & {
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
|
||||
/**
|
||||
* render a page-level provider (useful for handling auth)
|
||||
*/
|
||||
provider?: (props: { children: ReactNode }) => ReactNode;
|
||||
/**
|
||||
* replace the renderer
|
||||
*/
|
||||
render?: (props: APIPlaygroundProps) => ReactNode;
|
||||
};
|
||||
|
||||
operation?: {
|
||||
APIExampleSelector?: FC<{
|
||||
items: ExampleRequestItem[];
|
||||
|
||||
value: string | undefined;
|
||||
onValueChange: (id: string) => void;
|
||||
}>;
|
||||
};
|
||||
|
||||
components?: {
|
||||
Heading?: FC<ComponentProps<'h1'> & { id: string; depth: number }>;
|
||||
CodeBlock?: FC<{ lang: string; code: string }>;
|
||||
Markdown?: FC<{ md: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a prefix for `localStorage` keys.
|
||||
*
|
||||
* Useful when using multiple OpenAPI instances to prevent state conflicts.
|
||||
*
|
||||
* @defaultValue `fumadocs-openapi-`
|
||||
*/
|
||||
storageKeyPrefix?: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIPagePayload {
|
||||
bundled: Document;
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
export type CreateClientAPIPageOptions = Partial<
|
||||
Omit<CreateAPIPageOptions, 'generateTypeScriptSchema' | 'client'> &
|
||||
Omit<APIPageClientOptions, 'mediaAdapters' | 'codeUsages'>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Create `<APIPage />` for non-RSC environment, note that this may be unstable, and doesn't support the full set of features.
|
||||
* Create `<APIPage />` (a client component).
|
||||
*/
|
||||
export function createClientAPIPage({
|
||||
export function createOpenAPIPage({
|
||||
shiki = defaultShikiFactory,
|
||||
shikiOptions = { themes: { light: 'github-light', dark: 'github-dark' } },
|
||||
generateTypeScriptDefinitions = (schema, ctx) => {
|
||||
@@ -57,43 +242,21 @@ export function createClientAPIPage({
|
||||
name: 'Response',
|
||||
readOnly: ctx.readOnly,
|
||||
writeOnly: ctx.writeOnly,
|
||||
getSchemaId: ctx.schema.getRawRef,
|
||||
getSchemaId: ctx.ctx.schema.getRawRef,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to generate typescript schema:', e);
|
||||
}
|
||||
},
|
||||
...options
|
||||
}: CreateClientAPIPageOptions = {}): FC<ClientApiPageProps> {
|
||||
}: CreateOpenAPIPageOptions = {}): FC<ClientApiPageProps> {
|
||||
let processor: ReturnType<typeof createMarkdownProcessor>;
|
||||
const mdxComponents = {
|
||||
...defaultMdxComponents,
|
||||
img: undefined,
|
||||
pre: MarkdownPre,
|
||||
};
|
||||
|
||||
function createMarkdownProcessor() {
|
||||
function rehypeReact(this: any) {
|
||||
this.compiler = (tree: any, file: any) => {
|
||||
return toJsxRuntime(tree, {
|
||||
development: false,
|
||||
filePath: file.path,
|
||||
...JsxRuntime,
|
||||
components: mdxComponents,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return remark().use(remarkGfm).use(remarkRehype).use(rehypeReact);
|
||||
}
|
||||
|
||||
function renderPlaygroundDefault({ method, path, ctx }: APIPlaygroundProps) {
|
||||
return (
|
||||
<ctx.clientBoundary.PlaygroundClient
|
||||
route={path}
|
||||
securities={parseSecurities(method, ctx.schema.dereferenced)}
|
||||
method={method.method}
|
||||
doc={ctx.schema.bundled}
|
||||
proxyUrl={ctx.proxyUrl}
|
||||
writeOnly
|
||||
readOnly={false}
|
||||
@@ -107,7 +270,7 @@ export function createClientAPIPage({
|
||||
}
|
||||
|
||||
return function ClientAPIPage({ payload, ...props }) {
|
||||
const processed = useMemo(() => dereferenceDocument(payload.bundled), [payload.bundled]);
|
||||
const processed = useMemo(() => dereferenceOpenApiDocument(payload.bundled), [payload.bundled]);
|
||||
|
||||
const ctx: RenderContext = useMemo(
|
||||
() => ({
|
||||
@@ -120,6 +283,9 @@ export function createClientAPIPage({
|
||||
client: options,
|
||||
...options,
|
||||
codeUsages: options.codeUsages ?? registerDefault(createCodeUsageGeneratorRegistry()),
|
||||
_getMarkdownProcessor() {
|
||||
return (processor ??= createMarkdownProcessor());
|
||||
},
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...options.mediaAdapters,
|
||||
@@ -129,21 +295,6 @@ export function createClientAPIPage({
|
||||
provider: options.playground?.provider ?? renderPlaygroundProviderDefault,
|
||||
render: options.playground?.render ?? renderPlaygroundDefault,
|
||||
},
|
||||
renderMarkdown(text) {
|
||||
if (options.renderMarkdown) return options.renderMarkdown(text);
|
||||
processor ??= createMarkdownProcessor();
|
||||
|
||||
return processor.processSync({
|
||||
value: text,
|
||||
}).result as ReactNode;
|
||||
},
|
||||
renderCodeBlock(props) {
|
||||
if (options.renderCodeBlock) {
|
||||
return options.renderCodeBlock(props);
|
||||
}
|
||||
|
||||
return <ClientCodeBlock {...props} />;
|
||||
},
|
||||
}),
|
||||
[payload.proxyUrl, processed],
|
||||
);
|
||||
@@ -156,6 +307,27 @@ export function createClientAPIPage({
|
||||
};
|
||||
}
|
||||
|
||||
const mdxComponents = {
|
||||
...defaultMdxComponents,
|
||||
img: undefined,
|
||||
pre: MarkdownPre,
|
||||
};
|
||||
|
||||
export function createMarkdownProcessor() {
|
||||
function rehypeReact(this: any) {
|
||||
this.compiler = (tree: any, file: any) => {
|
||||
return toJsxRuntime(tree, {
|
||||
development: false,
|
||||
filePath: file.path,
|
||||
...JsxRuntime,
|
||||
components: mdxComponents,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return remark().use(remarkGfm).use(remarkRehype).use(rehypeReact);
|
||||
}
|
||||
|
||||
function MarkdownPre(props: ComponentProps<'pre'>) {
|
||||
const code = Children.only(props.children) as ReactElement;
|
||||
const codeProps = code.props as ComponentProps<'code'>;
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import * as base from './base';
|
||||
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
|
||||
import { FullProvider } from './client/full';
|
||||
|
||||
export type CreateAPIPageOptions = Partial<base.CreateAPIPageOptions>;
|
||||
|
||||
export function createAPIPage(server: OpenAPIServer, options: CreateAPIPageOptions = {}) {
|
||||
const APIPage = base.createAPIPage(server, {
|
||||
shiki: defaultShikiFactory,
|
||||
shikiOptions: { themes: { light: 'github-light', dark: 'github-dark' } },
|
||||
...options,
|
||||
});
|
||||
|
||||
return function APIPageFull(props: base.ServerApiPageProps) {
|
||||
return (
|
||||
<FullProvider>
|
||||
<APIPage {...props} />
|
||||
</FullProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
createOpenAPIPage as createAPIPage,
|
||||
type APIPlaygroundProps,
|
||||
type ClientApiPageProps,
|
||||
type CreateOpenAPIPageOptions as CreateAPIPageOptions,
|
||||
type GenerateTypeScriptDefinitionsContext,
|
||||
} from './create-client';
|
||||
export type { ApiPageProps, OperationItem, WebhookItem } from './api-page';
|
||||
|
||||
@@ -31,6 +31,8 @@ import { SelectTabs, SelectTabTrigger, SelectTab } from '../components/select-ta
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { AnchorSection } from '@/utils/auto-anchor.client';
|
||||
import { Heading } from '@/ui/components/heading';
|
||||
import { Markdown } from '../components/markdown';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
|
||||
const paramTypeKeys = ['path', 'query', 'header', 'cookie'] as const;
|
||||
|
||||
@@ -57,8 +59,9 @@ export function Operation({
|
||||
} = ctx;
|
||||
const body = method.requestBody;
|
||||
let headNode: ReactNode = null;
|
||||
const descriptionNode =
|
||||
showDescription && method.description && ctx.renderMarkdown(method.description);
|
||||
const descriptionNode = showDescription && method.description && (
|
||||
<Markdown md={method.description} />
|
||||
);
|
||||
let bodyNode: ReactNode = null;
|
||||
let authNode: ReactNode = null;
|
||||
let responseNode: ReactNode = null;
|
||||
@@ -104,7 +107,7 @@ export function Operation({
|
||||
<p className="text-fd-muted-foreground not-prose">{items[0].label}</p>
|
||||
)}
|
||||
</div>
|
||||
{body!.description && ctx.renderMarkdown(body!.description)}
|
||||
{body?.description && <Markdown md={body.description} />}
|
||||
{contentTypes.map(([type, content]) => {
|
||||
if (!isMediaTypeSupported(type, ctx.mediaAdapters)) {
|
||||
throw new Error(`Media type ${type} is not supported (in ${path})`);
|
||||
@@ -112,7 +115,7 @@ export function Operation({
|
||||
|
||||
return (
|
||||
<SelectTab key={type} anchorSegments={['request-body', type]} value={type}>
|
||||
<RequestBodyContentItem content={content} method={method} ctx={ctx} />
|
||||
<RequestBodyContentItem content={content} method={method} />
|
||||
</SelectTab>
|
||||
);
|
||||
})}
|
||||
@@ -130,7 +133,7 @@ export function Operation({
|
||||
</Heading>
|
||||
<Accordions type="multiple">
|
||||
{statuses.map((status) => (
|
||||
<ResponseAccordion key={status} status={status} operation={method} ctx={ctx} />
|
||||
<ResponseAccordion key={status} status={status} operation={method} />
|
||||
))}
|
||||
</Accordions>
|
||||
</>
|
||||
@@ -169,7 +172,6 @@ export function Operation({
|
||||
}
|
||||
readOnly={method.method === 'get'}
|
||||
writeOnly={method.method !== 'get'}
|
||||
ctx={ctx}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -380,7 +382,7 @@ export function Operation({
|
||||
callbacks: callbacksNode,
|
||||
parameters: parameterNode,
|
||||
responses: responseNode,
|
||||
requests: <RequestTabs examples={exampleRequests} path={path} operation={method} ctx={ctx} />,
|
||||
requests: <RequestTabs examples={exampleRequests} path={path} operation={method} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -388,19 +390,18 @@ export function Operation({
|
||||
function RequestBodyContentItem({
|
||||
content,
|
||||
method,
|
||||
ctx,
|
||||
}: {
|
||||
content: NoReference<MediaTypeObject>;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const ctx = useRenderContext();
|
||||
let ts = useMemo(() => {
|
||||
if (!content.schema || !ctx.generateTypeScriptDefinitions) return;
|
||||
return ctx.generateTypeScriptDefinitions(content.schema, {
|
||||
operation: method,
|
||||
readOnly: false,
|
||||
writeOnly: true,
|
||||
...ctx,
|
||||
ctx,
|
||||
});
|
||||
}, [content.schema, ctx, method]);
|
||||
if (ts instanceof Promise) ts = use(ts);
|
||||
@@ -418,7 +419,6 @@ function RequestBodyContentItem({
|
||||
root={content.schema}
|
||||
readOnly={method.method === 'get'}
|
||||
writeOnly={method.method !== 'get'}
|
||||
ctx={ctx}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -428,11 +428,9 @@ function RequestBodyContentItem({
|
||||
function ResponseAccordion({
|
||||
status,
|
||||
operation,
|
||||
ctx,
|
||||
}: {
|
||||
status: string;
|
||||
operation: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const response = operation.responses![status];
|
||||
const contentTypes = response.content ? Object.entries(response.content) : [];
|
||||
@@ -459,18 +457,12 @@ function ResponseAccordion({
|
||||
<AccordionContent className="ps-4.5 pe-3 border rounded-xl">
|
||||
{response.description && (
|
||||
<div className="prose-no-margin mt-3 mb-2">
|
||||
{ctx.renderMarkdown(response.description)}
|
||||
<Markdown md={response.description} />
|
||||
</div>
|
||||
)}
|
||||
{contentTypes.map(([type, item]) => (
|
||||
<SelectTab key={type} value={type} anchorSegments={[type]}>
|
||||
<RepsonseAccordionItem
|
||||
type={type}
|
||||
status={status}
|
||||
item={item}
|
||||
operation={operation}
|
||||
ctx={ctx}
|
||||
/>
|
||||
<RepsonseAccordionItem item={item} operation={operation} />
|
||||
</SelectTab>
|
||||
))}
|
||||
</AccordionContent>
|
||||
@@ -480,31 +472,22 @@ function ResponseAccordion({
|
||||
}
|
||||
|
||||
function RepsonseAccordionItem({
|
||||
type,
|
||||
status,
|
||||
operation,
|
||||
item: { schema },
|
||||
ctx,
|
||||
}: {
|
||||
type: string;
|
||||
status: string;
|
||||
operation: MethodInformation;
|
||||
item: NoReference<MediaTypeObject>;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const ctx = useRenderContext();
|
||||
let ts = useMemo(() => {
|
||||
if (!schema || !ctx.generateTypeScriptDefinitions) return;
|
||||
return ctx.generateTypeScriptDefinitions(schema, {
|
||||
readOnly: true,
|
||||
writeOnly: false,
|
||||
ctx,
|
||||
operation,
|
||||
_internal_legacy: {
|
||||
statusCode: status,
|
||||
contentType: type,
|
||||
},
|
||||
...ctx,
|
||||
});
|
||||
}, [ctx, operation, schema, status, type]);
|
||||
}, [ctx, operation, schema]);
|
||||
// assume it is on server component when returned async
|
||||
if (ts instanceof Promise) ts = use(ts);
|
||||
|
||||
@@ -519,7 +502,6 @@ function RepsonseAccordionItem({
|
||||
}}
|
||||
root={schema}
|
||||
readOnly
|
||||
ctx={ctx}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -549,7 +531,7 @@ function AuthScheme({
|
||||
deprecated={scheme.deprecated}
|
||||
scopes={scopes}
|
||||
>
|
||||
{scheme.description && ctx.renderMarkdown(scheme.description)}
|
||||
{scheme.description && <Markdown md={scheme.description} />}
|
||||
<p>
|
||||
<I18nLabel label="authTokenIn" />: <code>header</code>
|
||||
</p>
|
||||
@@ -565,7 +547,7 @@ function AuthScheme({
|
||||
deprecated={scheme.deprecated}
|
||||
scopes={scopes}
|
||||
>
|
||||
{scheme.description && ctx.renderMarkdown(scheme.description)}
|
||||
{scheme.description && <Markdown md={scheme.description} />}
|
||||
<p>
|
||||
<I18nLabel label="authTokenIn" />: <code>{scheme.in}</code>
|
||||
</p>
|
||||
@@ -581,7 +563,7 @@ function AuthScheme({
|
||||
deprecated={scheme.deprecated}
|
||||
scopes={scopes}
|
||||
>
|
||||
{scheme.description && ctx.renderMarkdown(scheme.description)}
|
||||
{scheme.description && <Markdown md={scheme.description} />}
|
||||
</AuthProperty>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import type { MethodInformation } from '@/types';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import { I18nLabel } from '@/ui/client/i18n';
|
||||
import {
|
||||
@@ -14,9 +14,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from 'fumadocs-ui/components
|
||||
import { resolveRequestData } from '@/utils/url';
|
||||
import { MethodLabel } from '../components/method-label';
|
||||
import type { ExampleRequestItem } from './get-example-requests';
|
||||
import { Markdown } from '../components/markdown';
|
||||
import { ClientCodeBlock } from '../components/codeblock';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
|
||||
export interface RequestTabsRenderContext extends RenderContext {
|
||||
export interface RequestTabsRenderOptions {
|
||||
route: string;
|
||||
items: ExampleRequestItem[];
|
||||
operation: NoReference<MethodInformation>;
|
||||
}
|
||||
|
||||
@@ -24,25 +28,29 @@ export function RequestTabs({
|
||||
path,
|
||||
operation,
|
||||
examples,
|
||||
ctx,
|
||||
}: {
|
||||
path: string;
|
||||
examples: ExampleRequestItem[];
|
||||
operation: NoReference<MethodInformation>;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const ctx = useRenderContext();
|
||||
if (!operation.requestBody) return null;
|
||||
const { renderRequestTabs = renderRequestTabsDefault } = ctx.content ?? {};
|
||||
|
||||
return renderRequestTabs(examples, {
|
||||
...ctx,
|
||||
route: path,
|
||||
operation,
|
||||
});
|
||||
return renderRequestTabs(
|
||||
{
|
||||
items: examples,
|
||||
route: path,
|
||||
operation,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
function renderRequestTabsDefault(items: ExampleRequestItem[], ctx: RequestTabsRenderContext) {
|
||||
function renderRequestTabsDefault(options: RequestTabsRenderOptions) {
|
||||
const { items } = options;
|
||||
let children: ReactNode;
|
||||
|
||||
if (items.length > 1) {
|
||||
children = (
|
||||
<Tabs defaultValue={items[0].id}>
|
||||
@@ -55,13 +63,13 @@ function renderRequestTabsDefault(items: ExampleRequestItem[], ctx: RequestTabsR
|
||||
</TabsList>
|
||||
{items.map((item) => (
|
||||
<TabsContent key={item.id} value={item.id}>
|
||||
<RequestTabsItem item={item} ctx={ctx} />
|
||||
<RequestTabsItem item={item} options={options} />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
} else if (items.length === 1) {
|
||||
children = <RequestTabsItem item={items[0]} ctx={ctx} />;
|
||||
children = <RequestTabsItem item={items[0]} options={options} />;
|
||||
} else {
|
||||
children = (
|
||||
<p className="text-fd-muted-foreground text-xs">
|
||||
@@ -82,10 +90,10 @@ function renderRequestTabsDefault(items: ExampleRequestItem[], ctx: RequestTabsR
|
||||
|
||||
function RequestTabsItem({
|
||||
item,
|
||||
ctx,
|
||||
options,
|
||||
}: {
|
||||
item: ExampleRequestItem;
|
||||
ctx: RequestTabsRenderContext;
|
||||
options: RequestTabsRenderOptions;
|
||||
}) {
|
||||
const requestData = item.data;
|
||||
const displayNames: Partial<Record<keyof RawRequestData, ReactNode>> = {
|
||||
@@ -105,10 +113,10 @@ function RequestTabsItem({
|
||||
|
||||
return (
|
||||
<>
|
||||
{item.description && ctx.renderMarkdown(item.description)}
|
||||
{item.description && <Markdown md={item.description} />}
|
||||
<div className="flex flex-row gap-2 items-center justify-between">
|
||||
<MethodLabel>{requestData.method}</MethodLabel>
|
||||
<code>{resolveRequestData(ctx.route, item.encoded)}</code>
|
||||
<code>{resolveRequestData(options.route, item.encoded)}</code>
|
||||
</div>
|
||||
|
||||
<Accordions type="multiple" className="mt-2">
|
||||
@@ -122,10 +130,7 @@ function RequestTabsItem({
|
||||
<AccordionTrigger>{v}</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent className="prose-no-margin">
|
||||
{ctx.renderCodeBlock({
|
||||
lang: 'json',
|
||||
code: JSON.stringify(data, null, 2),
|
||||
})}
|
||||
<ClientCodeBlock lang="json" code={JSON.stringify(data, null, 2)} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import { sample } from '@/utils/schema/sample';
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import { I18nLabel } from '@/ui/client/i18n';
|
||||
import { Markdown } from '../components/markdown';
|
||||
import { ClientCodeBlock } from '../components/codeblock';
|
||||
|
||||
export interface ResponseTab {
|
||||
/**
|
||||
@@ -41,6 +43,10 @@ interface ResponseExample {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ResponseTabsRenderOptions {
|
||||
tabs: ResponseTab[];
|
||||
}
|
||||
|
||||
export function ResponseTabs({
|
||||
operation,
|
||||
ctx,
|
||||
@@ -90,18 +96,15 @@ export function ResponseTabs({
|
||||
|
||||
const { renderResponseTabs = renderResponseTabsDefault } = ctx.content ?? {};
|
||||
|
||||
return renderResponseTabs(tabs, ctx);
|
||||
return renderResponseTabs({ tabs }, ctx);
|
||||
}
|
||||
|
||||
function renderResponseTabsDefault(tabs: ResponseTab[], ctx: RenderContext): ReactNode {
|
||||
function renderResponseTabsDefault({ tabs }: ResponseTabsRenderOptions): ReactNode {
|
||||
function renderExampleContent(example: ResponseExample) {
|
||||
return (
|
||||
<>
|
||||
{example.description && ctx.renderMarkdown(example.description)}
|
||||
{ctx.renderCodeBlock({
|
||||
lang: 'json',
|
||||
code: JSON.stringify(example.sample, null, 2),
|
||||
})}
|
||||
{example.description && <Markdown md={example.description} />}
|
||||
<ClientCodeBlock lang="json" code={JSON.stringify(example.sample, null, 2)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useApiContext, useServerContext } from '@/ui/contexts/api';
|
||||
import { useRenderContext, useServerContext } from '@/ui/contexts/api';
|
||||
import { joinURL, withBase, resolveServerUrl, resolveRequestData } from '@/utils/url';
|
||||
import {
|
||||
Select,
|
||||
@@ -16,7 +16,7 @@ import type { ExampleRequestItem } from '../get-example-requests';
|
||||
|
||||
export function UsageTabsSelector() {
|
||||
const { example: key, setExample: setKey, examples } = useOperationContext();
|
||||
const { APIExampleSelector: Override } = useApiContext().client.operation ?? {};
|
||||
const { APIExampleSelector: Override } = useRenderContext().operation ?? {};
|
||||
|
||||
if (Override) {
|
||||
return <Override items={examples} value={key} onValueChange={setKey} />;
|
||||
@@ -54,7 +54,7 @@ export function UsageTab({
|
||||
lang,
|
||||
_client,
|
||||
}: Pick<CodeUsageGenerator, 'lang' | '_client'> & { id: string }) {
|
||||
const { mediaAdapters, codeUsages } = useApiContext();
|
||||
const { mediaAdapters, codeUsages } = useRenderContext();
|
||||
const {
|
||||
examples,
|
||||
example: selectedExampleId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import type { RenderContext } from '@/types';
|
||||
@@ -5,6 +6,8 @@ import { FormatFlags, schemaToString } from '@/utils/schema/to-string';
|
||||
import { mergeAllOf } from '@/utils/schema/merge';
|
||||
import type { SchemaUIProps } from '@/ui/schema/client';
|
||||
import { I18nLabel } from '../client/i18n';
|
||||
import { useRenderContext } from '../contexts/api';
|
||||
import { Markdown } from '../components/markdown';
|
||||
|
||||
export interface FieldBase {
|
||||
description?: ReactNode;
|
||||
@@ -77,15 +80,8 @@ export interface SchemaUIGeneratedData {
|
||||
refs: Record<string, SchemaData>;
|
||||
}
|
||||
|
||||
export function Schema({
|
||||
ctx,
|
||||
client,
|
||||
root,
|
||||
readOnly,
|
||||
writeOnly,
|
||||
}: SchemaUIOptions & {
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
export function Schema({ client, root, readOnly, writeOnly }: SchemaUIOptions) {
|
||||
const ctx = useRenderContext();
|
||||
const generated = useMemo(
|
||||
() => generateSchemaUI(root, readOnly, writeOnly, ctx),
|
||||
[root, readOnly, writeOnly, ctx],
|
||||
@@ -207,7 +203,7 @@ export function generateSchemaUI(
|
||||
}
|
||||
|
||||
return {
|
||||
description: schema.description ? ctx.renderMarkdown(schema.description) : undefined,
|
||||
description: schema.description ? <Markdown md={schema.description} /> : undefined,
|
||||
infoTags: generateInfoTags(schema),
|
||||
typeName: schemaToString(schema, ctx.schema),
|
||||
aliasName: schemaToString(schema, ctx.schema, FormatFlags.UseAlias),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Internal JSON Pointer locations that MUST remain Reference Objects in AsyncAPI 3.0.
|
||||
*
|
||||
* @see https://github.com/asyncapi/bundler#dereference-of-the-external-references
|
||||
*/
|
||||
const mandatoryAsyncApiV3RefPatterns = [
|
||||
/^#\/channels\/.*\/servers(?:\/|$)/,
|
||||
/^#\/operations\/.*\/channel(?:\/|$)/,
|
||||
/^#\/operations\/.*\/messages(?:\/|$)/,
|
||||
/^#\/operations\/.*\/reply\/channel(?:\/|$)/,
|
||||
/^#\/operations\/.*\/reply\/messages(?:\/|$)/,
|
||||
/^#\/components\/channels\/.*\/servers(?:\/|$)/,
|
||||
/^#\/components\/operations\/.*\/channel(?:\/|$)/,
|
||||
/^#\/components\/operations\/.*\/messages(?:\/|$)/,
|
||||
/^#\/components\/operations\/.*\/reply\/channel(?:\/|$)/,
|
||||
/^#\/components\/operations\/.*\/reply\/messages(?:\/|$)/,
|
||||
] as const;
|
||||
|
||||
export function isMandatoryAsyncApiV3Ref(pointer: string): boolean {
|
||||
return mandatoryAsyncApiV3RefPatterns.some((pattern) => pattern.test(pointer));
|
||||
}
|
||||
@@ -2,34 +2,64 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
import type { NoReference } from '../schema';
|
||||
import { dereferenceSync } from '../schema/dereference';
|
||||
import type { Document } from '@/types';
|
||||
import type { AsyncAPIObject } from '@/types/asyncapi-3';
|
||||
import { isMandatoryAsyncApiV3Ref } from './asyncapi-refs';
|
||||
|
||||
export interface DereferencedDocument {
|
||||
export interface DereferencedDocument<T = Document> {
|
||||
/**
|
||||
* dereferenced document
|
||||
*/
|
||||
dereferenced: NoReference<Document>;
|
||||
dereferenced: NoReference<T>;
|
||||
|
||||
/**
|
||||
* Get raw $ref from dereferenced object
|
||||
*/
|
||||
getRawRef: (obj: object) => string | undefined;
|
||||
|
||||
bundled: Document;
|
||||
bundled: T;
|
||||
}
|
||||
|
||||
export function dereferenceDocument(bundled: Document): DereferencedDocument {
|
||||
export interface DereferencedAsyncApiDocument {
|
||||
/**
|
||||
* Dereferenced value and its original `$ref` value
|
||||
* dereferenced document — mandatory AsyncAPI 3 refs may remain as Reference Objects
|
||||
*/
|
||||
dereferenced: AsyncAPIObject;
|
||||
|
||||
/**
|
||||
* Get raw $ref from dereferenced object
|
||||
*/
|
||||
getRawRef: (obj: object) => string | undefined;
|
||||
|
||||
bundled: AsyncAPIObject;
|
||||
}
|
||||
|
||||
function createDereferencedDocument<T extends object>(
|
||||
bundled: T,
|
||||
preserveRef?: (pointer: string) => boolean,
|
||||
): DereferencedDocument<T> | DereferencedAsyncApiDocument {
|
||||
const dereferenceMap = new Map<object, string>();
|
||||
|
||||
return {
|
||||
bundled,
|
||||
dereferenced: dereferenceSync(bundled as JSONSchema, (schema, ref) => {
|
||||
dereferenceMap.set(schema as object, ref);
|
||||
}) as NoReference<Document>,
|
||||
dereferenced: dereferenceSync(bundled as JSONSchema, {
|
||||
preserveRef,
|
||||
setOriginalRef(schema, ref) {
|
||||
dereferenceMap.set(schema as object, ref);
|
||||
},
|
||||
}) as NoReference<T>,
|
||||
getRawRef(obj: object) {
|
||||
return dereferenceMap.get(obj);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function dereferenceOpenApiDocument(bundled: Document): DereferencedDocument {
|
||||
return createDereferencedDocument(bundled) as DereferencedDocument;
|
||||
}
|
||||
|
||||
export function dereferenceAsyncApiDocument(bundled: AsyncAPIObject): DereferencedAsyncApiDocument {
|
||||
return createDereferencedDocument(
|
||||
bundled,
|
||||
isMandatoryAsyncApiV3Ref,
|
||||
) as DereferencedAsyncApiDocument;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AsyncAPIObject } from '@/types/asyncapi-3';
|
||||
import { bundle } from '@apidevtools/json-schema-ref-parser';
|
||||
import { dereferenceAsyncApiDocument, type DereferencedAsyncApiDocument } from './dereference';
|
||||
|
||||
export type ProcessedAsyncApiDocument = DereferencedAsyncApiDocument;
|
||||
|
||||
function assertAsyncApiV3(document: AsyncAPIObject): void {
|
||||
if (!document.asyncapi.startsWith('3.')) {
|
||||
throw new Error(`[AsyncAPI] Expected AsyncAPI 3.x, got ${document.asyncapi}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process & resolve an AsyncAPI 3 document to a Fumadocs-compatible format.
|
||||
*/
|
||||
export async function processAsyncApiDocument(
|
||||
input: string | AsyncAPIObject,
|
||||
): Promise<ProcessedAsyncApiDocument> {
|
||||
let bundled: AsyncAPIObject;
|
||||
try {
|
||||
bundled = await bundle<AsyncAPIObject>(input);
|
||||
} catch (e) {
|
||||
throw new Error(`[AsyncAPI] Failed to resolve input: ${input}`, { cause: e });
|
||||
}
|
||||
|
||||
assertAsyncApiV3(bundled);
|
||||
|
||||
try {
|
||||
return dereferenceAsyncApiDocument(bundled);
|
||||
} catch (e) {
|
||||
throw new Error(`[AsyncAPI] Failed to resolve input: ${input}`, { cause: e });
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Document } from '@/types';
|
||||
import { upgrade } from '@scalar/openapi-upgrader';
|
||||
import { dereferenceDocument, type DereferencedDocument } from './dereference';
|
||||
import { bundle } from '@apidevtools/json-schema-ref-parser';
|
||||
import { dereferenceOpenApiDocument, type DereferencedDocument } from './dereference';
|
||||
|
||||
export type ProcessedDocument = DereferencedDocument;
|
||||
|
||||
/**
|
||||
* process & reference input document to a Fumadocs OpenAPI compatible format
|
||||
* Process & reference input document to a Fumadocs OpenAPI compatible format
|
||||
*/
|
||||
export async function processDocument(input: string | Document): Promise<ProcessedDocument> {
|
||||
try {
|
||||
let bundled: Document = await bundle(input);
|
||||
let bundled = await bundle<Document>(input);
|
||||
bundled = upgrade(bundled, '3.2') as Document;
|
||||
return dereferenceDocument(bundled);
|
||||
return dereferenceOpenApiDocument(bundled);
|
||||
} catch (e) {
|
||||
throw new Error(`[OpenAPI] Failed to resolve input: ${input}`, {
|
||||
cause: e,
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
import { appendInternalRefPath } from './ref';
|
||||
import { resolveRefSync } from './resolve-ref';
|
||||
import type { NoReferenceSwallow } from '.';
|
||||
import type { NoReferenceSwallow as NoReferenceShallow } from '.';
|
||||
import { isPlainObject } from '../is-plain-object';
|
||||
|
||||
export interface DereferenceOptions {
|
||||
/**
|
||||
* Return `true` to keep the Reference Object at `pointer` without inlining.
|
||||
*/
|
||||
preserveRef?: (pointer: string) => boolean;
|
||||
setOriginalRef?: (schema: JSONSchema, ref: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all $ref pointers in a schema and returns a new schema without any $ref pointers.
|
||||
* Resolves all $ref pointers in a schema and returns a new schema without any $ref pointers,
|
||||
* except at locations where `preserveRef` returns `true`.
|
||||
*/
|
||||
export function dereferenceSync(
|
||||
schema: JSONSchema,
|
||||
setOriginalRef: (schema: JSONSchema, ref: string) => void,
|
||||
): JSONSchema {
|
||||
export function dereferenceSync(schema: JSONSchema, options: DereferenceOptions = {}): JSONSchema {
|
||||
const { preserveRef, setOriginalRef = () => {} } = options;
|
||||
if (typeof schema === 'boolean') return schema;
|
||||
const visitedNodes = new Set<unknown>();
|
||||
const cloned = structuredClone(schema);
|
||||
|
||||
function resolve(current: unknown): JSONSchema {
|
||||
function resolve(current: unknown, pointer: string): JSONSchema {
|
||||
// make sure we don't visit the same node twice
|
||||
if (visitedNodes.has(current)) {
|
||||
return current as never;
|
||||
@@ -23,13 +31,17 @@ export function dereferenceSync(
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
for (let index = 0; index < current.length; index++) {
|
||||
current[index] = resolve(current[index]);
|
||||
current[index] = resolve(current[index], appendInternalRefPath(pointer, index));
|
||||
}
|
||||
} else if (isPlainObject(current)) {
|
||||
if (typeof current.$ref === 'string') {
|
||||
if (preserveRef?.(pointer)) {
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
const ref = current['$ref'];
|
||||
delete current['$ref'];
|
||||
const resolved = resolve(resolveRefSync(ref, cloned) as JSONSchema);
|
||||
const resolved = resolve(resolveRefSync(ref, cloned) as JSONSchema, pointer);
|
||||
setOriginalRef(resolved, ref);
|
||||
setOriginalRef(current as JSONSchema, ref);
|
||||
|
||||
@@ -42,22 +54,22 @@ export function dereferenceSync(
|
||||
}
|
||||
|
||||
for (const key in current) {
|
||||
current[key] = resolve(current[key]);
|
||||
current[key] = resolve(current[key], appendInternalRefPath(pointer, key));
|
||||
}
|
||||
}
|
||||
|
||||
return current as JSONSchema;
|
||||
}
|
||||
|
||||
return resolve(cloned);
|
||||
return resolve(cloned, '#');
|
||||
}
|
||||
|
||||
export function dereferenceSwallow<T>(schema: T, full: unknown): NoReferenceSwallow<T> {
|
||||
export function dereferenceShallow<T>(schema: T, full: unknown): NoReferenceShallow<T> {
|
||||
if (isPlainObject(schema)) {
|
||||
if (typeof schema.$ref !== 'string') return schema as never;
|
||||
|
||||
const { $ref, ...rest } = schema;
|
||||
const resolved = dereferenceSwallow(resolveRefSync($ref, full), full);
|
||||
const resolved = dereferenceShallow(resolveRefSync($ref, full), full);
|
||||
|
||||
if (typeof resolved !== 'object') throw new Error(`invalid schema referenced via "${$ref}"`);
|
||||
return { ...resolved, ...rest } as never;
|
||||
|
||||
@@ -20,6 +20,14 @@ export function encodeInternalRef(segments: readonly string[]): string {
|
||||
return `#/${segments.map(encodeSegment).join('/')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a segment to an in-document JSON Pointer (e.g. `#` + `components` → `#/components`).
|
||||
*/
|
||||
export function appendInternalRefPath(base: string, segment: string | number): string {
|
||||
const encoded = encodeSegment(String(segment));
|
||||
if (base === '#') return `#/${encoded}`;
|
||||
return `${base}/${encoded}`;
|
||||
}
|
||||
/**
|
||||
* Parse an in-document `$ref` (`#/…`) into decoded path segments for walking the root document.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import {
|
||||
getMessagePayload,
|
||||
resolveOperation,
|
||||
resolveOperationMessages,
|
||||
} from '@/ui/asyncapi/resolve';
|
||||
import type { AsyncAPIObject } from '@/types/asyncapi-3';
|
||||
|
||||
const document: AsyncAPIObject = {
|
||||
asyncapi: '3.0.0',
|
||||
info: { title: 'Test', version: '1.0.0' },
|
||||
channels: {
|
||||
userSignedUp: {
|
||||
address: 'user/signedup',
|
||||
messages: {
|
||||
userSignedUp: {
|
||||
payload: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
operations: {
|
||||
onUserSignedUp: {
|
||||
action: 'receive',
|
||||
channel: { $ref: '#/channels/userSignedUp' },
|
||||
messages: [{ $ref: '#/channels/userSignedUp/messages/userSignedUp' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('resolveOperation resolves mandatory refs for display', () => {
|
||||
const operation = resolveOperation(document, 'onUserSignedUp');
|
||||
expect(operation?.action).toBe('receive');
|
||||
|
||||
const messages = resolveOperationMessages(document, operation!);
|
||||
expect(getMessagePayload(messages[0]!)).toMatchObject({
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { processAsyncApiDocument } from '@/utils/document/process-asyncapi';
|
||||
import { isMandatoryAsyncApiV3Ref } from '@/utils/document/asyncapi-refs';
|
||||
import type { AsyncAPIObject } from '@/types/asyncapi-3';
|
||||
|
||||
const baseDocument: AsyncAPIObject = {
|
||||
asyncapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Test',
|
||||
version: '1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
test('isMandatoryAsyncApiV3Ref matches spec-required locations', () => {
|
||||
expect(isMandatoryAsyncApiV3Ref('#/operations/foo/channel')).toBe(true);
|
||||
expect(isMandatoryAsyncApiV3Ref('#/operations/foo/messages/0')).toBe(true);
|
||||
expect(isMandatoryAsyncApiV3Ref('#/components/schemas/User')).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects non-AsyncAPI 3 documents', async () => {
|
||||
await expect(
|
||||
processAsyncApiDocument({
|
||||
asyncapi: '2.6.0',
|
||||
info: { title: 'Test', version: '1.0.0' },
|
||||
}),
|
||||
).rejects.toThrow('[AsyncAPI] Expected AsyncAPI 3.x');
|
||||
});
|
||||
|
||||
test('dereferences schemas while preserving mandatory operation refs', async () => {
|
||||
const result = await processAsyncApiDocument({
|
||||
...baseDocument,
|
||||
channels: {
|
||||
userSignedUp: {
|
||||
address: 'user/signedup',
|
||||
messages: {
|
||||
userSignedUp: {
|
||||
payload: {
|
||||
$ref: '#/components/schemas/UserSignedUp',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
operations: {
|
||||
onUserSignedUp: {
|
||||
action: 'receive',
|
||||
channel: {
|
||||
$ref: '#/channels/userSignedUp',
|
||||
},
|
||||
messages: [{ $ref: '#/channels/userSignedUp/messages/userSignedUp' }],
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
UserSignedUp: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
},
|
||||
} as object,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const operation = result.dereferenced.operations?.onUserSignedUp;
|
||||
expect(operation && 'channel' in operation && operation.channel).toEqual({
|
||||
$ref: '#/channels/userSignedUp',
|
||||
});
|
||||
expect(operation && 'messages' in operation && operation.messages).toEqual([
|
||||
{ $ref: '#/channels/userSignedUp/messages/userSignedUp' },
|
||||
]);
|
||||
|
||||
const channel = result.dereferenced.channels?.userSignedUp;
|
||||
const message =
|
||||
channel && 'messages' in channel ? channel.messages?.userSignedUp : undefined;
|
||||
const payload = message && 'payload' in message ? message.payload : undefined;
|
||||
expect(payload).toMatchObject({
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
},
|
||||
});
|
||||
expect(payload).not.toHaveProperty('$ref');
|
||||
});
|
||||
|
||||
test('merges $ref target into local schema for sibling keywords', async () => {
|
||||
const result = await processAsyncApiDocument({
|
||||
...baseDocument,
|
||||
components: {
|
||||
schemas: {
|
||||
Base: {
|
||||
type: 'object',
|
||||
description: 'base description',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
Derived: {
|
||||
$ref: '#/components/schemas/Base',
|
||||
description: 'local description',
|
||||
},
|
||||
},
|
||||
} as AsyncAPIObject['components'],
|
||||
});
|
||||
|
||||
const derived = result.dereferenced.components?.schemas?.Derived as Record<string, unknown>;
|
||||
expect(derived).toMatchObject({
|
||||
type: 'object',
|
||||
description: 'local description',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
},
|
||||
required: ['id'],
|
||||
});
|
||||
expect(derived).not.toHaveProperty('$ref');
|
||||
expect(result.getRawRef(derived)).toBe('#/components/schemas/Base');
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
import { RenderContext } from '@/types';
|
||||
import { processDocument } from '@/utils/document/process';
|
||||
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
|
||||
import * as ClientBoundary from '@/ui/client/boundary.lazy';
|
||||
import * as ClientBoundary from '@/ui/client/boundary';
|
||||
import { createCodeUsageGeneratorRegistry } from '@/requests/generators';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
import { createMarkdownProcessor } from '@/ui/create-client';
|
||||
|
||||
export async function renderContextFrom(input: string): Promise<RenderContext> {
|
||||
const schema = await processDocument(input);
|
||||
const processor = createMarkdownProcessor();
|
||||
|
||||
return {
|
||||
mediaAdapters: {},
|
||||
@@ -16,12 +18,9 @@ export async function renderContextFrom(input: string): Promise<RenderContext> {
|
||||
},
|
||||
shiki: defaultShikiFactory,
|
||||
shikiOptions: { theme: 'github-light' },
|
||||
renderCodeBlock({ code }) {
|
||||
return code;
|
||||
},
|
||||
clientBoundary: ClientBoundary,
|
||||
renderMarkdown(text) {
|
||||
return text;
|
||||
_getMarkdownProcessor() {
|
||||
return processor;
|
||||
},
|
||||
schema,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ export default defineConfig({
|
||||
'./src/{index,i18n}.ts',
|
||||
'./src/ui/index.tsx',
|
||||
'./src/ui/base.tsx',
|
||||
'./src/ui/create-client.tsx',
|
||||
'./src/ui/asyncapi/index.tsx',
|
||||
'./src/ui/asyncapi-base.tsx',
|
||||
'./src/ui/client/index.tsx',
|
||||
'./src/playground/client.tsx',
|
||||
'./src/scalar/index.tsx',
|
||||
|
||||
Reference in New Issue
Block a user