mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-28 00:15:01 +00:00
UI: support .core exports for dynamic codeblocks
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@fumadocs/base-ui": patch
|
||||
"@fumadocs/ui": patch
|
||||
---
|
||||
|
||||
support `.core` exports for dynamic codeblocks
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
import { CodeBlock, type CodeBlockProps, Pre } from '@/components/codeblock';
|
||||
import { useShiki, type UseShikiOptions } from 'fumadocs-core/highlight/core/client';
|
||||
import { cn } from '@fumadocs/ui/cn';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
Suspense,
|
||||
use,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
export interface DynamicCodeblockProps {
|
||||
lang: string;
|
||||
code: string;
|
||||
/**
|
||||
* Extra props for the underlying `<CodeBlock />` component.
|
||||
*
|
||||
* Ignored if you defined your own `pre` component in `options.components`.
|
||||
*/
|
||||
codeblock?: CodeBlockProps;
|
||||
/**
|
||||
* Wrap in React `<Suspense />` and provide a fallback.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
wrapInSuspense?: boolean;
|
||||
options?: DistributiveOmit<UseShikiOptions, 'lang'>;
|
||||
}
|
||||
|
||||
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
||||
|
||||
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
|
||||
|
||||
function DefaultPre(props: ComponentProps<'pre'>) {
|
||||
const extraProps = use(PropsContext);
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
{...props}
|
||||
{...extraProps}
|
||||
className={cn('my-0', props.className, extraProps?.className)}
|
||||
>
|
||||
<Pre>{props.children}</Pre>
|
||||
</CodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicCodeBlock({
|
||||
lang,
|
||||
code,
|
||||
codeblock,
|
||||
options,
|
||||
wrapInSuspense = true,
|
||||
}: DynamicCodeblockProps) {
|
||||
const id = useId();
|
||||
const shikiOptions: UseShikiOptions = {
|
||||
lang,
|
||||
...options,
|
||||
components: {
|
||||
pre: DefaultPre,
|
||||
...options?.components,
|
||||
},
|
||||
};
|
||||
|
||||
const children = (
|
||||
<PropsContext value={codeblock}>
|
||||
<Internal id={id} {...useDeferredValue({ code, options: shikiOptions })} />
|
||||
</PropsContext>
|
||||
);
|
||||
|
||||
if (wrapInSuspense)
|
||||
return (
|
||||
<Suspense fallback={<Placeholder code={code} components={shikiOptions.components} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function Placeholder({
|
||||
code,
|
||||
components = {},
|
||||
}: {
|
||||
code: string;
|
||||
components: UseShikiOptions['components'];
|
||||
}) {
|
||||
const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<string, FC>;
|
||||
|
||||
return (
|
||||
<Pre>
|
||||
<Code>
|
||||
{code.split('\n').map((line, i) => (
|
||||
<span key={i} className="line">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
);
|
||||
}
|
||||
|
||||
function Internal({ id, code, options }: { id: string; code: string; options: UseShikiOptions }) {
|
||||
return useShiki(code, options, [id, options.lang, code]);
|
||||
}
|
||||
@@ -1,111 +1,19 @@
|
||||
'use client';
|
||||
import { CodeBlock, type CodeBlockProps, Pre } from '@/components/codeblock';
|
||||
import type {
|
||||
HighlightOptions,
|
||||
HighlightOptionsCommon,
|
||||
HighlightOptionsThemes,
|
||||
} from 'fumadocs-core/highlight';
|
||||
import { useShiki } from 'fumadocs-core/highlight/client';
|
||||
import { cn } from '@fumadocs/ui/cn';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
Suspense,
|
||||
use,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
export interface DynamicCodeblockProps {
|
||||
lang: string;
|
||||
code: string;
|
||||
/**
|
||||
* Extra props for the underlying `<CodeBlock />` component.
|
||||
*
|
||||
* Ignored if you defined your own `pre` component in `options.components`.
|
||||
*/
|
||||
codeblock?: CodeBlockProps;
|
||||
/**
|
||||
* Wrap in React `<Suspense />` and provide a fallback.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
wrapInSuspense?: boolean;
|
||||
options?: Omit<HighlightOptionsCommon, 'lang'> & HighlightOptionsThemes;
|
||||
}
|
||||
|
||||
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
|
||||
|
||||
function DefaultPre(props: ComponentProps<'pre'>) {
|
||||
const extraProps = use(PropsContext);
|
||||
import { useShikiConfigOptional } from 'fumadocs-core/highlight/core/client';
|
||||
import * as base from './dynamic-codeblock.core';
|
||||
import { withJSEngine } from 'fumadocs-core/highlight/full/config';
|
||||
|
||||
export function DynamicCodeBlock(props: base.DynamicCodeblockProps) {
|
||||
const config = useShikiConfigOptional() ?? withJSEngine;
|
||||
return (
|
||||
<CodeBlock
|
||||
<base.DynamicCodeBlock
|
||||
{...props}
|
||||
{...extraProps}
|
||||
className={cn('my-0', props.className, extraProps?.className)}
|
||||
>
|
||||
<Pre>{props.children}</Pre>
|
||||
</CodeBlock>
|
||||
options={{
|
||||
config,
|
||||
...props.options,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicCodeBlock({
|
||||
lang,
|
||||
code,
|
||||
codeblock,
|
||||
options,
|
||||
wrapInSuspense = true,
|
||||
}: DynamicCodeblockProps) {
|
||||
const id = useId();
|
||||
const shikiOptions = {
|
||||
lang,
|
||||
...options,
|
||||
components: {
|
||||
pre: DefaultPre,
|
||||
...options?.components,
|
||||
},
|
||||
} satisfies HighlightOptions;
|
||||
|
||||
const children = (
|
||||
<PropsContext value={codeblock}>
|
||||
<Internal id={id} {...useDeferredValue({ code, options: shikiOptions })} />
|
||||
</PropsContext>
|
||||
);
|
||||
|
||||
if (wrapInSuspense)
|
||||
return (
|
||||
<Suspense fallback={<Placeholder code={code} components={shikiOptions.components} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function Placeholder({
|
||||
code,
|
||||
components = {},
|
||||
}: {
|
||||
code: string;
|
||||
components: HighlightOptions['components'];
|
||||
}) {
|
||||
const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<string, FC>;
|
||||
|
||||
return (
|
||||
<Pre>
|
||||
<Code>
|
||||
{code.split('\n').map((line, i) => (
|
||||
<span key={i} className="line">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
);
|
||||
}
|
||||
|
||||
function Internal({ id, code, options }: { id: string; code: string; options: HighlightOptions }) {
|
||||
return useShiki(code, options, [id, options.lang, code]);
|
||||
}
|
||||
export type { DynamicCodeblockProps } from './dynamic-codeblock.core';
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
import { CodeBlock, type CodeBlockProps, Pre } from '@/components/codeblock';
|
||||
import { useShiki, type UseShikiOptions } from 'fumadocs-core/highlight/core/client';
|
||||
import { cn } from '@fumadocs/ui/cn';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
Suspense,
|
||||
use,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
export interface DynamicCodeblockProps {
|
||||
lang: string;
|
||||
code: string;
|
||||
/**
|
||||
* Extra props for the underlying `<CodeBlock />` component.
|
||||
*
|
||||
* Ignored if you defined your own `pre` component in `options.components`.
|
||||
*/
|
||||
codeblock?: CodeBlockProps;
|
||||
/**
|
||||
* Wrap in React `<Suspense />` and provide a fallback.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
wrapInSuspense?: boolean;
|
||||
options?: DistributiveOmit<UseShikiOptions, 'lang'>;
|
||||
}
|
||||
|
||||
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
||||
|
||||
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
|
||||
|
||||
function DefaultPre(props: ComponentProps<'pre'>) {
|
||||
const extraProps = use(PropsContext);
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
{...props}
|
||||
{...extraProps}
|
||||
className={cn('my-0', props.className, extraProps?.className)}
|
||||
>
|
||||
<Pre>{props.children}</Pre>
|
||||
</CodeBlock>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicCodeBlock({
|
||||
lang,
|
||||
code,
|
||||
codeblock,
|
||||
options,
|
||||
wrapInSuspense = true,
|
||||
}: DynamicCodeblockProps) {
|
||||
const id = useId();
|
||||
const shikiOptions: UseShikiOptions = {
|
||||
lang,
|
||||
...options,
|
||||
components: {
|
||||
pre: DefaultPre,
|
||||
...options?.components,
|
||||
},
|
||||
};
|
||||
|
||||
const children = (
|
||||
<PropsContext value={codeblock}>
|
||||
<Internal id={id} {...useDeferredValue({ code, options: shikiOptions })} />
|
||||
</PropsContext>
|
||||
);
|
||||
|
||||
if (wrapInSuspense)
|
||||
return (
|
||||
<Suspense fallback={<Placeholder code={code} components={shikiOptions.components} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function Placeholder({
|
||||
code,
|
||||
components = {},
|
||||
}: {
|
||||
code: string;
|
||||
components: UseShikiOptions['components'];
|
||||
}) {
|
||||
const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<string, FC>;
|
||||
|
||||
return (
|
||||
<Pre>
|
||||
<Code>
|
||||
{code.split('\n').map((line, i) => (
|
||||
<span key={i} className="line">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
);
|
||||
}
|
||||
|
||||
function Internal({ id, code, options }: { id: string; code: string; options: UseShikiOptions }) {
|
||||
return useShiki(code, options, [id, options.lang, code]);
|
||||
}
|
||||
@@ -1,109 +1,19 @@
|
||||
'use client';
|
||||
import { CodeBlock, type CodeBlockProps, Pre } from '@/components/codeblock';
|
||||
import type { HighlightOptions } from 'fumadocs-core/highlight';
|
||||
import { useShiki } from 'fumadocs-core/highlight/client';
|
||||
import { cn } from '@fumadocs/ui/cn';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
Suspense,
|
||||
use,
|
||||
useDeferredValue,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
export interface DynamicCodeblockProps {
|
||||
lang: string;
|
||||
code: string;
|
||||
/**
|
||||
* Extra props for the underlying `<CodeBlock />` component.
|
||||
*
|
||||
* Ignored if you defined your own `pre` component in `options.components`.
|
||||
*/
|
||||
codeblock?: CodeBlockProps;
|
||||
/**
|
||||
* Wrap in React `<Suspense />` and provide a fallback.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
wrapInSuspense?: boolean;
|
||||
options?: DistributiveOmit<HighlightOptions, 'lang'>;
|
||||
}
|
||||
|
||||
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
||||
|
||||
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
|
||||
|
||||
function DefaultPre(props: ComponentProps<'pre'>) {
|
||||
const extraProps = use(PropsContext);
|
||||
import { useShikiConfigOptional } from 'fumadocs-core/highlight/core/client';
|
||||
import * as base from './dynamic-codeblock.core';
|
||||
import { withJSEngine } from 'fumadocs-core/highlight/full/config';
|
||||
|
||||
export function DynamicCodeBlock(props: base.DynamicCodeblockProps) {
|
||||
const config = useShikiConfigOptional() ?? withJSEngine;
|
||||
return (
|
||||
<CodeBlock
|
||||
<base.DynamicCodeBlock
|
||||
{...props}
|
||||
{...extraProps}
|
||||
className={cn('my-0', props.className, extraProps?.className)}
|
||||
>
|
||||
<Pre>{props.children}</Pre>
|
||||
</CodeBlock>
|
||||
options={{
|
||||
config,
|
||||
...props.options,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DynamicCodeBlock({
|
||||
lang,
|
||||
code,
|
||||
codeblock,
|
||||
options,
|
||||
wrapInSuspense = true,
|
||||
}: DynamicCodeblockProps) {
|
||||
const id = useId();
|
||||
const shikiOptions = {
|
||||
lang,
|
||||
...options,
|
||||
components: {
|
||||
pre: DefaultPre,
|
||||
...options?.components,
|
||||
},
|
||||
} satisfies HighlightOptions;
|
||||
|
||||
const children = (
|
||||
<PropsContext value={codeblock}>
|
||||
<Internal id={id} {...useDeferredValue({ code, options: shikiOptions })} />
|
||||
</PropsContext>
|
||||
);
|
||||
|
||||
if (wrapInSuspense)
|
||||
return (
|
||||
<Suspense fallback={<Placeholder code={code} components={shikiOptions.components} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function Placeholder({
|
||||
code,
|
||||
components = {},
|
||||
}: {
|
||||
code: string;
|
||||
components: HighlightOptions['components'];
|
||||
}) {
|
||||
const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<string, FC>;
|
||||
|
||||
return (
|
||||
<Pre>
|
||||
<Code>
|
||||
{code.split('\n').map((line, i) => (
|
||||
<span key={i} className="line">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
);
|
||||
}
|
||||
|
||||
function Internal({ id, code, options }: { id: string; code: string; options: HighlightOptions }) {
|
||||
return useShiki(code, options, [id, options.lang, code]);
|
||||
}
|
||||
export type { DynamicCodeblockProps } from './dynamic-codeblock.core';
|
||||
|
||||
@@ -21,7 +21,7 @@ const installables = new Map<
|
||||
>();
|
||||
|
||||
/** file path (relative to registry dir) -> import specifier */
|
||||
const forwardables = new Map<string, string>();
|
||||
const forwardedFiles = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* - transform import to `@fumadocs/ui` into sub component references, or forward to upstream package instead.
|
||||
@@ -79,7 +79,7 @@ export function resolveForwardedAPIs(
|
||||
return v;
|
||||
})
|
||||
.join('/');
|
||||
const forwarded = forwardables.get(filePath);
|
||||
const forwarded = forwardedFiles.get(filePath);
|
||||
|
||||
if (forwarded)
|
||||
return resolveForwardedAPIs(
|
||||
@@ -228,7 +228,11 @@ for (const comp of registry.components) {
|
||||
}
|
||||
}
|
||||
|
||||
forwardables.set('i18n.tsx', '@fumadocs/ui/i18n');
|
||||
for await (const file of new Glob('{contexts,components,hooks}/**/*').scan(srcDir)) {
|
||||
forwardables.set(file, `@fumadocs/ui/${removeExtname(file)}`);
|
||||
forwardedFiles.set('i18n.tsx', '@fumadocs/ui/i18n');
|
||||
for await (const file of new Glob('{contexts,hooks}/**/*').scan(srcDir)) {
|
||||
forwardedFiles.set(file, `@fumadocs/ui/${removeExtname(file)}`);
|
||||
}
|
||||
|
||||
for await (const file of new Glob('components/toc/**/*').scan(srcDir)) {
|
||||
forwardedFiles.set(file, `@fumadocs/ui/${removeExtname(file)}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user