mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support sync loading components on client mode
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Support sync loading components on client mode
|
||||
@@ -104,12 +104,12 @@ function Page() {
|
||||
</DocsPage>
|
||||
);
|
||||
} else {
|
||||
content = clientLoader.useContent(page.path, page);
|
||||
content = <Suspense>{clientLoader.useContent(page.path, page)}</Suspense>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={page.pageTree}>
|
||||
<Suspense>{content}</Suspense>
|
||||
{content}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
type FC,
|
||||
Fragment,
|
||||
lazy,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -53,6 +52,7 @@ import type { ParameterObject } from '@/types';
|
||||
import { ClientCodeBlock } from '@/ui/components/codeblock';
|
||||
import { useTranslations } from '@/ui/client/i18n';
|
||||
import { useOperationContext } from '@/ui/operation/client';
|
||||
import { OauthDialog, OauthDialogTrigger } from './components/oauth-dialog';
|
||||
|
||||
export interface FormValues extends Record<string, unknown> {
|
||||
path: Record<string, unknown>;
|
||||
@@ -131,17 +131,6 @@ export interface PlaygroundClientOptions {
|
||||
) => ReactNode;
|
||||
}
|
||||
|
||||
const OauthDialog = lazy(() =>
|
||||
import('./components/oauth-dialog').then((mod) => ({
|
||||
default: mod.OauthDialog,
|
||||
})),
|
||||
);
|
||||
const OauthDialogTrigger = lazy(() =>
|
||||
import('./components/oauth-dialog').then((mod) => ({
|
||||
default: mod.OauthDialogTrigger,
|
||||
})),
|
||||
);
|
||||
|
||||
export default function PlaygroundClient({
|
||||
route,
|
||||
method = 'GET',
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
} from '@/types';
|
||||
import { getPreferredType, NoReference, type ParsedSchema } from '@/utils/schema';
|
||||
import { type PlaygroundClientProps } from './client';
|
||||
import { ClientLazy } from './lazy';
|
||||
|
||||
interface Context {
|
||||
references: Record<string, ParsedSchema>;
|
||||
@@ -97,7 +96,7 @@ export function APIPlayground({ path, method, ctx }: APIPlaygroundProps) {
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
return <ClientLazy {...props} />;
|
||||
return <ctx.clientBoundary.PlaygroundClient {...props} />;
|
||||
}
|
||||
|
||||
function writeReferences(
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
'use client';
|
||||
import { wrapLazy } from '../utils/lazy';
|
||||
|
||||
export const ClientLazy = wrapLazy(() => import('./client'));
|
||||
@@ -44,6 +44,7 @@ export interface RenderContext
|
||||
* dereferenced schema
|
||||
*/
|
||||
schema: ProcessedDocument;
|
||||
clientBoundary: typeof import('@/ui/client/boundary');
|
||||
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Operation } from '@/ui/operation';
|
||||
import type { HttpMethods, RenderContext, ServerObject } from '@/types';
|
||||
import { createMethod } from '@/utils/schema';
|
||||
import { ApiProviderLazy, ServerProviderLazy } from './contexts/api.lazy';
|
||||
|
||||
export interface ApiPageProps {
|
||||
document: string;
|
||||
@@ -113,10 +112,10 @@ export function APIPage({
|
||||
);
|
||||
|
||||
return (
|
||||
<ApiProviderLazy shikiOptions={ctx.shikiOptions} client={ctx.client ?? {}}>
|
||||
<ServerProviderLazy servers={dereferenced.servers as ServerObject[]}>
|
||||
<ctx.clientBoundary.ApiProvider shikiOptions={ctx.shikiOptions} client={ctx.client ?? {}}>
|
||||
<ctx.clientBoundary.ServerProvider servers={dereferenced.servers as ServerObject[]}>
|
||||
{content}
|
||||
</ServerProviderLazy>
|
||||
</ApiProviderLazy>
|
||||
</ctx.clientBoundary.ServerProvider>
|
||||
</ctx.clientBoundary.ApiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { BundledTheme, CodeOptionsThemes, CodeToHastOptionsCommon } from 's
|
||||
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 * as ClientBoundary from '@/ui/client/boundary.lazy';
|
||||
|
||||
export interface GenerateTypeScriptDefinitionsContext extends RenderContext {
|
||||
operation: NoReference<MethodInformation>;
|
||||
@@ -256,6 +257,7 @@ export function createAPIPage(
|
||||
const ctx: RenderContext = {
|
||||
schema: processed,
|
||||
proxyUrl: server.options.proxyUrl,
|
||||
clientBoundary: ClientBoundary,
|
||||
...options,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
'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'));
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
export { ApiProvider, ServerProvider } from '@/ui/contexts/api';
|
||||
export { UsageTab, UsageTabsSelector } from '@/ui/operation/usage-tabs/client';
|
||||
export { SchemaUI } from '@/ui/schema/client';
|
||||
export { default as PlaygroundClient } from '@/playground/client';
|
||||
@@ -1,10 +0,0 @@
|
||||
'use client';
|
||||
import { wrapLazy } from '../../utils/lazy';
|
||||
|
||||
export const ApiProviderLazy = wrapLazy(() =>
|
||||
import('./api').then((mod) => ({ default: mod.ApiProvider })),
|
||||
);
|
||||
|
||||
export const ServerProviderLazy = wrapLazy(() =>
|
||||
import('./api').then((mod) => ({ default: mod.ServerProvider })),
|
||||
);
|
||||
@@ -26,6 +26,7 @@ import { ClientCodeBlock, ClientCodeBlockProvider } from './components/codeblock
|
||||
import { dereferenceSync } from '@/utils/schema/dereference';
|
||||
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
||||
import { slug } from 'github-slugger';
|
||||
import * as ClientBoundary from '@/ui/client/boundary';
|
||||
|
||||
export interface ClientApiPageProps extends Omit<ApiPageProps, 'document'> {
|
||||
payload: ClientApiPagePayload;
|
||||
@@ -107,6 +108,7 @@ export function createClientAPIPage({
|
||||
shiki,
|
||||
shikiOptions,
|
||||
generateTypeScriptDefinitions,
|
||||
clientBoundary: ClientBoundary,
|
||||
...options,
|
||||
mediaAdapters: {
|
||||
...defaultAdapters,
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { isMediaTypeSupported } from '@/requests/media/adapter';
|
||||
import { APIPlayground } from '@/playground';
|
||||
import { RequestTabs } from './request-tabs';
|
||||
import { ServerProviderLazy } from '../contexts/api.lazy';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getExampleRequests } from './get-example-requests';
|
||||
import { SelectTabs, SelectTabTrigger, SelectTab } from '../components/server-tab';
|
||||
@@ -304,9 +303,9 @@ export function Operation({
|
||||
);
|
||||
if (method.servers) {
|
||||
content = (
|
||||
<ServerProviderLazy servers={method.servers as ServerObject[]}>
|
||||
<ctx.clientBoundary.ServerProvider servers={method.servers as ServerObject[]}>
|
||||
{content}
|
||||
</ServerProviderLazy>
|
||||
</ctx.clientBoundary.ServerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CodeBlockTabsList,
|
||||
CodeBlockTabsTrigger,
|
||||
} from 'fumadocs-ui/components/codeblock';
|
||||
import { UsageTabsSelectorLazy, UsageTabLazy } from './lazy';
|
||||
import { ResponseTabs } from '../response-tabs';
|
||||
import { registerDefault } from '@/requests/generators/all';
|
||||
import { useMemo } from 'react';
|
||||
@@ -42,7 +41,7 @@ export function UsageTabs({ method, ctx }: { method: MethodInformation; ctx: Ren
|
||||
</CodeBlockTabsList>
|
||||
{map.map(([id, item]) => (
|
||||
<CodeBlockTab key={id} value={id}>
|
||||
<UsageTabLazy id={id} lang={item.lang} _client={item._client} />
|
||||
<ctx.clientBoundary.UsageTab id={id} lang={item.lang} _client={item._client} />
|
||||
</CodeBlockTab>
|
||||
))}
|
||||
</CodeBlockTabs>
|
||||
@@ -74,7 +73,7 @@ export function UsageTabs({ method, ctx }: { method: MethodInformation; ctx: Ren
|
||||
|
||||
return renderAPIExampleLayout(
|
||||
{
|
||||
selector: method['x-exclusiveCodeSample'] ? null : <UsageTabsSelectorLazy />,
|
||||
selector: method['x-exclusiveCodeSample'] ? null : <ctx.clientBoundary.UsageTabsSelector />,
|
||||
usageTabs: renderAPIExampleUsageTabs(registry, ctx),
|
||||
responseTabs: <ResponseTabs operation={method} ctx={ctx} />,
|
||||
},
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
'use client';
|
||||
import { wrapLazy } from '../../../utils/lazy';
|
||||
|
||||
export const UsageTabsSelectorLazy = wrapLazy(() =>
|
||||
import('./client').then((mod) => ({ default: mod.UsageTabsSelector })),
|
||||
);
|
||||
|
||||
export const UsageTabLazy = wrapLazy(() =>
|
||||
import('./client').then((mod) => ({ default: mod.UsageTab })),
|
||||
);
|
||||
@@ -4,7 +4,6 @@ import type { RenderContext } from '@/types';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema/to-string';
|
||||
import { mergeAllOf } from '@/utils/merge-schema';
|
||||
import type { SchemaUIProps } from '@/ui/schema/client';
|
||||
import { SchemaUILazy } from '@/ui/schema/lazy';
|
||||
import { I18nLabel } from '../client/i18n';
|
||||
|
||||
export interface FieldBase {
|
||||
@@ -80,21 +79,29 @@ export interface SchemaUIGeneratedData {
|
||||
|
||||
export function Schema({
|
||||
ctx,
|
||||
...options
|
||||
client,
|
||||
root,
|
||||
readOnly,
|
||||
writeOnly,
|
||||
}: SchemaUIOptions & {
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const generated = useMemo(
|
||||
() => generateSchemaUI(root, readOnly, writeOnly, ctx),
|
||||
[root, readOnly, writeOnly, ctx],
|
||||
);
|
||||
|
||||
if (ctx.schemaUI?.render) {
|
||||
return ctx.schemaUI.render(options, ctx);
|
||||
return ctx.schemaUI.render({ client, root, readOnly, writeOnly }, ctx);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- assume options unchanged
|
||||
const generated = useMemo(() => generateSchemaUI(options, ctx), [ctx, options]);
|
||||
return <SchemaUILazy {...options.client} generated={generated} />;
|
||||
return <ctx.clientBoundary.SchemaUI {...client} generated={generated} />;
|
||||
}
|
||||
|
||||
export function generateSchemaUI(
|
||||
{ root, readOnly, writeOnly }: SchemaUIOptions,
|
||||
root: ResolvedSchema,
|
||||
readOnly = false,
|
||||
writeOnly = false,
|
||||
ctx: RenderContext,
|
||||
): SchemaUIGeneratedData {
|
||||
const refs: Record<string, SchemaData> = {};
|
||||
@@ -185,8 +192,8 @@ export function generateSchemaUI(
|
||||
|
||||
function isVisible(schema: ResolvedSchema): boolean {
|
||||
if (typeof schema === 'boolean') return true;
|
||||
if (schema.writeOnly) return writeOnly ?? false;
|
||||
if (schema.readOnly) return readOnly ?? false;
|
||||
if (schema.writeOnly) return writeOnly;
|
||||
if (schema.readOnly) return readOnly;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
'use client';
|
||||
import { wrapLazy } from '../../utils/lazy';
|
||||
|
||||
export const SchemaUILazy = wrapLazy(() =>
|
||||
import('./client').then((mod) => ({ default: mod.SchemaUI })),
|
||||
);
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type ComponentType, lazy } from 'react';
|
||||
|
||||
// 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
|
||||
export function wrapLazy<T extends ComponentType<any>>(load: () => Promise<{ default: T }>): T {
|
||||
const V = lazy(load);
|
||||
|
||||
return function wrapper(props) {
|
||||
return <V {...props} />;
|
||||
} as T;
|
||||
}
|
||||
@@ -11,13 +11,7 @@ test('double-oneOf in allOf does not crash', async () => {
|
||||
const ctx = await renderContextFrom(path.join(cwd, './fixtures/double-oneof.yaml'));
|
||||
|
||||
const createItem = ctx.schema.dereferenced.components!.schemas!.CreateItem as ResolvedSchema;
|
||||
const out = generateSchemaUI(
|
||||
{
|
||||
client: { name: 'test' },
|
||||
root: createItem,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
const out = generateSchemaUI(createItem, undefined, undefined, ctx);
|
||||
|
||||
// Should produce a cross-product of 4 oneOf variants, not crash
|
||||
expect(out).toBeDefined();
|
||||
@@ -28,12 +22,9 @@ test('test', async () => {
|
||||
const ctx = await renderContextFrom(path.join(cwd, './fixtures/unkey.json'));
|
||||
|
||||
const out = generateSchemaUI(
|
||||
{
|
||||
client: {
|
||||
name: 'test',
|
||||
},
|
||||
root: ctx.schema.dereferenced.components!.schemas!.V1KeysVerifyKeyResponse as ResolvedSchema,
|
||||
},
|
||||
ctx.schema.dereferenced.components!.schemas!.V1KeysVerifyKeyResponse as ResolvedSchema,
|
||||
undefined,
|
||||
undefined,
|
||||
ctx,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RenderContext } from '@/types';
|
||||
import { processDocument } from '@/utils/process-document';
|
||||
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
|
||||
import * as ClientBoundary from '@/ui/client/boundary.lazy';
|
||||
|
||||
export async function renderContextFrom(input: string): Promise<RenderContext> {
|
||||
const schema = await processDocument(input);
|
||||
@@ -18,6 +19,7 @@ export async function renderContextFrom(input: string): Promise<RenderContext> {
|
||||
renderHeading(_depth, text) {
|
||||
return text;
|
||||
},
|
||||
clientBoundary: ClientBoundary,
|
||||
renderMarkdown(text) {
|
||||
return text;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user