docs: share API method rendering

This commit is contained in:
Taesu
2026-08-28 12:06:28 +09:00
parent 7ac7bd04c0
commit e656747759
7 changed files with 821 additions and 944 deletions
+110 -702
View File
@@ -1,5 +1,8 @@
import { Link } from "lucide-react";
import type { JSX, ReactNode } from "react";
import { Children, isValidElement } from "react";
import type { ApiMethodOptions, ApiMethodProperty } from "@/lib/api-method";
import { generateApiMethodExamples, parseApiMethod } from "@/lib/api-method";
import { cn } from "@/lib/utils";
import {
ApiMethodTabs,
@@ -11,181 +14,52 @@ import { Endpoint } from "./endpoint";
import { Button } from "./ui/button";
import { DynamicCodeBlock } from "./ui/dynamic-code-block";
type Property = {
isOptional: boolean;
description: string | null;
propName: string;
type: string;
exampleValue: string | null;
comments: string | null;
isServerOnly: boolean;
path: string[];
isNullable: boolean;
isClientOnly: boolean;
};
const apiMethodTabs = [
{
value: "client",
label: "Client",
icon: (
<>
<rect width="20" height="14" x="2" y="3" rx="2" />
<path d="M8 21h8m-4-4v4" />
</>
),
},
{
value: "server",
label: "Server",
icon: (
<>
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
<path d="M6 6h.01M6 18h.01" />
</>
),
},
] as const;
const placeholderProperty: Property = {
isOptional: false,
comments: null,
description: null,
exampleValue: null,
propName: "",
type: "",
isServerOnly: false,
path: [],
isNullable: false,
isClientOnly: false,
};
const indentationSpace = ` `;
export const APIMethod = ({
path,
isServerOnly,
isClientOnly,
isExternalOnly,
method,
export function APIMethod({
children,
noResult,
requireSession,
requireHeaders,
requireBearerToken,
headersComment,
note,
clientOnlyNote,
serverOnlyNote,
resultVariable = "data",
forceAsBody,
forceAsParam,
forceAsQuery,
}: {
/**
* Endpoint path
*/
path: string;
/**
* If enabled, we will add `headers` to the fetch options, indicating the given API method requires auth headers.
*
* @default false
*/
requireSession?: boolean;
/**
* If enabled, will add a bearer authorization header to the fetch options
*
* @default false
*/
requireBearerToken?: boolean;
/**
* If enabled, we will add `headers` to the fetch options without assuming the request is backed by a session.
*
* @default false
*/
requireHeaders?: boolean;
/**
* A custom comment to display above the generated `headers` line when `requireHeaders` is enabled.
*/
headersComment?: string;
/**
* The HTTP method to the endpoint
*
* @default "GET"
*/
method?: "POST" | "GET" | "DELETE" | "PUT";
/**
* Wether the endpoint is server only or not.
*
* @default false
*/
isServerOnly?: boolean;
/**
* Wether the code example is client-only, thus meaning it's an endpoint.
*
* @default false
*/
isClientOnly?: boolean;
/**
* Wether the code example is meant for external consumers
*/
isExternalOnly?: boolean;
/**
* The `ts` codeblock which describes the API method.
* I recommend checking other parts of the Better-Auth docs which is using this component to get an idea of how to
* write out the children.
*/
children: JSX.Element;
/**
* If enabled, will remove the `const data = ` part, since this implies there will be no return data from the API method.
*/
noResult?: boolean;
/**
* A small note to display above the client-auth example code-block.
*/
clientOnlyNote?: string;
/**
* A small note to display above the server-auth example code-block.
*/
serverOnlyNote?: string;
/**
* A small note to display above both the client & server auth example code-blocks.
*/
note?: string;
/**
* The result output variable name.
*
* @default "data"
*/
resultVariable?: string;
/**
* Force the server auth API to use `body`, rather than auto choosing
*/
forceAsBody?: boolean;
/**
* Force the server auth API to use `query`, rather than auto choosing
*/
forceAsQuery?: boolean;
/**
* Force the server auth api to use `path`, rather than auto choosing
*/
forceAsParam?: boolean;
}) => {
const { props, functionName, code_prefix, code_suffix } = parseCode(children);
const authClientMethodPath = pathToDotNotation(path);
const clientBody = createClientBody({
props,
method: method ?? "GET",
forceAsBody,
forceAsQuery,
forceAsParam,
});
const serverBody = createServerBody({
props,
method: method ?? "GET",
requireSession: requireSession ?? false,
requireHeaders: requireHeaders ?? false,
requireBearerToken: requireBearerToken ?? false,
headersComment,
forceAsQuery,
forceAsParam,
forceAsBody,
});
const serverCodeBlock = (
<DynamicCodeBlock
code={`${code_prefix}${
noResult ? "" : `const ${resultVariable} = `
}await auth.api.${functionName}(${serverBody});${code_suffix}`}
lang="ts"
allowCopy={!isClientOnly}
/>
);
...options
}: ApiMethodOptions & { children: ReactNode }) {
const {
path,
method,
isServerOnly,
isClientOnly,
isExternalOnly,
note,
clientOnlyNote,
serverOnlyNote,
} = options;
const definition = parseApiMethod(getTextContent(children));
const examples = generateApiMethodExamples(definition, options);
const properties = definition.properties;
const serverTabContent = (
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
{isClientOnly || isServerOnly ? null : (
<Endpoint method={method || "GET"} path={path} className="" />
<Endpoint method={method ?? "GET"} path={path} />
)}
{serverOnlyNote || note ? (
<Note>
@@ -198,15 +72,19 @@ export const APIMethod = ({
) : null}
</Note>
) : null}
<div className={cn("relative w-full")}>
{serverCodeBlock}
<div className="relative w-full">
<DynamicCodeBlock
code={examples.server}
lang="ts"
allowCopy={!isClientOnly}
/>
{isClientOnly ? (
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
<span>This is a client-only endpoint</span>
</div>
) : null}
</div>
{!isClientOnly ? <TypeTable props={props} isServer /> : null}
{!isClientOnly ? <TypeTable properties={properties} isServer /> : null}
</div>
);
@@ -230,31 +108,7 @@ export const APIMethod = ({
className="gap-0 w-full"
>
<ApiMethodTabsList className="relative flex justify-start w-full p-0 bg-background hover:[&>div>a>button]:opacity-100">
{(
[
{
value: "client",
label: "Client",
icon: (
<>
<rect width="20" height="14" x="2" y="3" rx="2" />
<path d="M8 21h8m-4-4v4" />
</>
),
},
{
value: "server",
label: "Server",
icon: (
<>
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
<path d="M6 6h.01M6 18h.01" />
</>
),
},
] as const
).map((tab) => (
{apiMethodTabs.map((tab) => (
<ApiMethodTabsTrigger
key={tab.value}
value={tab.value}
@@ -281,7 +135,7 @@ export const APIMethod = ({
<Button
variant="ghost"
className="opacity-100 transition-all duration-150 ease-in-out scale-90 md:opacity-0"
size={"icon"}
size="icon"
>
<Link className="size-4" />
</Button>
@@ -291,7 +145,7 @@ export const APIMethod = ({
<ApiMethodTabsContent value="client">
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
{isServerOnly ? null : (
<Endpoint method={method || "GET"} path={path} />
<Endpoint method={method ?? "GET"} path={path} />
)}
{clientOnlyNote || note ? (
<Note>
@@ -304,15 +158,9 @@ export const APIMethod = ({
) : null}
</Note>
) : null}
<div className={cn("relative w-full")}>
<div className="relative w-full">
<DynamicCodeBlock
code={`${code_prefix}${
noResult
? ""
: `const { data${
resultVariable === "data" ? "" : `: ${resultVariable}`
}, error } = `
}await authClient.${authClientMethodPath}(${clientBody});${code_suffix}`}
code={examples.client}
lang="ts"
allowCopy={!isServerOnly}
/>
@@ -323,7 +171,7 @@ export const APIMethod = ({
) : null}
</div>
{!isServerOnly ? (
<TypeTable props={props} isServer={false} />
<TypeTable properties={properties} isServer={false} />
) : null}
</div>
</ApiMethodTabsContent>
@@ -333,60 +181,32 @@ export const APIMethod = ({
</ApiMethodTabs>
</>
);
};
function pathToDotNotation(input: string): string {
return input
.split("/") // split into segments
.filter(Boolean) // remove empty strings (from leading '/')
.map((segment) =>
segment
.split("-") // split kebab-case
.map((word, i) =>
i === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1),
)
.join(""),
)
.join(".");
}
function getChildren(
x:
| ({ props: { children: string } } | string)
| ({ props: { children: string } } | string)[],
): string[] {
if (Array.isArray(x)) {
const res = [];
for (const item of x) {
res.push(getChildren(item));
}
return res.flat();
} else {
if (typeof x === "string") return [x];
return [x.props.children];
}
function getTextContent(node: ReactNode): string {
return Children.toArray(node)
.map((child) =>
isValidElement<{ children?: ReactNode }>(child)
? getTextContent(child.props.children)
: String(child),
)
.join("");
}
function TypeTable({
props,
properties,
isServer,
}: {
props: Property[];
properties: ApiMethodProperty[];
isServer: boolean;
}) {
if (!isServer && !props.filter((x) => !x.isServerOnly).length) return null;
if (isServer && !props.filter((x) => !x.isClientOnly).length) return null;
if (!props.length) return null;
const filteredProps = props.filter(
(prop) =>
!(prop.isServerOnly && isServer === false) &&
!(prop.isClientOnly && isServer === true),
const visibleProperties = properties.filter(
(property) =>
!(property.serverOnly && isServer === false) &&
!(property.clientOnly && isServer === true),
);
if (!filteredProps.length) return null;
if (!visibleProperties.length) return null;
return (
<div className="mt-0">
@@ -412,27 +232,27 @@ function TypeTable({
Parameters
</span>
</div>
<PropertyList props={filteredProps} />
<PropertyList properties={visibleProperties} />
</div>
);
}
function PropertyItem({ prop }: { prop: Property }) {
function PropertyItem({ property }: { property: ApiMethodProperty }) {
return (
<div className="flex items-center gap-2 flex-wrap">
<code className="text-xs font-semibold text-foreground/90">
{prop.propName}
{property.name}
</code>
<span className="text-xs font-mono text-foreground/60 font-medium">
{prop.type}
{prop.isNullable ? " | null" : ""}
{property.type}
{property.nullable ? " | null" : ""}
</span>
{!prop.isOptional && (
{!property.optional && (
<span className="text-[10px] font-mono font-medium text-amber-600 dark:text-amber-500/80">
required
</span>
)}
{prop.isServerOnly && (
{property.serverOnly && (
<span className="text-[10px] font-mono font-medium text-blue-600 dark:text-blue-400/80">
server
</span>
@@ -442,33 +262,38 @@ function PropertyItem({ prop }: { prop: Property }) {
}
function PropertyList({
props,
properties,
nested = false,
}: {
props: Property[];
properties: ApiMethodProperty[];
nested?: boolean;
}) {
const groups: { prop: Property; children: Property[] }[] = [];
let i = 0;
const groups: {
property: ApiMethodProperty;
children: ApiMethodProperty[];
}[] = [];
let propertyIndex = 0;
while (i < props.length) {
const prop = props[i];
if (prop.type === "Object") {
const parentSegments = [...prop.path, prop.propName];
const children: Property[] = [];
i++;
while (propertyIndex < properties.length) {
const property = properties[propertyIndex];
if (property.type === "Object") {
const parentSegments = [...property.path, property.name];
const children: ApiMethodProperty[] = [];
propertyIndex++;
while (
i < props.length &&
props[i].path.length >= parentSegments.length &&
parentSegments.every((seg, idx) => props[i].path[idx] === seg)
propertyIndex < properties.length &&
properties[propertyIndex].path.length >= parentSegments.length &&
parentSegments.every(
(segment, index) => properties[propertyIndex].path[index] === segment,
)
) {
children.push(props[i]);
i++;
children.push(properties[propertyIndex]);
propertyIndex++;
}
groups.push({ prop, children });
groups.push({ property, children });
} else {
groups.push({ prop, children: [] });
i++;
groups.push({ property, children: [] });
propertyIndex++;
}
}
@@ -476,21 +301,21 @@ function PropertyList({
<div className="divide-y divide-border">
{groups.map((group) => (
<div
key={`${group.prop.path.join(".")}.${group.prop.propName}`}
key={`${group.property.path.join(".")}.${group.property.name}`}
className={cn(
nested ? "px-3 py-3" : "px-3.5 py-3",
group.children.length > 0 && "pb-3",
)}
>
<PropertyItem prop={group.prop} />
{group.prop.description && (
<PropertyItem property={group.property} />
{group.property.description && (
<p className="mt-1 mb-0 text-sm leading-relaxed max-w-xl">
{tsxifyBackticks(group.prop.description)}
{tsxifyBackticks(group.property.description)}
</p>
)}
{group.children.length > 0 && (
<div className="mt-3 border rounded-md overflow-hidden">
<PropertyList props={group.children} nested />
<PropertyList properties={group.children} nested />
</div>
)}
</div>
@@ -500,445 +325,28 @@ function PropertyList({
}
function tsxifyBackticks(input: string): JSX.Element {
const parts = input.split(/(`[^`]+`)/g); // Split by backtick sections
const parts = input.split(/(`[^`]+`)/g);
return (
<>
{parts.map((part, index) => {
if (part.startsWith("`") && part.endsWith("`")) {
const content = part.slice(1, -1); // remove backticks
const content = part.slice(1, -1);
return <code key={index}>{content}</code>;
} else {
return <span key={index}>{part}</span>;
}
return <span key={index}>{part}</span>;
})}
</>
);
}
function parseCode(children: JSX.Element) {
// These two variables are essentially taking the `children` JSX shiki code, and converting them to
// an array string purely of it's code content.
const arrayOfJSXCode = children?.props.children.props.children.props.children
.map((x: any) =>
x === "\n" ? { props: { children: { props: { children: "\n" } } } } : x,
)
.map((x: any) => x.props.children)
.filter((x: any) => x != null);
const arrayOfCode: string[] = arrayOfJSXCode
.flatMap(
(
x: { props: { children: string } } | { props: { children: string } }[],
) => {
return getChildren(x);
},
)
.join("")
.split("\n");
const props: Property[] = [];
let functionName: string = "";
let currentJSDocDescription: string = "";
let withinApiMethodType = false;
let hasAlreadyDefinedApiMethodType = false;
let isServerOnly_ = false;
let isClientOnly_ = false;
const nestPath: string[] = []; // str arr segmented-path, eg: ["data", "metadata", "something"]
const serverOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"]
const clientOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"]
let isNullable = false;
let code_prefix = "";
let code_suffix = "";
for (let line of arrayOfCode) {
const originalLine = line;
line = line.trim();
if (line === "}" && withinApiMethodType && !nestPath.length) {
withinApiMethodType = false;
hasAlreadyDefinedApiMethodType = true;
continue;
} else {
if (line === "}" && withinApiMethodType && nestPath.length) {
nestPath.pop();
continue;
}
}
if (
line.toLowerCase().startsWith("type") &&
!hasAlreadyDefinedApiMethodType &&
!withinApiMethodType
) {
withinApiMethodType = true;
// Will grab the name of the API method function name from:
// type createOrganization = {
// ^^^^^^^^^^^^^^^^^^
functionName = line.replace("type ", "").split("=")[0].trim();
continue;
}
if (!withinApiMethodType) {
if (!hasAlreadyDefinedApiMethodType) {
code_prefix += originalLine + "\n";
} else {
code_suffix += "\n" + originalLine + "";
}
continue;
}
if (
line.startsWith("/*") ||
line.startsWith("*") ||
line.startsWith("*/")
) {
if (line.startsWith("/*")) {
continue;
} else if (line.startsWith("*/")) {
continue;
} else {
if (line === "*" || line === "* ") continue;
line = line.replace("* ", "");
if (line.trim() === "@serverOnly") {
isServerOnly_ = true;
continue;
} else if (line.trim() === "@nullable") {
isNullable = true;
continue;
} else if (line.trim() === "@clientOnly") {
isClientOnly_ = true;
continue;
}
currentJSDocDescription += line + " ";
}
} else {
// New property field
// Example:
// name: string = "My Organization",
let propName = line.split(":")[0].trim();
const isOptional = propName.endsWith("?") ? true : false;
if (isOptional) propName = propName.slice(0, -1); // Remove `?` in propname.
let propType = line
.replace(propName, "")
.replace("?", "")
.replace(":", "")
.split("=")[0]
.trim()!;
let isTheStartOfNest = false;
if (propType === "{") {
// This means that it's a nested object.
propType = `Object`;
isTheStartOfNest = true;
nestPath.push(propName);
if (isServerOnly_) {
serverOnlyPaths.push(nestPath.join("."));
}
if (isClientOnly_) {
clientOnlyPaths.push(nestPath.join("."));
}
}
if (clientOnlyPaths.includes(nestPath.join("."))) {
isClientOnly_ = true;
}
if (serverOnlyPaths.includes(nestPath.join("."))) {
isServerOnly_ = true;
}
let exampleValue = !line.includes("=")
? null
: line
.replace(propName, "")
.replace("?", "")
.replace(":", "")
.replace(propType, "")
.replace("=", "")
.trim();
if (exampleValue?.endsWith(",")) exampleValue = exampleValue.slice(0, -1);
// const comments =
// line
// .replace(propName, "")
// .replace("?", "")
// .replace(":", "")
// .replace(propType, "")
// .replace("=", "")
// .replace(exampleValue || "IMPOSSIBLE_TO_REPLACE_!!!!", "")
// .split("//")[1] ?? null;
const comments = null;
const description =
currentJSDocDescription.length > 0 ? currentJSDocDescription : null;
if (description) {
currentJSDocDescription = "";
}
const property: Property = {
...placeholderProperty,
description,
comments,
exampleValue,
isOptional,
propName,
type: propType,
isServerOnly: isServerOnly_,
isClientOnly: isClientOnly_,
path: isTheStartOfNest
? nestPath.slice(0, nestPath.length - 1)
: nestPath.slice(),
isNullable: isNullable,
};
isServerOnly_ = false;
isClientOnly_ = false;
isNullable = false;
// console.log(property);
props.push(property);
}
}
return {
functionName,
props,
code_prefix,
code_suffix,
};
}
/**
* Builds a property line with proper formatting and comments
*/
function buildPropertyLine(
prop: Property,
indentLevel: number,
additionalComments: string[] = [],
): string {
const comments: string[] = [...additionalComments];
if (!prop.isOptional) comments.push("required");
if (prop.comments) comments.push(prop.comments);
const addComment = comments.length > 0;
const indent = indentationSpace.repeat(indentLevel);
const propValue = prop.exampleValue ? `: ${prop.exampleValue}` : "";
const commentText = addComment ? ` // ${comments.join(", ")}` : "";
if (prop.type === "Object") {
// For object types, put comment after the opening brace
return `${indent}${prop.propName}${propValue}: {${commentText}\n`;
} else {
// For non-object types, put comment after the comma
return `${indent}${prop.propName}${propValue},${commentText}\n`;
}
}
/**
* Determines if the client request should use query parameters
*
* - GET requests use query params by default, unless `forceAsBody` is true
* - Any request can be forced to use query params with `forceAsQuery`
*/
function shouldClientUseQueryParams(
method: string | undefined,
forceAsBody: boolean | undefined,
forceAsQuery: boolean | undefined,
forceAsParam: boolean | undefined,
): boolean {
if (forceAsQuery) return true;
if (forceAsBody) return false;
if (forceAsParam) return false;
return method === "GET";
}
function createClientBody({
props,
method,
forceAsBody,
forceAsQuery,
forceAsParam,
}: {
props: Property[];
method?: string;
forceAsBody?: boolean;
forceAsQuery?: boolean;
forceAsParam?: boolean;
}) {
const isQueryParam = shouldClientUseQueryParams(
method,
forceAsBody,
forceAsQuery,
forceAsParam,
);
const baseIndentLevel = isQueryParam ? 2 : 1;
let params = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isServerOnly) continue;
if (params === "") params += "{\n";
params += buildPropertyLine(prop, prop.path.length + baseIndentLevel);
if ((props[i + 1]?.path?.length || 0) < prop.path.length) {
const diff = prop.path.length - (props[i + 1]?.path?.length || 0);
for (const index of Array(diff)
.fill(0)
.map((_, i) => i)
.reverse()) {
params += `${indentationSpace.repeat(index + baseIndentLevel)}},\n`;
}
}
}
if (params !== "") {
if (isQueryParam) {
// Wrap in query object for GET requests and when forceAsQuery is true
params = `{\n query: ${params} },\n}`;
} else {
params += "}";
}
}
return params;
}
/**
* Determines if the server request should use query parameters
*
* - GET requests use query params by default, unless `forceAsBody` is true
* - Other methods (POST, PUT, DELETE) use body by default, unless `forceAsQuery` is true
*/
function shouldServerUseQueryParams(
method: string,
forceAsBody: boolean | undefined,
forceAsQuery: boolean | undefined,
forceAsParam: boolean | undefined,
): boolean {
if (forceAsQuery) return true;
if (forceAsBody) return false;
if (forceAsParam) return false;
return method === "GET";
}
function createServerBody({
props,
requireSession,
requireHeaders,
requireBearerToken,
headersComment,
method,
forceAsBody,
forceAsParam,
forceAsQuery,
}: {
props: Property[];
requireSession: boolean;
requireHeaders: boolean;
requireBearerToken: boolean;
headersComment: string | undefined;
method: string;
forceAsQuery: boolean | undefined;
forceAsParam: boolean | undefined;
forceAsBody: boolean | undefined;
}) {
const isQueryParam = shouldServerUseQueryParams(
method,
forceAsBody,
forceAsQuery,
forceAsParam,
);
const clientOnlyProps = props.filter((x) => !x.isClientOnly);
// Build properties content
let propertiesContent = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isClientOnly) continue;
if (propertiesContent === "") propertiesContent += "{\n";
// Check if this is a server-only nested property
const isNestedServerOnlyProp =
prop.isServerOnly &&
!(
prop.path.length &&
props.find(
(x) =>
x.path.join(".") ===
prop.path.slice(0, prop.path.length - 2).join(".") &&
x.propName === prop.path[prop.path.length - 1],
)
);
const additionalComments: string[] = [];
if (isNestedServerOnlyProp) additionalComments.push("server-only");
propertiesContent += buildPropertyLine(
prop,
prop.path.length + 2,
additionalComments,
);
if ((props[i + 1]?.path?.length || 0) < prop.path.length) {
const diff = prop.path.length - (props[i + 1]?.path?.length || 0);
for (const index of Array(diff)
.fill(0)
.map((_, i) => i)
.reverse()) {
propertiesContent += `${indentationSpace.repeat(index + 2)}},\n`;
}
}
}
if (propertiesContent !== "") propertiesContent += " },";
// Build fetch options
let fetchOptions = "";
if (requireSession) {
fetchOptions +=
"\n // This endpoint requires session cookies.\n headers: await headers(),";
} else if (requireHeaders) {
fetchOptions += `\n // ${
headersComment ||
"Pass the current request headers so Better Auth can read and set cookies."
}\n headers: await headers(),`;
}
if (requireBearerToken) {
fetchOptions +=
"\n // This endpoint requires a bearer authentication token.\n headers: { authorization: 'Bearer <token>' },";
}
// Assemble final result
let result = "";
if (clientOnlyProps.length > 0) {
result += "{\n";
const paramType = isQueryParam ? "query" : forceAsParam ? "params" : "body";
result += ` ${paramType}: ${propertiesContent}${fetchOptions}\n}`;
} else if (fetchOptions.length) {
result += `{${fetchOptions}\n}`;
}
return result;
}
function Note({ children }: { children: ReactNode }) {
return (
<div className="flex relative flex-col gap-2 p-3 mb-2 w-full wrap-break-word rounded-md border-b text-md text-wrap bg-fd-muted/80">
<span className="-mb-1 w-full text-xs select-none text-foreground/80 font-medium">
Notes
</span>
<p className="mt-0 mb-0 text-sm text-fd-muted-foreground">
{children as any}
</p>
<p className="mt-0 mb-0 text-sm text-fd-muted-foreground">{children}</p>
</div>
);
}
+6 -6
View File
@@ -1,16 +1,16 @@
"use client";
import type { ApiMethodHttpMethod } from "@/lib/api-method";
import { cn } from "@/lib/utils";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
const methodColors: Record<HttpMethod, string> = {
const methodColors = {
GET: "text-green-600 dark:text-green-500",
POST: "text-yellow-600 dark:text-yellow-600",
PUT: "text-blue-600 dark:text-blue-400",
PATCH: "text-cyan-600 dark:text-cyan-400",
DELETE: "text-red-600 dark:text-red-400",
};
} satisfies Record<ApiMethodHttpMethod, string>;
function Method({ method }: { method: HttpMethod }) {
function Method({ method }: { method: ApiMethodHttpMethod }) {
return (
<span
className={cn(
@@ -29,7 +29,7 @@ export function Endpoint({
className,
}: {
path: string;
method: HttpMethod;
method: ApiMethodHttpMethod;
className?: string;
}) {
return (
+174
View File
@@ -0,0 +1,174 @@
import { describe, expect, it } from "vitest";
import { generateApiMethodExamples, parseApiMethod } from "./api-method";
const definition = parseApiMethod(`type verifyTotp = {
/**
* The current one-time password.
*/
code: string = "123456"
/**
* @serverOnly - Only available on the server.
*/
metadata?: {
ipAddress?: string = "127.0.0.1"
}
}`);
describe("API method code generation", () => {
it("parses the shared API method definition", () => {
expect(definition.functionName).toBe("verifyTotp");
expect(definition.typeDefinition).toContain("type verifyTotp");
expect(definition.properties).toEqual([
expect.objectContaining({
name: "code",
exampleValue: '"123456"',
description: "The current one-time password.",
}),
expect.objectContaining({
name: "metadata",
type: "Object",
serverOnly: true,
description: "Only available on the server.",
}),
expect.objectContaining({
name: "ipAddress",
path: ["metadata"],
serverOnly: true,
}),
]);
});
it("generates the same examples for UI and Markdown", () => {
const examples = generateApiMethodExamples(definition, {
path: "/two-factor/verify-totp",
method: "POST",
requireHeaders: true,
headersComment: "Forward the current request headers.",
});
expect(examples.client).toContain(
"await authClient.twoFactor.verifyTotp({",
);
expect(examples.client).not.toContain("metadata");
expect(examples.server).toContain("const data = await auth.api.verifyTotp");
expect(examples.server).toContain("Forward the current request headers.");
expect(examples.server).toContain("headers: await headers()");
expect(examples.server).toContain("metadata: {");
});
it("uses query parameters and result aliases", () => {
const examples = generateApiMethodExamples(definition, {
path: "/admin/list-users",
method: "GET",
resultVariable: "users",
});
expect(examples.client).toContain("const { data: users, error } =");
expect(examples.client).toContain("query:");
expect(examples.server).toContain("const users =");
expect(examples.server).toContain("query:");
});
it("inherits visibility directives through nested properties", () => {
const nestedDefinition = parseApiMethod(`type updateMetadata = {
/**
* @serverOnly
*/
metadata?: {
audit?: {
ipAddress?: string = "127.0.0.1"
}
}
}`);
expect(
nestedDefinition.properties
.filter((property) =>
["metadata", "audit", "ipAddress"].includes(property.name),
)
.map((property) => property.serverOnly),
).toEqual([true, true, true]);
});
it("closes nested properties after filtering hidden children", () => {
const clientDefinition = parseApiMethod(`type updateProfile = {
options?: {
visible?: string = "visible"
/**
* @serverOnly
*/
serverSecret?: string = "server"
}
name: string = "name"
}`);
const serverDefinition = parseApiMethod(`type updateProfile = {
options?: {
visible?: string = "visible"
/**
* @clientOnly
*/
clientSecret?: string = "client"
}
name: string = "name"
}`);
const clientExamples = generateApiMethodExamples(clientDefinition, {
path: "/update-profile",
method: "POST",
});
const serverExamples = generateApiMethodExamples(serverDefinition, {
path: "/update-profile",
method: "POST",
});
expect(clientExamples.client).not.toContain("serverSecret");
expect(clientExamples.client).toContain(
'visible: "visible",\n },\n name:',
);
expect(serverExamples.server).not.toContain("clientSecret");
expect(serverExamples.server).toContain(
'visible: "visible",\n },\n name:',
);
});
it("merges bearer tokens with forwarded request headers", () => {
const examples = generateApiMethodExamples(definition, {
path: "/two-factor/verify-totp",
method: "POST",
requireHeaders: true,
requireBearerToken: true,
});
expect(examples.server.match(/headers:/g)).toHaveLength(1);
expect(examples.server).toContain("...(await headers())");
expect(examples.server).toContain("authorization: 'Bearer <token>'");
});
it("closes objects when all nested properties are hidden", () => {
const clientDefinition = parseApiMethod(`type updateProfile = {
options?: {
/**
* @serverOnly
*/
secret?: string = "secret"
}
name: string = "name"
}`);
const serverDefinition = parseApiMethod(`type updateProfile = {
options?: {
/**
* @clientOnly
*/
secret?: string = "secret"
}
name: string = "name"
}`);
const options = { path: "/update-profile", method: "POST" } as const;
expect(
generateApiMethodExamples(clientDefinition, options).client,
).toContain("options: {\n },\n name:");
expect(
generateApiMethodExamples(serverDefinition, options).server,
).toContain("options: {\n },\n name:");
});
});
+389
View File
@@ -0,0 +1,389 @@
export const apiMethodHttpMethods = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
] as const;
export type ApiMethodHttpMethod = (typeof apiMethodHttpMethods)[number];
export interface ApiMethodOptions {
path: string;
method?: ApiMethodHttpMethod;
isServerOnly?: boolean;
isClientOnly?: boolean;
isExternalOnly?: boolean;
noResult?: boolean;
requireSession?: boolean;
requireHeaders?: boolean;
requireBearerToken?: boolean;
headersComment?: string;
note?: string;
clientOnlyNote?: string;
serverOnlyNote?: string;
resultVariable?: string;
forceAsBody?: boolean;
forceAsParam?: boolean;
forceAsQuery?: boolean;
}
export interface ApiMethodProperty {
optional: boolean;
description: string | null;
name: string;
type: string;
exampleValue: string | null;
serverOnly: boolean;
path: string[];
nullable: boolean;
clientOnly: boolean;
}
export interface ParsedApiMethod {
functionName: string;
properties: ApiMethodProperty[];
codePrefix: string;
codeSuffix: string;
typeDefinition: string;
}
const indentation = " ";
const typeDeclarationPattern = /^type\s+([A-Za-z_$][\w$]*)\s*=\s*\{$/;
const propertyPattern = /^([A-Za-z_$][\w$]*)(\?)?:\s*(.+)$/;
function parsePropertyLine(line: string) {
const match = propertyPattern.exec(line);
if (!match) return null;
const [, name, optional, declaration] = match;
const exampleSeparator = declaration.indexOf(" = ");
const type = (
exampleSeparator === -1
? declaration
: declaration.slice(0, exampleSeparator)
)
.trim()
.replace(/[;,]$/, "");
const exampleValue =
exampleSeparator === -1
? null
: declaration
.slice(exampleSeparator + 3)
.trim()
.replace(/[;,]$/, "");
return {
name,
optional: optional === "?",
type,
exampleValue,
};
}
function readDirective(value: string, directive: string): string | null {
if (value !== directive && !value.startsWith(`${directive} `)) return null;
return value
.slice(directive.length)
.replace(/^\s*-\s*/, "")
.trim();
}
function isPathRestricted(restrictedPaths: Set<string>, path: string[]) {
for (let length = 1; length <= path.length; length++) {
if (restrictedPaths.has(path.slice(0, length).join("."))) return true;
}
return false;
}
export function parseApiMethod(source: string): ParsedApiMethod {
const lines = source.replaceAll("\r\n", "\n").trim().split("\n");
const properties: ApiMethodProperty[] = [];
const nestedPath: string[] = [];
const serverOnlyPaths = new Set<string>();
const clientOnlyPaths = new Set<string>();
let functionName = "";
let typeStart = -1;
let typeEnd = -1;
let description = "";
let serverOnly = false;
let clientOnly = false;
let nullable = false;
for (const [index, originalLine] of lines.entries()) {
const line = originalLine.trim();
if (typeStart === -1) {
const declaration = typeDeclarationPattern.exec(line);
if (!declaration) continue;
functionName = declaration[1];
typeStart = index;
continue;
}
if (/^}[,;]?$/.test(line)) {
if (nestedPath.length > 0) {
nestedPath.pop();
continue;
}
typeEnd = index;
break;
}
if (line.startsWith("/*") || line.startsWith("*/")) continue;
if (line.startsWith("*")) {
const value = line.slice(1).trim();
if (!value) continue;
const serverOnlyDescription = readDirective(value, "@serverOnly");
if (serverOnlyDescription !== null) {
serverOnly = true;
if (serverOnlyDescription) {
description += `${serverOnlyDescription} `;
}
continue;
}
const clientOnlyDescription = readDirective(value, "@clientOnly");
if (clientOnlyDescription !== null) {
clientOnly = true;
if (clientOnlyDescription) {
description += `${clientOnlyDescription} `;
}
continue;
}
const nullableDescription = readDirective(value, "@nullable");
if (nullableDescription !== null) {
nullable = true;
if (nullableDescription) description += `${nullableDescription} `;
continue;
}
description += `${value} `;
continue;
}
if (!line) continue;
const parsed = parsePropertyLine(line);
if (!parsed) continue;
const propertyPath = nestedPath.slice();
const nested = parsed.type === "{";
if (nested) {
nestedPath.push(parsed.name);
if (serverOnly) serverOnlyPaths.add(nestedPath.join("."));
if (clientOnly) clientOnlyPaths.add(nestedPath.join("."));
}
properties.push({
name: parsed.name,
optional: parsed.optional,
type: nested ? "Object" : parsed.type,
exampleValue: parsed.exampleValue,
description: description.trim() || null,
serverOnly: serverOnly || isPathRestricted(serverOnlyPaths, nestedPath),
clientOnly: clientOnly || isPathRestricted(clientOnlyPaths, nestedPath),
path: propertyPath,
nullable,
});
description = "";
serverOnly = false;
clientOnly = false;
nullable = false;
}
if (typeStart === -1 || typeEnd === -1) {
return {
functionName: "",
properties: [],
codePrefix: "",
codeSuffix: "",
typeDefinition: source.trim(),
};
}
const prefix = lines.slice(0, typeStart).join("\n").trim();
const suffix = lines
.slice(typeEnd + 1)
.join("\n")
.trim();
return {
functionName,
properties,
codePrefix: prefix ? `${prefix}\n` : "",
codeSuffix: suffix ? `\n${suffix}` : "",
typeDefinition: lines.slice(typeStart, typeEnd + 1).join("\n"),
};
}
function pathToMethodName(path: string): string {
return path
.split("/")
.filter((segment) => segment.length > 0)
.map((segment) =>
segment
.split("-")
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1),
)
.join(""),
)
.join(".");
}
function buildPropertyLine(
property: ApiMethodProperty,
indentLevel: number,
additionalComments: string[] = [],
): string {
const comments = [...additionalComments];
if (!property.optional) comments.push("required");
const indent = indentation.repeat(indentLevel);
const value = property.exampleValue ? `: ${property.exampleValue}` : "";
const comment = comments.length > 0 ? ` // ${comments.join(", ")}` : "";
return property.type === "Object"
? `${indent}${property.name}${value}: {${comment}\n`
: `${indent}${property.name}${value},${comment}\n`;
}
function usesQuery(
method: ApiMethodHttpMethod,
options: Pick<
ApiMethodOptions,
"forceAsBody" | "forceAsParam" | "forceAsQuery"
>,
): boolean {
if (options.forceAsQuery) return true;
if (options.forceAsBody || options.forceAsParam) return false;
return method === "GET";
}
function closeNestedProperties(
properties: ApiMethodProperty[],
index: number,
baseIndentLevel: number,
): string {
const property = properties[index];
const currentDepth =
property.path.length + (property.type === "Object" ? 1 : 0);
const nextDepth = properties[index + 1]?.path.length ?? 0;
if (nextDepth >= currentDepth) return "";
let result = "";
for (let depth = currentDepth - 1; depth >= nextDepth; depth--) {
result += `${indentation.repeat(depth + baseIndentLevel)}},\n`;
}
return result;
}
function createClientBody(
properties: ApiMethodProperty[],
method: ApiMethodHttpMethod,
options: ApiMethodOptions,
): string {
const query = usesQuery(method, options);
const baseIndentLevel = query ? 2 : 1;
const clientProperties = properties.filter(
(property) => !property.serverOnly,
);
let params = "";
for (const [index, property] of clientProperties.entries()) {
if (!params) params = "{\n";
params += buildPropertyLine(
property,
property.path.length + baseIndentLevel,
);
params += closeNestedProperties(clientProperties, index, baseIndentLevel);
}
if (!params) return "";
if (query) return `{\n query: ${params} },\n}`;
return `${params}}`;
}
function createServerBody(
properties: ApiMethodProperty[],
method: ApiMethodHttpMethod,
options: ApiMethodOptions,
): string {
const query = usesQuery(method, options);
const serverProperties = properties.filter(
(property) => !property.clientOnly,
);
let propertiesContent = "";
for (const [index, property] of serverProperties.entries()) {
if (!propertiesContent) propertiesContent = "{\n";
const parentPath = property.path.slice(0, -1).join(".");
const inheritedServerOnly =
property.serverOnly &&
!properties.some(
(candidate) =>
candidate.path.join(".") === parentPath &&
candidate.name === property.path.at(-1),
);
propertiesContent += buildPropertyLine(
property,
property.path.length + 2,
inheritedServerOnly ? ["server-only"] : [],
);
propertiesContent += closeNestedProperties(serverProperties, index, 2);
}
if (propertiesContent) propertiesContent += " },";
let headers = "";
const requestHeadersRequired =
options.requireSession || options.requireHeaders;
if (options.requireSession) {
headers += "\n // This endpoint requires session cookies.";
} else if (options.requireHeaders) {
headers += `\n // ${
options.headersComment ??
"Pass the current request headers so Better Auth can read and set cookies."
}`;
}
if (options.requireBearerToken) {
headers += "\n // This endpoint requires a bearer authentication token.";
}
if (requestHeadersRequired && options.requireBearerToken) {
headers +=
"\n headers: {\n ...(await headers()),\n authorization: 'Bearer <token>',\n },";
} else if (requestHeadersRequired) {
headers += "\n headers: await headers(),";
} else if (options.requireBearerToken) {
headers += "\n headers: { authorization: 'Bearer <token>' },";
}
if (serverProperties.length > 0) {
const parameter = query
? "query"
: options.forceAsParam
? "params"
: "body";
return `{\n ${parameter}: ${propertiesContent}${headers}\n}`;
}
return headers ? `{${headers}\n}` : "";
}
export function generateApiMethodExamples(
definition: ParsedApiMethod,
options: ApiMethodOptions,
) {
const method = options.method ?? "GET";
const resultVariable = options.resultVariable ?? "data";
const clientResult = options.noResult
? ""
: `const { data${resultVariable === "data" ? "" : `: ${resultVariable}`}, error } = `;
const serverResult = options.noResult ? "" : `const ${resultVariable} = `;
return {
client: `${definition.codePrefix}${clientResult}await authClient.${pathToMethodName(options.path)}(${createClientBody(definition.properties, method, options)});${definition.codeSuffix}`,
server: `${definition.codePrefix}${serverResult}await auth.api.${definition.functionName}(${createServerBody(definition.properties, method, options)});${definition.codeSuffix}`,
};
}
+63
View File
@@ -6,6 +6,7 @@ import {
getLLMsIndexTitle,
getMarkdownPageUrl,
getRootLLMsIndex,
renderApiMethodMarkdown,
} from "./llm-text";
describe("LLM routes", () => {
@@ -48,6 +49,68 @@ describe("LLM routes", () => {
expect(getMarkdownPageUrl("/llms.txt")).toBe("/llms.txt");
});
it("renders API method placeholders with all endpoint metadata", () => {
const markdown = renderApiMethodMarkdown({
name: "APIMethod",
attributes: {
path: "/admin/list-users",
method: "GET",
requireHeaders: null,
headersComment: "Forward the request headers.",
resultVariable: {
value: '"users"',
},
},
children: `\`\`\`ts
type listUsers = {
limit?: number = 100
}
\`\`\``,
});
expect(markdown).toContain("**Endpoint:** `GET /admin/list-users`");
expect(markdown).toContain("const { data: users, error } =");
expect(markdown).toContain("const users = await auth.api.listUsers");
expect(markdown).toContain("Forward the request headers.");
expect(markdown).not.toContain("<APIMethod");
});
it("omits unavailable API method examples", () => {
const markdown = renderApiMethodMarkdown({
name: "APIMethod",
attributes: {
path: "/api-key/verify",
method: "POST",
isServerOnly: null,
},
children: `\`\`\`ts
type verifyApiKey = {
key: string = "secret"
}
\`\`\``,
});
expect(markdown).not.toContain("### Client Side");
expect(markdown).toContain("### Server Side");
});
it("renders PATCH API methods", () => {
const markdown = renderApiMethodMarkdown({
name: "APIMethod",
attributes: {
path: "/scim/v2/Users/:id",
method: "PATCH",
},
children: `\`\`\`ts
type patchUser = {
id: string = "user-id"
}
\`\`\``,
});
expect(markdown).toContain("**Endpoint:** `PATCH /scim/v2/Users/:id`");
});
it("returns a useful Markdown not-found response", () => {
const content = getLLMNotFound("/docs/missing.md");
+72 -234
View File
@@ -1,264 +1,102 @@
import type { PlaceholderData } from "fumadocs-core/mdx-plugins/remark-llms.runtime";
import { renderPlaceholder } from "fumadocs-core/mdx-plugins/remark-llms.runtime";
import type { Item, Node } from "fumadocs-core/page-tree";
import type { InferPageType } from "fumadocs-core/source";
import { llms } from "fumadocs-core/source";
import * as z from "zod";
import {
apiMethodHttpMethods,
generateApiMethodExamples,
parseApiMethod,
} from "./api-method";
import type { DocsVersion } from "./docs-versions";
import { scopeDocsContent } from "./scope-docs-content";
import type { source } from "./source";
type PropertyDefinition = {
name: string;
type: string;
required: boolean;
description: string;
exampleValue: string;
isServerOnly: boolean;
isClientOnly: boolean;
};
const docsPathPattern = /^\/docs(?:\/|$)/;
const productionUrl = new URL("https://better-auth.com");
const llmsDescription =
"The most comprehensive authentication framework for TypeScript";
const expressionStringSchema = z
.object({ value: z.string() })
.transform(({ value }) => z.string().parse(JSON.parse(value)));
const stringAttributeSchema = z.union([z.string(), expressionStringSchema]);
const markerAttributeSchema = z
.null()
.optional()
.transform((value) => value === null);
const apiMethodAttributesSchema = z.object({
path: stringAttributeSchema.pipe(z.string().startsWith("/")),
method: stringAttributeSchema
.pipe(z.enum(apiMethodHttpMethods))
.optional()
.default("GET"),
isServerOnly: markerAttributeSchema,
isClientOnly: markerAttributeSchema,
isExternalOnly: markerAttributeSchema,
noResult: markerAttributeSchema,
requireSession: markerAttributeSchema,
requireHeaders: markerAttributeSchema,
requireBearerToken: markerAttributeSchema,
headersComment: stringAttributeSchema.optional(),
note: stringAttributeSchema.optional(),
clientOnlyNote: stringAttributeSchema.optional(),
serverOnlyNote: stringAttributeSchema.optional(),
resultVariable: stringAttributeSchema.optional(),
forceAsBody: markerAttributeSchema,
forceAsParam: markerAttributeSchema,
forceAsQuery: markerAttributeSchema,
});
function extractAPIMethods(rawContent: string): string {
const apiMethodRegex = /<APIMethod\s+([^>]+)>([\s\S]*?)<\/APIMethod>/g;
function unwrapCodeBlock(markdown: string): string {
const lines = markdown.trim().split("\n");
if (lines[0]?.startsWith("```") && lines.at(-1)?.trim() === "```") {
return lines.slice(1, -1).join("\n");
}
return markdown.trim();
}
return rawContent.replace(apiMethodRegex, (match, attributes, content) => {
const pathMatch = attributes.match(/path="([^"]+)"/);
const methodMatch = attributes.match(/method="([^"]+)"/);
const requireSessionMatch = attributes.match(/requireSession/);
const noResultMatch = attributes.match(/noResult/);
const resultVariableMatch = attributes.match(/resultVariable="([^"]+)"/);
const forceAsBodyMatch = attributes.match(/forceAsBody/);
const forceAsQueryMatch = attributes.match(/forceAsQuery/);
function formatNote(note: string | undefined): string {
return note ? `> **Note:** ${note}\n\n` : "";
}
const path = pathMatch ? pathMatch[1] : "";
const method = methodMatch ? methodMatch[1] : "GET";
const requireSession = !!requireSessionMatch;
const noResult = !!noResultMatch;
const resultVariable = resultVariableMatch
? resultVariableMatch[1]
: "data";
const forceAsBody = !!forceAsBodyMatch;
const forceAsQuery = !!forceAsQueryMatch;
export function renderApiMethodMarkdown({
attributes,
children,
}: PlaceholderData): string {
const options = apiMethodAttributesSchema.parse(attributes);
const definition = parseApiMethod(unwrapCodeBlock(children));
if (!definition.functionName) return children;
const typeMatch = content.match(/type\s+(\w+)\s*=\s*\{([\s\S]*?)\}/);
if (!typeMatch) {
return match;
}
const examples = generateApiMethodExamples(definition, options);
const sections = [`**Endpoint:** \`${options.method} ${options.path}\``];
if (options.note) sections.push(formatNote(options.note).trimEnd());
const functionName = typeMatch[1];
const typeBody = typeMatch[2];
const properties = parseTypeBody(typeBody);
const clientCode = generateClientCode(functionName, properties, path);
const serverCode = generateServerCode(
functionName,
properties,
method,
requireSession,
forceAsBody,
forceAsQuery,
noResult,
resultVariable,
if (!options.isServerOnly && !options.isExternalOnly) {
sections.push(
`### Client Side\n\n${formatNote(options.clientOnlyNote)}\`\`\`ts\n${examples.client}\n\`\`\``,
);
return `
### Client Side
\`\`\`ts
${clientCode}
\`\`\`
### Server Side
\`\`\`ts
${serverCode}
\`\`\`
### Type Definition
\`\`\`ts
type ${functionName} = {${typeBody}
}
\`\`\`
`;
});
}
function parseTypeBody(typeBody: string): PropertyDefinition[] {
const properties: PropertyDefinition[] = [];
const lines = typeBody.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*"))
continue;
const propMatch = trimmed.match(
/^(\w+)(\?)?:\s*(.+?)(\s*=\s*["']([^"']+)["'])?(\s*\/\/\s*(.+))?$/,
}
if (!options.isClientOnly) {
sections.push(
`### Server Side\n\n${formatNote(options.serverOnlyNote)}\`\`\`ts\n${examples.server}\n\`\`\``,
);
if (propMatch) {
const [, name, optional, type, , exampleValue, , description] = propMatch;
let cleanType = type.trim();
const cleanExampleValue = exampleValue || "";
cleanType = cleanType.replace(/,$/, "");
properties.push({
name,
type: cleanType,
required: !optional,
description: description || "",
exampleValue: cleanExampleValue,
isServerOnly: false,
isClientOnly: false,
});
}
}
return properties;
}
// Generate client code example
function generateClientCode(
functionName: string,
properties: PropertyDefinition[],
path: string,
): string {
if (!functionName || !path) {
return "// Unable to generate client code - missing function name or path";
}
const clientMethodPath = pathToDotNotation(path);
const body = createClientBody(properties);
return `const { data, error } = await authClient.${clientMethodPath}(${body});`;
}
// Generate server code example
function generateServerCode(
functionName: string,
properties: PropertyDefinition[],
method: string,
requireSession: boolean,
forceAsBody: boolean,
forceAsQuery: boolean,
noResult: boolean,
resultVariable: string,
): string {
if (!functionName) {
return "// Unable to generate server code - missing function name";
}
const body = createServerBody(
properties,
method,
requireSession,
forceAsBody,
forceAsQuery,
sections.push(
`### Type Definition\n\n\`\`\`ts\n${definition.typeDefinition}\n\`\`\``,
);
return `${noResult ? "" : `const ${resultVariable} = `}await auth.api.${functionName}(${body});`;
}
function pathToDotNotation(input: string): string {
return input
.split("/")
.filter(Boolean)
.map((segment) =>
segment
.split("-")
.map((word, i) =>
i === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1),
)
.join(""),
)
.join(".");
}
function createClientBody(props: PropertyDefinition[]): string {
if (props.length === 0) return "{}";
let body = "{\n";
for (const prop of props) {
if (prop.isServerOnly) continue;
let comment = "";
if (!prop.required || prop.description) {
const comments: string[] = [];
if (!prop.required) comments.push("optional");
if (prop.description) comments.push(prop.description);
comment = ` // ${comments.join(", ")}`;
}
body += ` ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`;
}
body += "}";
return body;
}
function createServerBody(
props: PropertyDefinition[],
method: string,
requireSession: boolean,
forceAsBody: boolean,
forceAsQuery: boolean,
): string {
const relevantProps = props.filter((x) => !x.isClientOnly);
if (relevantProps.length === 0 && !requireSession) {
return "{}";
}
let serverBody = "{\n";
if (relevantProps.length > 0) {
const bodyKey =
(method === "POST" || forceAsBody) && !forceAsQuery ? "body" : "query";
serverBody += ` ${bodyKey}: {\n`;
for (const prop of relevantProps) {
let comment = "";
if (!prop.required || prop.description) {
const comments: string[] = [];
if (!prop.required) comments.push("optional");
if (prop.description) comments.push(prop.description);
comment = ` // ${comments.join(", ")}`;
}
serverBody += ` ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`;
}
serverBody += " }";
}
if (requireSession) {
if (relevantProps.length > 0) serverBody += ",";
serverBody +=
"\n // This endpoint requires session cookies.\n headers: await headers()";
}
serverBody += "\n}";
return serverBody;
return sections.join("\n\n");
}
export async function getLLMText(
docPage: InferPageType<typeof source>,
version?: DocsVersion,
): Promise<string> {
const pageData = docPage.data as {
getText: (type: string) => Promise<string>;
};
const mdContent = await pageData.getText("processed");
const renderedContent = extractAPIMethods(mdContent);
const mdContent = await docPage.data.getText("processed");
const renderedContent = await renderPlaceholder(mdContent, {
APIMethod: renderApiMethodMarkdown,
});
const processedContent =
version && version.id !== "latest"
? scopeDocsContent(renderedContent, version)
+7 -2
View File
@@ -1,3 +1,4 @@
import type { LLMsOptions } from "fumadocs-core/mdx-plugins/remark-llms";
import { pageSchema } from "fumadocs-core/source/schema";
import {
defineCollections,
@@ -17,12 +18,16 @@ const docsPageSchema = pageSchema.extend({
sidebarTitle: z.string().min(1).optional(),
});
const processedMarkdownOptions = {
mdxAsPlaceholder: ["APIMethod"],
} satisfies LLMsOptions;
export const docs = defineDocs({
dir: "./content/docs",
docs: {
schema: docsPageSchema,
postprocess: {
includeProcessedMarkdown: true,
includeProcessedMarkdown: processedMarkdownOptions,
},
async: true,
},
@@ -33,7 +38,7 @@ export const docsV16 = defineDocs({
docs: {
schema: docsPageSchema,
postprocess: {
includeProcessedMarkdown: true,
includeProcessedMarkdown: processedMarkdownOptions,
},
async: true,
},