mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support /ui/base for reduced bundle size
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
"@fumadocs/base-ui": patch
|
||||
"@fumadocs/ui": patch
|
||||
'@fumadocs/base-ui': patch
|
||||
'@fumadocs/ui': patch
|
||||
---
|
||||
|
||||
support `.core` exports for dynamic codeblocks
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Support `/ui/base` for reduced bundle size
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Base from 'fumadocs-ui/components/codeblock';
|
||||
import { getHighlighter, hastToJsx } from 'fumadocs-core/highlight';
|
||||
import { cn } from '@/lib/cn';
|
||||
import type { BundledLanguage } from 'shiki';
|
||||
import { getHighlighter, hastToJsx } from 'fumadocs-core/highlight/core';
|
||||
import { shikiConfig } from '@/lib/shiki';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
code: string;
|
||||
@@ -9,13 +9,13 @@ export interface CodeBlockProps {
|
||||
lang: string;
|
||||
}
|
||||
|
||||
const highlighter = await getHighlighter('js', {
|
||||
const highlighter = await getHighlighter(shikiConfig, {
|
||||
langs: ['js', 'ts', 'jsx', 'tsx'],
|
||||
themes: ['vesper', 'github-light'],
|
||||
});
|
||||
|
||||
export async function CodeBlock({ code, lang, wrapper }: CodeBlockProps) {
|
||||
await highlighter.loadLanguage(lang as BundledLanguage);
|
||||
await highlighter.loadLanguage(lang as never);
|
||||
const hast = highlighter.codeToHast(code, {
|
||||
lang,
|
||||
defaultColor: false,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { withJSEngine } from 'fumadocs-core/highlight/full/config';
|
||||
|
||||
export const shikiConfig = withJSEngine;
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { BundledHighlighterOptions, BundledLanguage, BundledTheme } from 'shiki';
|
||||
import type {
|
||||
BundledLanguage,
|
||||
BundledTheme,
|
||||
LanguageRegistration,
|
||||
ThemeRegistrationAny,
|
||||
} from 'shiki';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Root } from 'hast';
|
||||
import type { DistributiveOmit } from '@/types';
|
||||
@@ -30,7 +35,10 @@ export async function highlightHast(code: string, options: HighlightOptions): Pr
|
||||
*/
|
||||
export async function getHighlighter(
|
||||
engineType: 'js' | 'oniguruma',
|
||||
options: Omit<BundledHighlighterOptions<BundledLanguage, BundledTheme>, 'engine' | 'langAlias'>,
|
||||
options?: {
|
||||
langs?: (BundledLanguage | LanguageRegistration)[];
|
||||
themes?: (BundledTheme | ThemeRegistrationAny)[];
|
||||
},
|
||||
) {
|
||||
return base.getHighlighter(engineType === 'js' ? withJSEngine : withWASMEngine, options);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ShikiTransformer } from 'shiki';
|
||||
import type { ShikiTransformer } from 'shiki';
|
||||
|
||||
export type CodeBlockIcon =
|
||||
| {
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.4",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3"
|
||||
"next": "16.1.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"serve": "^14.2.5",
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.4",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"next": "16.1.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
@@ -10,27 +10,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@react-router/node": "^7.13.0",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"isbot": "^5.1.33",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-router": "^7.12.0"
|
||||
"isbot": "^5.1.34",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@react-router/dev": "^7.13.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"serve": "^14.2.5",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,27 +9,27 @@
|
||||
"types:check": "react-router typegen && fumadocs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-router/node": "^7.12.0",
|
||||
"@react-router/serve": "^7.12.0",
|
||||
"@react-router/node": "^7.13.0",
|
||||
"@react-router/serve": "^7.13.0",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"isbot": "^5.1.33",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-router": "^7.12.0"
|
||||
"isbot": "^5.1.34",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@react-router/dev": "^7.13.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,33 +7,35 @@
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"start": "serve dist/client",
|
||||
"preview": "vite preview",
|
||||
"types:check": "fumadocs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@tanstack/react-router": "1.154.13",
|
||||
"@tanstack/react-router-devtools": "1.154.13",
|
||||
"@tanstack/react-start": "1.154.13",
|
||||
"@tanstack/start-static-server-functions": "1.154.13",
|
||||
"@tanstack/react-router": "1.157.16",
|
||||
"@tanstack/react-router-devtools": "1.157.16",
|
||||
"@tanstack/react-start": "1.157.16",
|
||||
"@tanstack/start-static-server-functions": "1.157.16",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"nitro": "npm:[email protected]",
|
||||
"serve": "^14.2.5",
|
||||
"srvx": "^0.10.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';
|
||||
import tsConfigPaths from 'vite-tsconfig-paths';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import mdx from 'fumadocs-mdx/vite';
|
||||
import { nitro } from 'nitro/vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
@@ -35,5 +36,7 @@ export default defineConfig({
|
||||
],
|
||||
}),
|
||||
react(),
|
||||
// please see https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro for guides on hosting
|
||||
nitro(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -7,30 +7,32 @@
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"preview": "vite preview",
|
||||
"types:check": "fumadocs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "1.154.13",
|
||||
"@tanstack/react-router-devtools": "1.154.13",
|
||||
"@tanstack/react-start": "1.154.13",
|
||||
"@tanstack/react-router": "1.157.16",
|
||||
"@tanstack/react-router-devtools": "1.157.16",
|
||||
"@tanstack/react-start": "1.157.16",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"nitro": "npm:[email protected]",
|
||||
"srvx": "^0.10.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';
|
||||
import tsConfigPaths from 'vite-tsconfig-paths';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import mdx from 'fumadocs-mdx/vite';
|
||||
import { nitro } from 'nitro/vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
@@ -21,5 +22,12 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
react(),
|
||||
// please see https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro for guides on hosting
|
||||
nitro({
|
||||
preset: 'vercel',
|
||||
vercel: {
|
||||
entryFormat: 'node',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -15,20 +15,20 @@
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-server-dom-webpack": "^19.2.3",
|
||||
"waku": "1.0.0-alpha.2"
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-server-dom-webpack": "^19.2.4",
|
||||
"waku": "1.0.0-alpha.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
"vite-tsconfig-paths": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js"
|
||||
},
|
||||
"./ui/base": {
|
||||
"types": "./dist/ui/base.d.ts",
|
||||
"import": "./dist/ui/base.js"
|
||||
},
|
||||
"./ui/client": {
|
||||
"types": "./dist/ui/client/index.d.ts",
|
||||
"import": "./dist/ui/client/index.js"
|
||||
@@ -99,8 +103,8 @@
|
||||
"peerDependencies": {
|
||||
"@scalar/api-client-react": "*",
|
||||
"@types/react": "*",
|
||||
"fumadocs-core": "^16.2.0",
|
||||
"fumadocs-ui": "^16.2.0",
|
||||
"fumadocs-core": "^16.5.0",
|
||||
"fumadocs-ui": "^16.5.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { FetchResult } from '@/playground/fetcher';
|
||||
import type { ParameterField, SecurityEntry } from '@/playground/index';
|
||||
import { getStatusInfo } from './status-info';
|
||||
import { joinURL, resolveRequestData, resolveServerUrl, withBase } from '@/utils/url';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock.core';
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
import { useQuery } from '@/utils/use-query';
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NoReference } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { OpenAPIOptions } from '@/server';
|
||||
import type { CreateAPIPageOptions } from './ui/api-page';
|
||||
import type { CreateAPIPageOptions } from './ui/base';
|
||||
import type { CodeUsageGenerator } from './ui/operation/usage-tabs';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
@@ -27,7 +27,8 @@ export type MethodInformation = NoReference<OperationObject> & {
|
||||
'x-exclusiveCodeSample'?: string;
|
||||
};
|
||||
|
||||
export interface RenderContext extends Pick<OpenAPIOptions, 'proxyUrl'>, CreateAPIPageOptions {
|
||||
export interface RenderContext
|
||||
extends Pick<OpenAPIOptions, 'proxyUrl'>, Omit<CreateAPIPageOptions, 'renderMarkdown'> {
|
||||
servers: NoReference<ServerObject>[];
|
||||
slugger: Slugger;
|
||||
|
||||
@@ -48,3 +49,4 @@ export interface RenderContext extends Pick<OpenAPIOptions, 'proxyUrl'>, CreateA
|
||||
}
|
||||
|
||||
export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
||||
export type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
@@ -1,175 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
|
||||
import Slugger from 'github-slugger';
|
||||
import { Operation } from '@/ui/operation';
|
||||
import type { DistributiveOmit, MethodInformation, RenderContext } from '@/types';
|
||||
import { createMethod, type NoReference } from '@/utils/schema';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { createMethod } from '@/utils/schema';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { highlight, type HighlightOptions } from 'fumadocs-core/highlight';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import type { CodeUsageGenerator } from './operation/usage-tabs';
|
||||
import { ApiProviderLazy } from './contexts/api.lazy';
|
||||
import { Heading } from 'fumadocs-ui/components/heading';
|
||||
import { rehypeCode, type RehypeCodeOptions } from 'fumadocs-core/mdx-plugins/rehype-code';
|
||||
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 type { ExampleRequestItem } from './operation/request-tabs';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface CreateAPIPageOptions {
|
||||
/**
|
||||
* Generate TypeScript definitions from response schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*
|
||||
* @param method - the operation object
|
||||
* @param statusCode - status code
|
||||
*/
|
||||
generateTypeScriptSchema?:
|
||||
| ((method: NoReference<MethodInformation>, statusCode: string) => Awaitable<string>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate example code usage for endpoints.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeUsageGenerator[]>;
|
||||
|
||||
shikiOptions?: DistributiveOmit<HighlightOptions, 'lang' | 'components'>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* Customise page content
|
||||
*/
|
||||
content?: {
|
||||
renderResponseTabs?: (tabs: ResponseTab[], ctx: RenderContext) => Awaitable<ReactNode>;
|
||||
|
||||
renderRequestTabs?: (
|
||||
items: ExampleRequestItem[],
|
||||
ctx: RenderContext & {
|
||||
route: string;
|
||||
operation: NoReference<MethodInformation>;
|
||||
},
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
selector: ReactNode;
|
||||
usageTabs: ReactNode;
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGenerator[],
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<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,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderOperationLayout?: (
|
||||
slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
apiExample: ReactNode;
|
||||
apiPlayground: ReactNode;
|
||||
|
||||
authSchemes: ReactNode;
|
||||
paremeters: ReactNode;
|
||||
body: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
method: NoReference<MethodInformation>,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderWebhookLayout?: (slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
authSchemes: ReactNode;
|
||||
paremeters: ReactNode;
|
||||
body: ReactNode;
|
||||
requests: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Info UI for JSON schemas
|
||||
*/
|
||||
schemaUI?: {
|
||||
render?: (options: SchemaUIOptions, ctx: RenderContext) => Awaitable<ReactNode>;
|
||||
|
||||
/**
|
||||
* Show examples under the generated content of JSON schemas.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
showExample?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Customise API playground
|
||||
*/
|
||||
playground?: {
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* replace the server-side renderer
|
||||
*/
|
||||
render?: (props: {
|
||||
path: string;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
client?: APIPageClientOptions;
|
||||
}
|
||||
|
||||
export interface ApiPageProps {
|
||||
document: Promise<ProcessedDocument> | string | ProcessedDocument;
|
||||
@@ -203,96 +37,7 @@ export interface OperationItem {
|
||||
method: OpenAPIV3_1.HttpMethods;
|
||||
}
|
||||
|
||||
export function createAPIPage(
|
||||
server: OpenAPIServer,
|
||||
options: CreateAPIPageOptions = {},
|
||||
): FC<ApiPageProps> {
|
||||
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(rehypeCode, {
|
||||
langs: [],
|
||||
lazy: true,
|
||||
} satisfies Partial<RehypeCodeOptions>)
|
||||
.use(rehypeReact);
|
||||
}
|
||||
|
||||
return async function APIPageWrapper({ document, ...props }) {
|
||||
let processed: ProcessedDocument;
|
||||
if (typeof document === 'string') {
|
||||
processed = await server.getSchema(document);
|
||||
} else {
|
||||
processed = await document;
|
||||
}
|
||||
|
||||
const { dereferenced } = processed;
|
||||
const servers =
|
||||
dereferenced.servers && dereferenced.servers.length > 0
|
||||
? dereferenced.servers
|
||||
: [{ url: '/' }];
|
||||
|
||||
const slugger = new Slugger();
|
||||
|
||||
const ctx: RenderContext = {
|
||||
schema: processed,
|
||||
proxyUrl: server.options.proxyUrl,
|
||||
...options,
|
||||
servers,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
slugger,
|
||||
renderHeading(depth, text, props) {
|
||||
const id = slugger.slug(text);
|
||||
|
||||
return (
|
||||
<Heading id={id} key={id} as={`h${depth}` as `h1`} {...props}>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
},
|
||||
async renderMarkdown(text) {
|
||||
processor ??= createMarkdownProcessor();
|
||||
|
||||
const out = await processor.process({
|
||||
value: text,
|
||||
});
|
||||
|
||||
return out.result as ReactNode;
|
||||
},
|
||||
async renderCodeBlock(lang, code) {
|
||||
const rendered = await highlight(code, {
|
||||
lang,
|
||||
...options.shikiOptions,
|
||||
components: {
|
||||
pre: (props) => <Pre {...props} />,
|
||||
},
|
||||
});
|
||||
|
||||
return <CodeBlock className="my-0">{rendered}</CodeBlock>;
|
||||
},
|
||||
};
|
||||
|
||||
return <APIPage {...props} ctx={ctx} />;
|
||||
};
|
||||
}
|
||||
|
||||
async function APIPage({
|
||||
export async function APIPage({
|
||||
showTitle: hasHead = false,
|
||||
showDescription,
|
||||
operations,
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- rehype-react without types */
|
||||
import Slugger from 'github-slugger';
|
||||
import type { Awaitable, DistributiveOmit, MethodInformation, RenderContext } from '@/types';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { highlight, type CoreHighlightOptions } from 'fumadocs-core/highlight/core';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import type { APIPageClientOptions } from './client';
|
||||
import type { CodeUsageGenerator } from './operation/usage-tabs';
|
||||
import { Heading } from 'fumadocs-ui/components/heading';
|
||||
import { createRehypeCode } from 'fumadocs-core/mdx-plugins/rehype-code.min';
|
||||
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 type { ExampleRequestItem } from './operation/request-tabs';
|
||||
import type { ResolvedShikiConfig } from 'fumadocs-core/highlight/config';
|
||||
import { APIPage, type ApiPageProps, type OperationItem, type WebhookItem } from './api-page';
|
||||
|
||||
export interface CreateAPIPageOptions {
|
||||
/**
|
||||
* Generate TypeScript definitions from response schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*
|
||||
* @param method - the operation object
|
||||
* @param statusCode - status code
|
||||
*/
|
||||
generateTypeScriptSchema?:
|
||||
| ((
|
||||
method: NoReference<MethodInformation>,
|
||||
statusCode: string,
|
||||
contentType: string,
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<string | undefined>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate example code usage for endpoints.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeUsageGenerator[]>;
|
||||
|
||||
shiki: ResolvedShikiConfig;
|
||||
renderMarkdown?: (md: string) => Awaitable<ReactNode>;
|
||||
shikiOptions?: DistributiveOmit<CoreHighlightOptions, 'config' | 'lang' | 'components'>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* Customise page content
|
||||
*/
|
||||
content?: {
|
||||
renderResponseTabs?: (tabs: ResponseTab[], ctx: RenderContext) => Awaitable<ReactNode>;
|
||||
|
||||
renderRequestTabs?: (
|
||||
items: ExampleRequestItem[],
|
||||
ctx: RenderContext & {
|
||||
route: string;
|
||||
operation: NoReference<MethodInformation>;
|
||||
},
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderAPIExampleLayout?: (
|
||||
slots: {
|
||||
selector: ReactNode;
|
||||
usageTabs: ReactNode;
|
||||
responseTabs: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
/**
|
||||
* @param generators - codegens for API example usages
|
||||
*/
|
||||
renderAPIExampleUsageTabs?: (
|
||||
generators: CodeUsageGenerator[],
|
||||
ctx: RenderContext,
|
||||
) => Awaitable<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,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderOperationLayout?: (
|
||||
slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
apiExample: ReactNode;
|
||||
apiPlayground: ReactNode;
|
||||
|
||||
authSchemes: ReactNode;
|
||||
paremeters: ReactNode;
|
||||
body: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
},
|
||||
ctx: RenderContext,
|
||||
method: NoReference<MethodInformation>,
|
||||
) => Awaitable<ReactNode>;
|
||||
|
||||
renderWebhookLayout?: (slots: {
|
||||
header: ReactNode;
|
||||
description: ReactNode;
|
||||
authSchemes: ReactNode;
|
||||
paremeters: ReactNode;
|
||||
body: ReactNode;
|
||||
requests: ReactNode;
|
||||
responses: ReactNode;
|
||||
callbacks: ReactNode;
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Info UI for JSON schemas
|
||||
*/
|
||||
schemaUI?: {
|
||||
render?: (options: SchemaUIOptions, ctx: RenderContext) => Awaitable<ReactNode>;
|
||||
|
||||
/**
|
||||
* Show examples under the generated content of JSON schemas.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
showExample?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Customise API playground
|
||||
*/
|
||||
playground?: {
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* replace the server-side renderer
|
||||
*/
|
||||
render?: (props: {
|
||||
path: string;
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) => Awaitable<ReactNode>;
|
||||
};
|
||||
|
||||
client?: APIPageClientOptions;
|
||||
}
|
||||
|
||||
export function createAPIPage(
|
||||
server: OpenAPIServer,
|
||||
options: CreateAPIPageOptions,
|
||||
): FC<ApiPageProps> {
|
||||
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,
|
||||
})
|
||||
.use(rehypeReact);
|
||||
}
|
||||
|
||||
return async function APIPageWrapper({ document, ...props }) {
|
||||
let processed: ProcessedDocument;
|
||||
if (typeof document === 'string') {
|
||||
processed = await server.getSchema(document);
|
||||
} else {
|
||||
processed = await document;
|
||||
}
|
||||
|
||||
const { dereferenced } = processed;
|
||||
const servers =
|
||||
dereferenced.servers && dereferenced.servers.length > 0
|
||||
? dereferenced.servers
|
||||
: [{ url: '/' }];
|
||||
|
||||
const slugger = new Slugger();
|
||||
|
||||
const ctx: RenderContext = {
|
||||
schema: processed,
|
||||
proxyUrl: server.options.proxyUrl,
|
||||
...options,
|
||||
servers,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
slugger,
|
||||
renderHeading(depth, text, props) {
|
||||
const id = slugger.slug(text);
|
||||
|
||||
return (
|
||||
<Heading id={id} key={id} as={`h${depth}` as `h1`} {...props}>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
},
|
||||
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) {
|
||||
const rendered = await highlight(code, {
|
||||
lang,
|
||||
...options.shikiOptions,
|
||||
config: options.shiki,
|
||||
components: {
|
||||
pre: (props) => <Pre {...props} />,
|
||||
},
|
||||
});
|
||||
|
||||
return <CodeBlock className="my-0">{rendered}</CodeBlock>;
|
||||
},
|
||||
};
|
||||
|
||||
return <APIPage {...props} ctx={ctx} />;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
import * as base from 'fumadocs-core/highlight/core/client';
|
||||
import { withJSEngine } from 'fumadocs-core/highlight/full/config';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function ShikiConfigProvider({ children }: { children: ReactNode }) {
|
||||
const config = base.useShikiConfigOptional() ?? withJSEngine;
|
||||
return <base.ShikiConfigProvider config={config}>{children}</base.ShikiConfigProvider>;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
import * as base from './base';
|
||||
import { withJSEngine } from 'fumadocs-core/highlight/full/config';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { ApiPageProps } from './api-page';
|
||||
import { ShikiConfigProvider } from './full.client';
|
||||
|
||||
export type CreateAPIPageOptions = Partial<base.CreateAPIPageOptions>;
|
||||
|
||||
export function createAPIPage(server: OpenAPIServer, options: CreateAPIPageOptions = {}) {
|
||||
const APIPage = base.createAPIPage(server, {
|
||||
...options,
|
||||
shiki: withJSEngine,
|
||||
generateTypeScriptSchema(method, statusCode, contentType, ctx) {
|
||||
const schema = method.responses?.[statusCode]?.content?.[contentType];
|
||||
if (!schema) return;
|
||||
return getTypescriptSchema(schema, ctx);
|
||||
},
|
||||
});
|
||||
|
||||
return function APIPageFull(props: ApiPageProps) {
|
||||
return (
|
||||
<ShikiConfigProvider>
|
||||
<APIPage {...props} />
|
||||
</ShikiConfigProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from '@/ui/api-page';
|
||||
export * from './full';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { idToTitle } from '@/utils/id-to-title';
|
||||
import { Schema } from '../schema';
|
||||
import { UsageTabs } from '@/ui/operation/usage-tabs';
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { CopyResponseTypeScript, SelectTab, SelectTabs, SelectTabTrigger } from './client';
|
||||
import {
|
||||
AccordionContent,
|
||||
@@ -338,7 +337,6 @@ async function ResponseAccordion({
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const response = operation.responses![status];
|
||||
const { generateTypeScriptSchema } = ctx;
|
||||
const contentTypes = response.content ? Object.entries(response.content) : [];
|
||||
let wrapper = (children: ReactNode) => children;
|
||||
let selectorNode: ReactNode = null;
|
||||
@@ -372,10 +370,8 @@ async function ResponseAccordion({
|
||||
const schema = resType.schema;
|
||||
let ts: string | undefined;
|
||||
|
||||
if (generateTypeScriptSchema) {
|
||||
ts = await generateTypeScriptSchema(operation, status);
|
||||
} else if (generateTypeScriptSchema === undefined && schema) {
|
||||
ts = await getTypescriptSchema(schema, ctx);
|
||||
if (ctx.generateTypeScriptSchema) {
|
||||
ts = await ctx.generateTypeScriptSchema(operation, status, type, ctx);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '@/ui/components/select';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock.core';
|
||||
import { useState, useEffect, useMemo, createContext, ReactNode, useRef, use } from 'react';
|
||||
import type { CodeUsageGenerator } from '.';
|
||||
import type { ExampleRequestItem } from '../request-tabs';
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
entry: [
|
||||
'./src/index.ts',
|
||||
'./src/ui/index.ts',
|
||||
'./src/ui/base.tsx',
|
||||
'./src/ui/client/index.tsx',
|
||||
'./src/playground/{index,client}.tsx',
|
||||
'./src/scalar/index.tsx',
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { Code } from 'mdast';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
||||
import { defaultHandlers, toHast } from 'mdast-util-to-hast';
|
||||
import {
|
||||
ShikiError,
|
||||
type ShikiTransformer,
|
||||
type ShikiTransformerContext,
|
||||
type ShikiTransformerContextCommon,
|
||||
import type {
|
||||
ShikiTransformer,
|
||||
ShikiTransformerContext,
|
||||
ShikiTransformerContextCommon,
|
||||
} from 'shiki';
|
||||
import { ShikiError } from 'shiki/core';
|
||||
import {
|
||||
createTransformerFactory,
|
||||
rendererRich,
|
||||
|
||||
Reference in New Issue
Block a user