breaking(openapi): unify client API

This commit is contained in:
Fuma Nama
2026-06-07 20:08:57 +08:00
committed by Fuma Nama
parent 0b2650b403
commit f0277063d1
35 changed files with 694 additions and 768 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': major
---
`renderHeading`, `renderMarkdown`, `renderCodeBlock` -> `components.Heading`, `components.Markdown`, `components.CodeBlock`
+3 -2
View File
@@ -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",
+4 -4
View File
@@ -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 = {
+31 -34
View File
@@ -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');
+12 -13
View File
@@ -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);
},
});
}
+5 -11
View File
@@ -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');
}
+1 -5
View File
@@ -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>
-362
View File
@@ -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 -1
View File
@@ -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';
-45
View File
@@ -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]);
}
+11 -48
View File
@@ -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({
+229 -57
View File
@@ -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'>;
+7 -23
View File
@@ -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';
+19 -37
View File
@@ -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,
+6 -10
View File
@@ -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;
+8
View File
@@ -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.
*/
+47
View File
@@ -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');
});
+5 -6
View File
@@ -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,
};
+2 -1
View File
@@ -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',