e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
369 lines
9.9 KiB
TypeScript
369 lines
9.9 KiB
TypeScript
import {
|
|
Fragment,
|
|
cloneElement,
|
|
isValidElement,
|
|
type ReactElement,
|
|
type ReactNode,
|
|
} from "react";
|
|
import rehypeParse from "rehype-parse";
|
|
import rehypeRemark from "rehype-remark";
|
|
import remarkGfm from "remark-gfm";
|
|
import remarkStringify from "remark-stringify";
|
|
import { unified } from "unified";
|
|
import { AGENT_DOCS_DIRECTIVE_MARKDOWN } from "@/lib/agent-docs-directive";
|
|
import { LLM_COMPONENTS } from "@/lib/llm-components";
|
|
import type { examples, source, tapDocs } from "@/lib/source";
|
|
import type { InferPageType } from "fumadocs-core/source";
|
|
|
|
const processor = unified()
|
|
.use(rehypeParse, { fragment: true })
|
|
.use(rehypeRemark)
|
|
.use(remarkGfm)
|
|
.use(remarkStringify, {
|
|
bullet: "-",
|
|
fences: true,
|
|
rule: "-",
|
|
});
|
|
|
|
const OMITTED_STATIC_PROP_NAMES = new Set([
|
|
"children",
|
|
"className",
|
|
"style",
|
|
"id",
|
|
"icon",
|
|
"ref",
|
|
"key",
|
|
"tabIndex",
|
|
"data-line-numbers",
|
|
"src",
|
|
"sizes",
|
|
"width",
|
|
"height",
|
|
"blurWidth",
|
|
"blurHeight",
|
|
"codeblock",
|
|
"loading",
|
|
"priority",
|
|
]);
|
|
|
|
export type LLMRenderContext = { flavor: "radix" | "base" };
|
|
|
|
const DEFAULT_LLM_CONTEXT: LLMRenderContext = { flavor: "base" };
|
|
|
|
type StaticFunctionComponent = (
|
|
props: Record<string, unknown>,
|
|
ctx?: LLMRenderContext,
|
|
) => ReactNode | Promise<ReactNode>;
|
|
|
|
// Marker that React/Next.js stamp onto `"use client"` module exports. Part of
|
|
// the cross-bundler RSC contract, so stable across upgrades.
|
|
const REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference");
|
|
|
|
function isClientReference(type: unknown): boolean {
|
|
return (
|
|
(typeof type === "object" || typeof type === "function") &&
|
|
type !== null &&
|
|
(type as { $$typeof?: symbol }).$$typeof === REACT_CLIENT_REFERENCE
|
|
);
|
|
}
|
|
|
|
// Directly-imported MDX components (e.g. PreviewCode) bypass the LLM map, so they
|
|
// carry their text rendering as a `.llm` static instead.
|
|
function getLLMVariant(type: unknown): StaticFunctionComponent | undefined {
|
|
if (
|
|
(typeof type === "function" || typeof type === "object") &&
|
|
type !== null
|
|
) {
|
|
const llm = (type as { llm?: unknown }).llm;
|
|
if (typeof llm === "function") return llm as StaticFunctionComponent;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function componentDisplayName(type: unknown): string {
|
|
if (type == null) return "";
|
|
const named = type as { displayName?: unknown; name?: unknown };
|
|
if (typeof named.displayName === "string" && named.displayName)
|
|
return named.displayName;
|
|
if (typeof named.name === "string" && named.name) return named.name;
|
|
return "";
|
|
}
|
|
|
|
// Safety net for server components that throw because of transitive
|
|
// client-only API use. Stack frames are more durable than error messages.
|
|
function looksLikeStaticRenderFailure(error: unknown): boolean {
|
|
if (!(error instanceof Error)) return false;
|
|
|
|
const stack = error.stack ?? "";
|
|
if (
|
|
stack.includes("/react-dom/") ||
|
|
stack.includes("/react-server-dom") ||
|
|
stack.includes("/react/cjs/react.") ||
|
|
stack.includes("react-dom.") ||
|
|
stack.includes("react.development") ||
|
|
stack.includes("react.production")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return /invalid hook call|client reference|use client/i.test(error.message);
|
|
}
|
|
|
|
function isEmptyNode(node: ReactNode): boolean {
|
|
return (
|
|
node == null ||
|
|
typeof node === "boolean" ||
|
|
(Array.isArray(node) && node.every(isEmptyNode))
|
|
);
|
|
}
|
|
|
|
function withStableArrayKeys(nodes: ReactNode[]): ReactNode[] {
|
|
return nodes.map((node, index) => {
|
|
if (Array.isArray(node)) return withStableArrayKeys(node);
|
|
if (!isValidElement(node) || node.key != null) return node;
|
|
return cloneElement(node, { key: index });
|
|
});
|
|
}
|
|
|
|
function getStaticEntries(props: Record<string, unknown>) {
|
|
return Object.entries(props).filter(
|
|
([key, value]) =>
|
|
!OMITTED_STATIC_PROP_NAMES.has(key) &&
|
|
!key.startsWith("on") &&
|
|
value != null &&
|
|
typeof value !== "boolean" &&
|
|
typeof value !== "function",
|
|
);
|
|
}
|
|
|
|
async function renderDescriptionList(
|
|
entries: Array<[string, unknown]>,
|
|
ctx: LLMRenderContext,
|
|
): Promise<ReactNode> {
|
|
const renderedEntries = (
|
|
await Promise.all(
|
|
entries.map(async ([key, value]) => {
|
|
const rendered = await renderStaticValue(value, ctx);
|
|
if (isEmptyNode(rendered)) return null;
|
|
|
|
return (
|
|
<Fragment key={key}>
|
|
<dt>{key}</dt>
|
|
<dd>{rendered}</dd>
|
|
</Fragment>
|
|
);
|
|
}),
|
|
)
|
|
).filter((entry): entry is ReactElement => entry != null);
|
|
|
|
return renderedEntries.length > 0 ? <dl>{renderedEntries}</dl> : null;
|
|
}
|
|
|
|
async function renderStaticValue(
|
|
value: unknown,
|
|
ctx: LLMRenderContext,
|
|
): Promise<ReactNode> {
|
|
if (value == null || typeof value === "boolean") return null;
|
|
|
|
if (
|
|
typeof value === "string" ||
|
|
typeof value === "number" ||
|
|
typeof value === "bigint"
|
|
) {
|
|
return <span>{String(value)}</span>;
|
|
}
|
|
|
|
if (isValidElement(value)) {
|
|
return resolveStaticReactNode(value, ctx);
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
const items = await Promise.all(
|
|
value.map((item) => renderStaticValue(item, ctx)),
|
|
);
|
|
const visibleItems = items.filter((item) => !isEmptyNode(item));
|
|
if (visibleItems.length === 0) return null;
|
|
|
|
return (
|
|
<ul>
|
|
{visibleItems.map((item, index) => (
|
|
<li key={index}>{item}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
if (typeof value === "object") {
|
|
return renderDescriptionList(
|
|
Object.entries(value).filter(
|
|
([, entryValue]) =>
|
|
entryValue != null &&
|
|
typeof entryValue !== "boolean" &&
|
|
typeof entryValue !== "function",
|
|
),
|
|
ctx,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function renderClientFallback(
|
|
props: Record<string, unknown>,
|
|
children: ReactNode,
|
|
ctx: LLMRenderContext,
|
|
): Promise<ReactNode> {
|
|
const data = await renderDescriptionList(getStaticEntries(props), ctx);
|
|
|
|
if (!isEmptyNode(children) && !isEmptyNode(data)) {
|
|
return (
|
|
<section>
|
|
{data}
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!isEmptyNode(children)) return <>{children}</>;
|
|
return data;
|
|
}
|
|
|
|
async function resolveStaticReactNode(
|
|
node: ReactNode,
|
|
ctx: LLMRenderContext,
|
|
): Promise<ReactNode> {
|
|
if (
|
|
node == null ||
|
|
typeof node === "boolean" ||
|
|
typeof node === "string" ||
|
|
typeof node === "number" ||
|
|
typeof node === "bigint"
|
|
) {
|
|
return node;
|
|
}
|
|
|
|
if (Array.isArray(node)) {
|
|
return withStableArrayKeys(
|
|
await Promise.all(node.map((item) => resolveStaticReactNode(item, ctx))),
|
|
);
|
|
}
|
|
|
|
if (!isValidElement(node)) {
|
|
return null;
|
|
}
|
|
|
|
const element = node as ReactElement<Record<string, unknown>>;
|
|
const { children, ...props } = element.props;
|
|
|
|
if (element.type === Fragment) {
|
|
const resolvedChildren = await resolveStaticReactNode(
|
|
children as ReactNode,
|
|
ctx,
|
|
);
|
|
return <>{resolvedChildren}</>;
|
|
}
|
|
|
|
if (typeof element.type === "string") {
|
|
const resolvedChildren = await resolveStaticReactNode(
|
|
children as ReactNode,
|
|
ctx,
|
|
);
|
|
return cloneElement(element, props, resolvedChildren);
|
|
}
|
|
|
|
if (isClientReference(element.type)) {
|
|
const resolvedChildren = await resolveStaticReactNode(
|
|
children as ReactNode,
|
|
ctx,
|
|
);
|
|
// Turn links (e.g. next/link) into real anchors so they survive as markdown
|
|
// links, not a dumped `href` entry.
|
|
const href = props.href;
|
|
if (typeof href === "string" && href) {
|
|
return <a href={href}>{resolvedChildren}</a>;
|
|
}
|
|
const fallback = await renderClientFallback(props, resolvedChildren, ctx);
|
|
if (!isEmptyNode(fallback)) return fallback;
|
|
const name = componentDisplayName(element.type);
|
|
return (
|
|
<p>
|
|
{name
|
|
? `[interactive preview component ${name} omitted]`
|
|
: "[interactive preview omitted]"}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
if (typeof element.type === "function") {
|
|
const Component = (getLLMVariant(element.type) ??
|
|
element.type) as StaticFunctionComponent;
|
|
try {
|
|
const rendered = Component(
|
|
{ ...props, children: children as ReactNode },
|
|
ctx,
|
|
);
|
|
return resolveStaticReactNode(await rendered, ctx);
|
|
} catch (error) {
|
|
const resolvedChildren = await resolveStaticReactNode(
|
|
children as ReactNode,
|
|
ctx,
|
|
);
|
|
const fallback = await renderClientFallback(props, resolvedChildren, ctx);
|
|
if (!isEmptyNode(fallback)) return fallback;
|
|
|
|
if (!looksLikeStaticRenderFailure(error)) {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
// Tripwire for unrecognized React error shapes; update the
|
|
// heuristic when this fires.
|
|
console.warn(
|
|
"[get-llm-text] unexpected static render failure, rethrowing",
|
|
error,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
const resolvedChildren = await resolveStaticReactNode(
|
|
children as ReactNode,
|
|
ctx,
|
|
);
|
|
return renderClientFallback(props, resolvedChildren, ctx);
|
|
}
|
|
|
|
type LLMPage =
|
|
| InferPageType<typeof source>
|
|
| InferPageType<typeof examples>
|
|
| InferPageType<typeof tapDocs>;
|
|
|
|
export async function getLLMText(
|
|
page: LLMPage,
|
|
ctx: LLMRenderContext = DEFAULT_LLM_CONTEXT,
|
|
) {
|
|
const Body = page.data.body;
|
|
|
|
// TODO: Platform-scoped MDX currently renders with the server default
|
|
// platform ("react"). If llms output should include React Native or Ink
|
|
// variants, render once per platform or provide an explicit platform scope.
|
|
const staticBody = await resolveStaticReactNode(
|
|
<Body components={LLM_COMPONENTS} />,
|
|
ctx,
|
|
);
|
|
const { renderToStaticMarkup } = await import("react-dom/server");
|
|
const html = renderToStaticMarkup(staticBody);
|
|
// remark-stringify escapes the leading `[` of a callout admonition marker
|
|
// (`> [!info]` → `> \[!info]`); restore it so the GitHub syntax stays intact.
|
|
const markdown = String(await processor.process(html))
|
|
.replace(/^(\s*>\s*)\\\[!/gm, "$1[!")
|
|
.trim();
|
|
|
|
return `# ${page.data.title}
|
|
URL: ${page.url}
|
|
${page.data.description ? `\n${page.data.description}\n` : ""}
|
|
${AGENT_DOCS_DIRECTIVE_MARKDOWN}
|
|
|
|
${markdown}`;
|
|
}
|