OpenAPI: Support displaying response results of different mime types

This commit is contained in:
Fuma Nama
2026-04-07 15:56:03 +08:00
parent a070650436
commit 8462aa6245
9 changed files with 286 additions and 115 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Support displaying response results of different mime types
+1
View File
@@ -62,6 +62,7 @@
"ajv": "^8.18.0",
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"fast-content-type-parse": "^3.0.0",
"github-slugger": "^2.0.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"js-yaml": "^4.1.1",
+2 -1
View File
@@ -76,7 +76,8 @@ export const defaultTranslations = {
statusInternalServerError: 'Internal Server Error',
statusSuccessful: 'Successful',
statusError: 'Error',
statusNoDescription: 'No Description',
statusClientError: 'Client Error',
statusBinaryBody: 'Binary response body, {length} bytes',
// OAuth dialog
obtainAccessToken: 'Obtain the access token for API.',
+4 -43
View File
@@ -10,8 +10,8 @@ import {
useRef,
} from 'react';
import { useApiContext, useServerContext } from '@/ui/contexts/api';
import type { BrowserFetcherOptions, FetchResult } from '@/playground/fetcher';
import { getStatusInfo } from './status-info';
import type { BrowserFetcherOptions } from '@/playground/fetcher';
import { DefaultResultDisplay, type ResultDisplayProps } from './components/result-display';
import { joinURL, resolveRequestData, resolveServerUrl, withBase } from '@/utils/url';
import { MethodLabel } from '@/ui/components/method-label';
import { useQuery } from '@/utils/use-query';
@@ -48,7 +48,6 @@ import {
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 { ClientCodeBlock } from '@/ui/components/codeblock';
import { useTranslations } from '@/ui/client/i18n';
import { useOperationContext } from '@/ui/operation/client';
import { OAuthDialog, OAuthDialogContent, OAuthDialogTrigger } from './components/oauth-dialog';
@@ -74,10 +73,8 @@ export interface PlaygroundClientProps extends ComponentProps<'form'>, SchemaSco
proxyUrl?: string;
}
export interface ResultDisplayProps extends ComponentProps<'div'> {
data: FetchResult;
reset: () => void;
}
export type { ResultDisplayProps };
export { DefaultResultDisplay };
export interface CollapsiblePanelProps extends Omit<ComponentProps<typeof Collapsible>, 'title'> {
'data-type': 'authorization' | 'body' | ParamType;
@@ -788,42 +785,6 @@ function Route({ route, ...props }: ComponentProps<'div'> & { route: string }) {
);
}
export function DefaultResultDisplay({ data, reset, ...rest }: ResultDisplayProps) {
const t = useTranslations();
const statusInfo = useMemo(() => getStatusInfo(data.status, t), [data.status, t]);
return (
<div
{...rest}
className={cn(
'flex flex-col gap-3 mt-2 px-3 py-2 border-y bg-fd-secondary text-fd-secondary-foreground',
rest.className,
)}
>
<div className="flex justify-between items-center">
<div className="inline-flex items-center gap-1.5 text-sm font-medium">
<statusInfo.icon className={cn('size-4', statusInfo.color)} />
{statusInfo.description}
</div>
<button
type="button"
className={cn(buttonVariants({ size: 'sm', variant: 'outline' }))}
onClick={() => reset()}
>
{t.close}
</button>
</div>
<p className="text-sm text-fd-muted-foreground">{data.status}</p>
{data.data !== undefined && (
<ClientCodeBlock
lang={typeof data.data === 'string' && data.data.length > 50000 ? 'text' : data.type}
code={typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2)}
/>
)}
</div>
);
}
export function DefaultCollapsiblePanel({ title, children, ...props }: CollapsiblePanelProps) {
return (
<Collapsible {...props} className={cn('border-b last:border-b-0', props.className)}>
@@ -0,0 +1,161 @@
'use client';
import { ReactNode, useEffect, useMemo, useState, type ComponentProps } from 'react';
import { CircleX } from 'lucide-react';
import type { FetchResponseResult, FetchResult } from '@/playground/fetcher';
import { useStatusInfo } from '../status-info';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { cn } from '@/utils/cn';
import { ClientCodeBlock } from '@/ui/components/codeblock';
import { useTranslations, withReplacements } from '@/ui/client/i18n';
import { safeParse } from 'fast-content-type-parse';
import { cva } from 'class-variance-authority';
import type { BuiltinLanguage, SpecialLanguage } from 'shiki';
export interface ResultDisplayProps extends ComponentProps<'div'> {
data: FetchResult;
reset: () => void;
}
const panelVariants = cva(
'flex flex-col gap-2 mt-2 px-3 py-2 border-y bg-fd-secondary text-fd-secondary-foreground',
);
export function DefaultResultDisplay({ data, reset, ...rest }: ResultDisplayProps) {
const t = useTranslations();
if (data.type === 'client_error') {
return (
<div {...rest} className={cn(panelVariants(), rest.className)}>
<div className="flex gap-1.5 items-center">
<CircleX className="size-4 text-red-500" />
<p className="text-sm font-medium me-auto">{t.statusClientError}</p>
<button
type="button"
className={cn(buttonVariants({ size: 'sm', variant: 'outline' }))}
onClick={() => reset()}
>
{t.close}
</button>
</div>
<p>{data.message}</p>
</div>
);
}
return <ResponseResult data={data} reset={reset} {...rest} />;
}
function getTextFormat(mime: string): BuiltinLanguage | SpecialLanguage | null {
switch (mime) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types
case 'application/json':
return 'json';
case 'text/html':
return 'html';
case 'text/css':
return 'css';
case 'text/csv':
return 'csv';
case 'application/javascript':
case 'application/x-javascript':
return 'js';
case 'application/xml':
return 'xml';
}
if (mime.endsWith('+json')) return 'json';
if (mime.endsWith('+xml')) return 'xml';
if (mime.startsWith('text/')) return 'text';
return null;
}
function ResponseResult({
data,
reset,
...rest
}: ComponentProps<'div'> & {
data: FetchResponseResult;
reset: () => void;
}) {
const t = useTranslations();
const statusInfo = useStatusInfo(data.status);
const { parameters, type } = useMemo(
() => safeParse(data.headers.get('Content-Type') ?? 'text/plain'),
[data.headers],
);
let content: ReactNode;
if (type.startsWith('image/')) {
content = <ImageResult mime={type} buffer={data.body} />;
} else if (data.body.byteLength > 0) {
const lang = getTextFormat(type);
if (lang) {
content = <TextResult lang={lang} charset={parameters.charset} data={data} />;
} else {
content = (
<p className="p-2 border rounded-lg bg-fd-card text-fd-card-foreground">
{withReplacements(t.statusBinaryBody, { length: String(data.body.byteLength) })}
</p>
);
}
}
return (
<div {...rest} className={cn(panelVariants(), rest.className)}>
<div className="flex items-center gap-1.5">
<statusInfo.icon className={cn('size-4 shrink-0', statusInfo.color)} />
<p className="text-sm font-medium text-nowrap">
{data.status} {statusInfo.description}
</p>
<code className="ms-auto text-xs text-fd-muted-foreground truncate">{type}</code>
<button
type="button"
className={cn(buttonVariants({ size: 'sm', variant: 'outline' }))}
onClick={() => reset()}
>
{t.close}
</button>
</div>
{content}
</div>
);
}
function TextResult({
lang,
charset,
data,
}: {
lang: BuiltinLanguage | SpecialLanguage;
data: FetchResponseResult;
charset?: string;
}) {
const code = useMemo(() => {
if (charset) {
try {
return new TextDecoder(charset).decode(data.body);
} catch {
/* invalid label — fall through */
}
}
return new TextDecoder('utf-8').decode(data.body);
}, [charset, data.body]);
return <ClientCodeBlock lang={code.length > 5000 ? 'text' : lang} code={code} />;
}
function ImageResult({ mime, buffer }: { mime: string; buffer: ArrayBuffer }) {
const [objectUrl, setObjectUrl] = useState<string | null>(null);
useEffect(() => {
const blob = new Blob([buffer], { type: mime });
const url = URL.createObjectURL(blob);
setObjectUrl(url);
return () => URL.revokeObjectURL(url);
}, [mime, buffer]);
if (!objectUrl) return;
return <img src={objectUrl} alt="" className="w-full rounded-md border border-fd-border" />;
}
+23 -24
View File
@@ -3,10 +3,18 @@ import type { MediaAdapter } from '@/requests/media/adapter';
import { resolveMediaAdapter } from '@/requests/media/adapter';
import type { Awaitable } from '@/types';
export interface FetchResult {
export type FetchResult = FetchResponseResult | FetchErrorResult;
export interface FetchErrorResult {
type: 'client_error';
message: string;
}
export interface FetchResponseResult {
type: 'response';
status: number;
type: 'json' | 'html' | 'text';
data: unknown;
headers: Headers;
body: ArrayBuffer;
}
export interface Fetcher {
@@ -75,9 +83,8 @@ export function createBrowserFetcher(
const adapter = resolveMediaAdapter(data.bodyMediaType, adapters);
if (!adapter)
return {
status: 400,
type: 'text',
data: `[Fumadocs] No adapter for ${data.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
type: 'client_error',
message: `[Fumadocs] No adapter for ${data.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
};
if (data.bodyMediaType !== 'multipart/form-data') {
@@ -110,28 +117,20 @@ export function createBrowserFetcher(
if (onRequestInit) requestInit = await onRequestInit(requestInit);
return fetch(requestUrl, requestInit)
.then(async (res) => {
const contentType = res.headers.get('Content-Type') ?? '';
let type: FetchResult['type'];
let data: unknown;
if (contentType.startsWith('application/json')) {
type = 'json';
data = await res.json();
} else {
type = contentType.startsWith('text/html') ? 'html' : 'text';
data = await res.text();
}
return { status: res.status, type, data };
.then(async (res): Promise<FetchResult> => {
return {
type: 'response',
status: res.status,
headers: res.headers,
body: await res.arrayBuffer(),
};
})
.catch((e) => {
.catch((e): FetchResult => {
const message = e instanceof Error ? `[${e.name}] ${e.message}` : e.toString();
return {
status: 400,
type: 'text',
data: `Client side error: ${message}`,
type: 'client_error',
message: `Client side error: ${message}`,
};
});
},
+26 -21
View File
@@ -1,8 +1,10 @@
import { CircleCheck, CircleX } from 'lucide-react';
import type { Translations } from '@/i18n';
import { useTranslations } from '@/ui/client/i18n';
import { useMemo } from 'react';
interface StatusInfo {
description: string;
description?: string;
color: string;
icon: React.ElementType;
}
@@ -18,27 +20,30 @@ const statusKeys: Record<
500: { key: 'statusInternalServerError', color: 'text-red-500', icon: CircleX },
};
export function getStatusInfo(status: number, t: Translations): StatusInfo {
if (status in statusKeys) {
const { key, color, icon } = statusKeys[status];
return { description: t[key], color, icon };
}
export function useStatusInfo(status: number): StatusInfo {
const t = useTranslations();
return useMemo(() => {
if (status in statusKeys) {
const { key, color, icon } = statusKeys[status];
return { description: t[key], color, icon };
}
if (status >= 200 && status < 300) {
return {
description: t.statusSuccessful,
color: 'text-green-500',
icon: CircleCheck,
};
}
if (status >= 400) {
return { description: t.statusError, color: 'text-red-500', icon: CircleX };
}
if (status >= 200 && status < 300) {
return {
description: t.statusSuccessful,
color: 'text-green-500',
icon: CircleCheck,
color: 'text-fd-muted-foreground',
icon: CircleX,
};
}
if (status >= 400) {
return { description: t.statusError, color: 'text-red-500', icon: CircleX };
}
return {
description: t.statusNoDescription,
color: 'text-fd-muted-foreground',
icon: CircleX,
};
}, [t, status]);
}
+6 -3
View File
@@ -17,9 +17,12 @@ export interface OpenAPII18nLabelProps {
*/
export function I18nLabel({ label, replacements = {} }: OpenAPII18nLabelProps): string {
const text = useTranslations();
let value = text[label];
return withReplacements(text[label], replacements);
}
export function withReplacements(t: string, replacements: Record<string, string>) {
for (const [k, v] of Object.entries(replacements)) {
value = value.replaceAll(`{${k}}`, v);
t = t.replaceAll(`{${k}}`, v);
}
return value;
return t;
}
+58 -23
View File
@@ -145,7 +145,7 @@ importers:
version: 12.38.0([email protected]([email protected]))([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next-themes:
specifier: ^0.4.6
version: 0.4.6([email protected]([email protected]))([email protected])
@@ -251,7 +251,7 @@ importers:
version: 4.0.3
mcp-handler:
specifier: ^1.1.0
version: 1.1.0(@modelcontextprotocol/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 1.1.0(@modelcontextprotocol/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
next-validate-link:
specifier: ^1.6.4
version: 1.6.4
@@ -345,7 +345,7 @@ importers:
version: 1.7.0([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -394,7 +394,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -440,7 +440,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -477,7 +477,7 @@ importers:
version: 5.2.17(@types/[email protected])([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -526,7 +526,7 @@ importers:
version: 1.7.0([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -575,7 +575,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -624,7 +624,7 @@ importers:
version: 1.7.0([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -679,7 +679,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -725,7 +725,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -777,7 +777,7 @@ importers:
version: link:../../packages/radix-ui
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -1682,7 +1682,7 @@ importers:
version: 12.38.0([email protected]([email protected]))([email protected])
next:
specifier: 16.x.x
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next-themes:
specifier: ^0.4.6
version: 0.4.6([email protected]([email protected]))([email protected])
@@ -1978,7 +1978,7 @@ importers:
version: 1.7.0([email protected])
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
react-router:
specifier: ^7.13.2
version: 7.13.2([email protected]([email protected]))([email protected])
@@ -2276,7 +2276,7 @@ importers:
version: 3.1.0
next:
specifier: 16.2.2
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react:
specifier: ^19.2.4
version: 19.2.4
@@ -2468,6 +2468,9 @@ importers:
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
fast-content-type-parse:
specifier: ^3.0.0
version: 3.0.0
github-slugger:
specifier: ^2.0.0
version: 2.0.0
@@ -2794,7 +2797,7 @@ importers:
version: 12.38.0([email protected]([email protected]))([email protected])
next:
specifier: 16.x.x
version: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
version: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next-themes:
specifier: ^0.4.6
version: 0.4.6([email protected]([email protected]))([email protected])
@@ -7579,6 +7582,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -13060,7 +13064,7 @@ snapshots:
dependencies:
'@content-collections/core': 0.14.3([email protected])
'@content-collections/integrations': 0.5.0(@content-collections/[email protected]([email protected]))
next: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
'@dimforge/[email protected]': {}
@@ -18351,7 +18355,7 @@ snapshots:
optionalDependencies:
'@types/mdast': 4.0.4
'@types/mdx': 2.0.13
next: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
react: 19.2.4
vite: 8.0.3(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
vue: 3.5.31([email protected])
@@ -19164,14 +19168,14 @@ snapshots:
[email protected]: {}
[email protected](@modelcontextprotocol/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])):
[email protected](@modelcontextprotocol/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])):
dependencies:
'@modelcontextprotocol/sdk': 1.29.0([email protected])
chalk: 5.6.2
commander: 11.1.0
redis: 4.7.1
optionalDependencies:
next: 16.2.2(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
next: 16.2.2(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
[email protected]:
dependencies:
@@ -19913,7 +19917,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]):
[email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected]):
dependencies:
'@next/env': 16.2.2
'@swc/helpers': 0.5.15
@@ -19922,7 +19926,32 @@ snapshots:
postcss: 8.4.31
react: 19.2.4
react-dom: 19.2.4([email protected])
styled-jsx: 5.1.6(@babel/[email protected])([email protected])([email protected])
styled-jsx: 5.1.6(@babel/[email protected])([email protected])
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.2
'@next/swc-darwin-x64': 16.2.2
'@next/swc-linux-arm64-gnu': 16.2.2
'@next/swc-linux-arm64-musl': 16.2.2
'@next/swc-linux-x64-gnu': 16.2.2
'@next/swc-linux-x64-musl': 16.2.2
'@next/swc-win32-arm64-msvc': 16.2.2
'@next/swc-win32-x64-msvc': 16.2.2
'@opentelemetry/api': 1.9.1
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
[email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]):
dependencies:
'@next/env': 16.2.2
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.13
caniuse-lite: 1.0.30001784
postcss: 8.4.31
react: 19.2.4
react-dom: 19.2.4([email protected])
styled-jsx: 5.1.6([email protected])([email protected])
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.2
'@next/swc-darwin-x64': 16.2.2
@@ -21534,12 +21563,18 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
[email protected](@babel/[email protected])([email protected])([email protected]):
[email protected](@babel/[email protected])([email protected]):
dependencies:
client-only: 0.0.1
react: 19.2.4
optionalDependencies:
'@babel/core': 7.29.0
[email protected]([email protected])([email protected]):
dependencies:
client-only: 0.0.1
react: 19.2.4
optionalDependencies:
babel-plugin-macros: 3.1.0
[email protected]: {}