mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
docs: share API method rendering
This commit is contained in:
+110
-702
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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:");
|
||||
});
|
||||
});
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user