chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import path from "path";
import { REFERENCE_DOCS } from "./lib/files";
import { ReferenceDoc } from "./lib/reference-doc";
// Resolve all source/destination paths relative to the repo root so the
// pipeline behaves the same regardless of where it's invoked from.
const repoRoot = path.resolve(__dirname, "..", "..");
process.chdir(repoRoot);
// allSettled so one missing source (e.g. an SDK file that was renamed
// upstream) doesn't abort the entire pipeline — we still want every other
// reference page to land in shell-docs.
Promise.allSettled(
REFERENCE_DOCS.map(async (referenceDoc) => {
const doc = new ReferenceDoc(referenceDoc);
await doc.generate();
}),
).then((results) => {
const failures = results
.map((r, i) => ({ r, i }))
.filter(({ r }) => r.status === "rejected");
for (const { r, i } of failures) {
const reason = (r as PromiseRejectedResult).reason;
console.error(
`[gen] Failed: ${REFERENCE_DOCS[i].destinationPath}: ${
reason instanceof Error ? reason.message : String(reason)
}`,
);
}
console.log(
`All reference docs processed (${results.length - failures.length}/${results.length} succeeded)`,
);
});
+114
View File
@@ -0,0 +1,114 @@
import * as ts from "typescript";
export class Comments {
static getCleanedCommentsForNode(
node: ts.Node,
sourceFile: ts.SourceFile,
): string {
const fullText = sourceFile.getFullText();
const commentRanges = ts.getLeadingCommentRanges(
fullText,
node.getFullStart(),
);
if (!commentRanges) return "";
return commentRanges
.map((comment) => {
let commentText = fullText.substring(comment.pos, comment.end);
commentText = Comments.removeCommentSyntax(commentText);
// for now, remove @default annotations
commentText = commentText
.split("\n")
.filter((line) => !line.includes("@default"))
.join("\n");
return commentText;
})
.join("\n")
.trim();
}
static getDefaultValueForNode(
node: ts.Node,
sourceFile: ts.SourceFile,
): string | undefined {
const fullText = sourceFile.getFullText();
const commentRanges = ts.getLeadingCommentRanges(
fullText,
node.getFullStart(),
);
if (!commentRanges) return "";
let defaultValue: string | undefined = undefined;
for (const comment of commentRanges) {
let commentText = fullText.substring(comment.pos, comment.end);
commentText = Comments.removeCommentSyntax(commentText);
for (const line of commentText.split("\n")) {
if (line.includes("@default")) {
defaultValue = line.split("@default")[1].trim();
break;
}
}
if (defaultValue !== undefined) break;
}
return defaultValue;
}
static removeCommentSyntax(commentText: string): string {
return commentText
.replace(/\/\*\*|\*\/|\*|\/\* ?/gm, "")
.replace(/^ /gm, "")
.trim();
}
static getFirstCommentBlock(sourceFile: ts.SourceFile): string | null {
for (const statement of sourceFile.statements) {
const comments = Comments.getCleanedCommentsForNode(
statement,
sourceFile,
);
if (comments) return comments;
}
return null;
}
static getTsDocCommentsForFunction(node: ts.Node, sourceFile: ts.SourceFile) {
const params: Record<string, string> = {};
const trivia =
ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) || [];
let comment = "";
for (const range of trivia) {
const commentText = Comments.removeCommentSyntax(
sourceFile.text.substring(range.pos, range.end),
);
const lines = commentText.split("\n").map((line) => line.trim());
if (lines.length && !lines[0].startsWith("@param")) {
comment = lines[0];
}
lines.forEach((line) => {
if (line.startsWith("@param")) {
const parts = line.split(/\s+/);
if (parts.length >= 3) {
const paramName = parts[1];
const description = parts.slice(2).join(" ");
params[paramName] = description.trim();
}
}
});
}
return { comment, params };
}
}
+243
View File
@@ -0,0 +1,243 @@
import type { ReferenceDocConfiguration } from "./reference-doc";
export const REFERENCE_DOCS: ReferenceDocConfiguration[] = [
/* Runtime */
{
sourcePath:
"packages/runtime/src/service-adapters/google/google-genai-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/GoogleGenerativeAIAdapter.mdx",
className: "GoogleGenerativeAIAdapter",
description:
"Copilot Runtime adapter for Google Generative AI (e.g. Gemini).",
},
{
sourcePath: "packages/runtime/src/service-adapters/groq/groq-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/GroqAdapter.mdx",
className: "GroqAdapter",
description: "Copilot Runtime adapter for Groq.",
},
{
sourcePath:
"packages/runtime/src/service-adapters/langchain/langchain-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/LangChainAdapter.mdx",
className: "LangChainAdapter",
description: "Copilot Runtime adapter for LangChain.",
},
{
sourcePath:
"packages/runtime/src/service-adapters/openai/openai-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/OpenAIAdapter.mdx",
className: "OpenAIAdapter",
description: "Copilot Runtime adapter for OpenAI.",
},
{
sourcePath:
"packages/runtime/src/service-adapters/openai/openai-assistant-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/OpenAIAssistantAdapter.mdx",
className: "OpenAIAssistantAdapter",
description: "Copilot Runtime adapter for OpenAI Assistant API.",
},
{
sourcePath:
"packages/runtime/src/service-adapters/anthropic/anthropic-adapter.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/llm-adapters/AnthropicAdapter.mdx",
className: "AnthropicAdapter",
description: "Copilot Runtime adapter for Anthropic.",
},
/* Classes */
{
sourcePath: "packages/react-core/src/lib/copilot-task.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/CopilotTask.mdx",
className: "CopilotTask",
description:
"CopilotTask is used to execute one-off tasks, for example on button click.",
},
{
sourcePath: "packages/runtime/src/lib/runtime/copilot-runtime.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/classes/CopilotRuntime.mdx",
className: "CopilotRuntime",
description:
"Copilot Runtime is the back-end component of CopilotKit, enabling interaction with LLMs.",
},
/* Components */
{
sourcePath: "packages/react-ui/src/components/chat/Chat.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/components/chat/CopilotChat.mdx",
component: "CopilotChat",
description:
"The CopilotChat component, providing a chat interface for interacting with your copilot.",
},
{
sourcePath:
"packages/react-core/src/components/copilot-provider/copilotkit.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/components/CopilotKit.mdx",
component: "CopilotKit",
description:
"The CopilotKit provider component, wrapping your application.",
},
{
sourcePath: "packages/react-ui/src/components/chat/Popup.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/components/chat/CopilotPopup.mdx",
component: "CopilotPopup",
description:
"The CopilotPopup component, providing a popup interface for interacting with your copilot.",
},
{
sourcePath: "packages/react-ui/src/components/chat/Sidebar.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/components/chat/CopilotSidebar.mdx",
component: "CopilotSidebar",
description:
"The CopilotSidebar component, providing a sidebar interface for interacting with your copilot.",
},
{
sourcePath:
"packages/react-textarea/src/components/copilot-textarea/copilot-textarea.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/components/CopilotTextarea.mdx",
component: "CopilotTextarea",
description:
"An AI-powered textarea component for your application, which serves as a drop-in replacement for any textarea.",
},
/* Hooks */
{
sourcePath: "packages/react-core/src/hooks/use-copilot-chat.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCopilotChat.mdx",
hook: "useCopilotChat",
},
{
sourcePath: "packages/react-core/src/hooks/use-copilot-chat-headless_c.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCopilotChatHeadless_c.mdx",
hook: "useCopilotChatHeadless_c",
},
{
sourcePath: "packages/react-ui/src/hooks/use-copilot-chat-suggestions.tsx",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCopilotChatSuggestions.mdx",
hook: "useCopilotChatSuggestions",
description:
"The useCopilotChatSuggestions hook generates suggestions in the chat window based on real-time app state.",
},
{
sourcePath: "packages/react-core/src/hooks/use-copilot-readable.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCopilotReadable.mdx",
hook: "useCopilotReadable",
description:
"The useCopilotReadable hook allows you to provide knowledge to your copilot (e.g. application state).",
},
{
sourcePath: "packages/react-core/src/hooks/use-coagent-state-render.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCoAgentStateRender.mdx",
hook: "useCoAgentStateRender",
description:
"The useCoAgentStateRender hook allows you to render the state of the agent in the chat.",
},
{
sourcePath: "packages/react-core/src/hooks/use-coagent.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCoAgent.mdx",
hook: "useCoAgent",
description:
"The useCoAgent hook allows you to share state bidirectionally between your application and the agent.",
},
{
sourcePath:
"packages/react-core/src/hooks/use-copilot-additional-instructions.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/v1/hooks/useCopilotAdditionalInstructions.mdx",
hook: "useCopilotAdditionalInstructions",
description:
"The useCopilotAdditionalInstructions hook allows you to provide additional instructions to the agent.",
},
/* SDKs */
{
sourcePath: "sdk-python/copilotkit/langgraph.py",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/python/LangGraph.mdx",
title: "LangGraph SDK",
description:
"The CopilotKit LangGraph SDK for Python allows you to build and run LangGraph workflows with CopilotKit.",
pythonSymbols: [
"copilotkit_customize_config",
"copilotkit_exit",
"copilotkit_emit_state",
"copilotkit_emit_message",
"copilotkit_emit_tool_call",
],
},
{
sourcePath: "sdk-python/copilotkit/crewai/crewai_sdk.py",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/python/CrewAI.mdx",
title: "CrewAI SDK",
description:
"The CopilotKit CrewAI SDK for Python allows you to build and run CrewAI agents with CopilotKit.",
pythonSymbols: [
"copilotkit_emit_state",
"copilotkit_predict_state",
"copilotkit_exit",
"copilotkit_emit_message",
"copilotkit_emit_tool_call",
],
},
/* Agents */
{
sourcePath: "sdk-python/copilotkit/langgraph_agui_agent.py",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/python/LangGraphAGUIAgent.mdx",
title: "LangGraphAGUIAgent",
description:
"LangGraphAGUIAgent lets you define your agent for use with CopilotKit.",
pythonSymbols: ["LangGraphAGUIAgent", "CopilotKitConfig"],
},
{
sourcePath: "sdk-python/copilotkit/crewai/crewai_agent.py",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/python/CrewAIAgent.mdx",
title: "CrewAIAgent",
description:
"CrewAIAgent lets you define your agent for use with CopilotKit.",
pythonSymbols: ["CrewAIAgent", "CopilotKitConfig"],
},
{
sourcePath: "sdk-python/copilotkit/sdk.py",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/python/RemoteEndpoints.mdx",
title: "Remote Endpoints",
description:
"CopilotKit Remote Endpoints allow you to connect actions and agents written in Python to your CopilotKit application.",
pythonSymbols: ["CopilotKitRemoteEndpoint", "CopilotKitContext"],
},
{
sourcePath: "packages/sdk-js/src/langgraph/index.ts",
destinationPath:
"showcase/shell-docs/src/content/reference/sdk/js/LangGraph.mdx",
title: "LangGraph SDK",
description:
"The CopilotKit LangGraph SDK for JavaScript allows you to build and run LangGraph workflows with CopilotKit.",
typescriptSymbols: [
"copilotkitCustomizeConfig",
"copilotkitExit",
"copilotkitEmitState",
"copilotkitEmitMessage",
"copilotkitEmitToolCall",
],
},
];
+192
View File
@@ -0,0 +1,192 @@
interface ParsedFunctionDoc {
description: string;
parameters: Array<{
name: string;
type: string;
description: string;
}>;
returns: {
type: string;
description: string;
} | null;
}
/**
* Return an array of parameter objects from a block of text that
* looks like a NumPy docstring parameters section.
*/
function parseParameters(rawParameters: string) {
// Split by lines.
const lines = rawParameters.split("\n");
interface Param {
name: string;
type: string;
description: string;
}
const parameters: Param[] = [];
let currentParam: Param | null = null;
// Regex for a parameter heading, e.g.:
// base_config : Optional[RunnableConfig]
// or with indentation, e.g.:
// base_config : Optional[RunnableConfig]
const paramHeadingRegex = /^[ \t]*([A-Za-z_]\w*)\s*:\s*(.*)$/;
for (const line of lines) {
const trimmedLine = line.trim();
// 1) If line matches the heading format, that's a *new* parameter
const headingMatch = line.match(paramHeadingRegex);
if (headingMatch) {
// If we were building a previous param, push it first
if (currentParam) {
parameters.push(currentParam);
}
// Start a new param
const pName = headingMatch[1];
const pType = headingMatch[2];
currentParam = {
name: pName.trim(),
type: pType.trim(),
description: "", // well accumulate description lines
};
continue;
}
// 2) If its not a heading line and we have a current param, treat it as description
if (currentParam && trimmedLine) {
// Add a space if we already have some text
if (currentParam.description.length > 0) {
currentParam.description += " ";
}
currentParam.description += trimmedLine;
}
}
// Push the last param if we have one
if (currentParam) {
parameters.push(currentParam);
}
return parameters;
}
/**
* Parses out function docstrings for the given Python functions (by name).
*
* @param functionNames - The names of the functions to parse
* @param fileContent - The entire Python file content
* @returns A record where each key is a function name, and the value is the parsed doc info
*/
export function parsePythonDocstrings(
functionNames: string[],
fileContent: string,
): Record<string, ParsedFunctionDoc> {
const results: Record<string, ParsedFunctionDoc> = {};
// Regex to capture:
// 1) Optional leading "async"
// 2) `def`
// 3) function name
// 4) Anything until the `"""` docstring start
// 5) The content inside the triple quotes
//
// The `[\s\S]` is used so that `.` can match newlines.
// The `?` in `[\s\S]*?` makes it non-greedy so we capture the smallest triple-quote block.
// The `m` flag is used so ^ can match the start of lines.
// The `g` flag is for capturing all occurrences.
//
// We also add a lookbehind for ) or : to ensure we match the pattern of a function signature,
// but you can tweak as needed.
// Updated regex to handle multiline signatures
const functionRegex =
/\b(?:async\s+)?(def|class)\s+([A-Za-z_]\w*)[\s\S]*?"""([\s\S]*?)"""/gm;
let match: RegExpExecArray | null;
while ((match = functionRegex.exec(fileContent)) !== null) {
const fnName = match[2];
const docstring = match[3];
// Only parse if the function is in functionNames
if (!functionNames.includes(fnName)) {
continue;
}
// 1) Split docstring by "Parameters" and/or "Returns" blocks
// We'll do a very naive parse in NumPy style:
//
// description (until we see the line "Parameters" or "Returns")
// (optional) Parameters
// (optional) Returns
//
// Example NumPy-ish block:
//
// Parameters
// ----------
// param1 : str
// Description...
// param2 : int
// ...
//
// Returns
// -------
// int
// Some description...
//
// We'll break it up with a simple approach:
const [rawDescription, maybeParamsAndBeyond = ""] = docstring.split(
/\n\s*Parameters\s*[-=]+\s*\n/, // Splits after 'Parameters'
);
let rawParameters = "";
let rawReturns = "";
// Check if there's a "Returns" block in the "maybeParamsAndBeyond" chunk
const returnsSplit = maybeParamsAndBeyond.split(
/\n\s*Returns\s*[-=]+\s*\n/,
);
if (returnsSplit.length === 2) {
// [ paramsBlock, returnsBlock ]
rawParameters = returnsSplit[0];
rawReturns = returnsSplit[1];
} else {
// no Returns block found
rawParameters = maybeParamsAndBeyond;
}
// 2) Parse description: everything up to "Parameters"
const description = rawDescription.trim();
// 3) Parse parameters from rawParameters
const parameters = parseParameters(rawParameters);
// 4) Parse returns from rawReturns
//
// Returns
// -------
// ReturnType
// Description ...
//
// We'll do something simple: get the first line as type, the rest as the description
let returnType = "";
let returnDescription = "";
if (rawReturns.trim()) {
// The first non-empty line is the type
const lines = rawReturns.split("\n").map((l) => l.trim());
returnType = lines[0];
// The rest is the description
returnDescription = lines.slice(1).join(" ");
}
results[fnName] = {
description,
parameters,
returns: returnType
? { type: returnType, description: returnDescription.trim() }
: null,
};
}
return results;
}
+244
View File
@@ -0,0 +1,244 @@
import { SourceFile } from "./source";
import { Comments } from "./comments";
// @ts-ignore
import fs from "fs";
import { parsePythonDocstrings } from "./python";
export interface ReferenceDocConfiguration {
sourcePath: string;
destinationPath: string;
className?: string;
component?: string;
hook?: string;
description?: string;
title?: string;
pythonSymbols?: string[];
typescriptSymbols?: string[];
}
export class ReferenceDoc {
constructor(private readonly referenceDoc: ReferenceDocConfiguration) {}
async generate() {
let generatedDocumentation: string | null = null;
if (this.referenceDoc.pythonSymbols) {
generatedDocumentation = this.generatedDocsPythonSymbols();
} else if (this.referenceDoc.typescriptSymbols) {
generatedDocumentation = await this.generatedDocsTypeScriptSymbols();
} else {
generatedDocumentation = await this.generatedDocsTypeScript();
}
if (generatedDocumentation) {
const dest = this.referenceDoc.destinationPath;
// Ensure parent directory exists so retargeting to a fresh tree
// (e.g. shell-docs) doesn't require manual mkdir.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fs as any).mkdirSync(dest.split("/").slice(0, -1).join("/"), {
recursive: true,
});
fs.writeFileSync(dest, generatedDocumentation);
console.log(
`Successfully autogenerated ${dest} from ${this.referenceDoc.sourcePath}`,
);
}
}
generateTitle(
title: string,
description: string,
sourcePath: string,
): string {
let result = `---\n`;
result += `title: "${title}"\n`;
if (description) {
result += `description: "${description}"\n`;
}
result += `---\n\n`;
result += `{\n`;
result += ` /*\n`;
result += ` * ATTENTION! DO NOT MODIFY THIS FILE!\n`;
result += ` * This page is auto-generated. If you want to make any changes to this page, changes must be made at:\n`;
result += ` * ${sourcePath}\n`;
result += ` */\n`;
result += `}\n`;
return result;
}
generatedDocsPythonSymbols(): string | null {
const content = fs.readFileSync(this.referenceDoc.sourcePath, "utf8");
const parsed = parsePythonDocstrings(
this.referenceDoc.pythonSymbols!,
content,
);
let result = this.generateTitle(
this.referenceDoc.title || "",
this.referenceDoc.description || "",
this.referenceDoc.sourcePath,
);
for (const fn of this.referenceDoc.pythonSymbols!) {
if (fn in parsed) {
const fnDoc = parsed[fn];
result += `## ${fn}\n\n`;
result += `${fnDoc.description}\n\n`;
if (fnDoc.parameters) {
result += `### Parameters\n\n`;
for (const param of fnDoc.parameters) {
const required = !param.type.startsWith("Optional");
result += `<PropertyReference name="${param.name}" type="${param.type}" ${required ? "required" : ""}> \n`;
result += `${param.description}\n`;
result += `</PropertyReference>\n\n`;
}
}
if (fnDoc.returns) {
result += `### Returns\n\n`;
result += `<PropertyReference name="returns" type="${fnDoc.returns.type}">\n`;
result += `${fnDoc.returns.description}\n`;
result += `</PropertyReference>\n\n`;
}
}
}
return result;
}
async generatedDocsTypeScriptSymbols(): Promise<string | null> {
const source = new SourceFile(this.referenceDoc.sourcePath);
await source.parse();
let result = this.generateTitle(
this.referenceDoc.title || "",
this.referenceDoc.description || "",
this.referenceDoc.sourcePath,
);
for (const fn of this.referenceDoc.typescriptSymbols!) {
const args = source.getFunctionArguments(fn);
if (!args) {
console.warn(`${fn} not found in ${this.referenceDoc.sourcePath}`);
continue;
}
const comment = source.getFunctionComment(fn);
result += `## ${fn}\n\n`;
if (comment) {
result += `${comment}\n\n`;
}
if (args) {
result += `### Parameters\n\n`;
for (const arg of args) {
result += `<PropertyReference name="${arg.name}" type="${arg.type}" ${arg.required ? "required" : ""} ${arg.defaultValue ? `default="${arg.defaultValue}"` : ""}> \n`;
result += `${arg.description}\n`;
result += `</PropertyReference>\n\n`;
}
}
}
return result;
}
async generatedDocsTypeScript(): Promise<string | null> {
const source = new SourceFile(this.referenceDoc.sourcePath);
await source.parse();
const comment = Comments.getFirstCommentBlock(source.sourceFile);
if (!comment) {
console.warn(`No comment found for ${this.referenceDoc.sourcePath}`);
console.warn("Skipping...");
return null;
}
// handle imports
const slashes = this.referenceDoc.destinationPath.split("/").length;
let importPathPrefix = "";
for (let i = 0; i < slashes - 2; i++) {
importPathPrefix += "../";
}
let result = this.generateTitle(
this.referenceDoc.className ||
this.referenceDoc.component ||
this.referenceDoc.hook ||
"",
this.referenceDoc.description || "",
this.referenceDoc.sourcePath,
);
result += `${comment}\n\n`;
const arg0Interface = await source.getArg0Interface(
this.referenceDoc.className ||
this.referenceDoc.component ||
this.referenceDoc.hook ||
"",
);
if (arg0Interface) {
const hasProperties = arg0Interface.properties.length > 0;
if (this.referenceDoc.hook && hasProperties) {
result += `## Parameters\n\n`;
} else if (this.referenceDoc.component && hasProperties) {
result += `## Properties\n\n`;
} else if (this.referenceDoc.className && hasProperties) {
result += `## Constructor Parameters\n\n`;
}
for (const property of arg0Interface.properties) {
if (property.comment.includes("@deprecated")) {
continue;
}
const type = property.type.replace(/"/g, "'");
result += `<PropertyReference name="${property.name}" type="${type}" ${property.required ? "required" : ""} ${property.defaultValue ? `default="${property.defaultValue}"` : ""}> \n`;
result += `${property.comment}\n`;
result += `</PropertyReference>\n\n`;
}
} else if (this.referenceDoc.className) {
const constr = source.getConstructorDefinition(
this.referenceDoc.className,
);
if (constr) {
result += `## ${constr.signature}\n\n`;
result += `${constr.comment}\n\n`;
for (const param of constr.parameters) {
const type = param.type.replace(/"/g, "'");
result += `<PropertyReference name="${param.name}" type="${type}" ${param.required ? "required" : ""}>\n`;
result += `${param.comment}\n`;
result += `</PropertyReference>\n\n`;
}
}
}
if (this.referenceDoc.className) {
const methodDefinitions = await source.getPublicMethodDefinitions(
this.referenceDoc.className,
);
for (const method of methodDefinitions) {
if (
method.signature ===
"process(request: CopilotRuntimeChatCompletionRequest)" ||
method.signature === "process(request: CopilotRuntimeRequest)"
) {
// skip the process method
continue;
}
const methodName = method.signature.split("(")[0];
const methodArgs = method.signature.split("(")[1].split(")")[0];
result += `<PropertyReference name="${methodName}" type="${methodArgs}">\n`;
result += `${method.comment}\n\n`;
for (const param of method.parameters) {
const type = param.type.replace(/"/g, "'");
result += ` <PropertyReference name="${param.name}" type="${type}" ${param.required ? "required" : ""}>\n`;
result += ` ${param.comment}\n`;
result += ` </PropertyReference>\n\n`;
}
result += `</PropertyReference>\n\n`;
}
}
return result;
}
}
+411
View File
@@ -0,0 +1,411 @@
import * as ts from "typescript";
// @ts-ignore
import * as fs from "fs";
// @ts-ignore
import { existsSync } from "fs";
// @ts-ignore
import { dirname, resolve } from "path";
import { Comments } from "./comments";
export interface InterfaceDefinition {
name: string;
properties: {
name: string;
type: string;
required: boolean;
comment: string;
defaultValue?: string;
}[];
}
export interface MethodDefinition {
signature: string;
comment: string;
parameters: {
name: string;
type: string;
required: boolean;
comment: string;
}[];
}
export class SourceFile {
public sourceFile!: ts.SourceFile;
constructor(private readonly filePath: string) {}
parse() {
const fileContents = fs.readFileSync(this.filePath, "utf8");
this.sourceFile = ts.createSourceFile(
this.filePath,
fileContents,
ts.ScriptTarget.Latest,
true,
);
}
/**
* Get the interface definition of the first argument of the function or class constructor.
*/
getArg0Interface(name: string): InterfaceDefinition | null {
let interfaceName: string = "";
const visit = (node: ts.Node) => {
// if we find a matching function declaration
if (
ts.isFunctionDeclaration(node) &&
node.name?.getText() === name &&
node.parameters.length &&
node.parameters[0].type &&
ts.isTypeReferenceNode(node.parameters[0].type)
) {
interfaceName = node.parameters[0].type.typeName.getText();
}
// if we find a matching class declaration
else if (ts.isClassDeclaration(node) && node.name?.getText() === name) {
const constructor = node.members.find((member) =>
ts.isConstructorDeclaration(member),
) as ts.ConstructorDeclaration;
if (
constructor &&
constructor.parameters.length &&
constructor.parameters[0].type &&
ts.isTypeReferenceNode(constructor.parameters[0].type)
) {
interfaceName = constructor.parameters[0].type.typeName.getText();
}
}
// if we find a matching forwardRef declaration
else if (ts.isVariableStatement(node) || ts.isVariableDeclaration(node)) {
const declarations = ts.isVariableStatement(node)
? node.declarationList.declarations
: [node];
declarations.forEach((declaration) => {
if (
ts.isVariableDeclaration(declaration) &&
declaration.name.getText() === name &&
declaration.initializer &&
ts.isCallExpression(declaration.initializer) &&
declaration.initializer.expression.getText() === "React.forwardRef"
) {
const func = declaration.initializer.arguments[0];
if (
ts.isArrowFunction(func) &&
func.parameters.length &&
func.parameters[0].type &&
ts.isTypeReferenceNode(func.parameters[0].type)
) {
interfaceName = func.parameters[0].type.typeName.getText();
}
}
});
}
ts.forEachChild(node, visit);
};
// analyze the source file
visit(this.sourceFile);
if (!interfaceName) {
return null;
}
// extract the interface definition
let interfaceFilePath =
this.findTypeDeclaration(interfaceName) || this.filePath;
const interfaceSource = new SourceFile(interfaceFilePath);
interfaceSource.parse();
return interfaceSource.extractInterfaceDefinition(interfaceName);
}
/**
* Extracts the interface definition from the source file.
*/
protected extractInterfaceDefinition(
interfaceName: string,
): InterfaceDefinition {
const definition: InterfaceDefinition = {
name: interfaceName,
properties: [],
};
const visit = (node: ts.Node) => {
if (ts.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
let omittedProperties: Set<string> = new Set();
// Check for extended interfaces
if (node.heritageClauses && node.heritageClauses.length > 0) {
const firstClause = node.heritageClauses[0];
firstClause.types.forEach((type) => {
const typeName = type.expression.getText(this.sourceFile);
let extendedInterfaceName = typeName;
// Check if the type is an Omit
if (typeName.startsWith("Omit")) {
const omitArgs = type.typeArguments;
if (omitArgs && omitArgs.length > 0) {
extendedInterfaceName = omitArgs[0].getText(this.sourceFile);
if (omitArgs.length > 1) {
const omittedProps = omitArgs[1];
if (ts.isUnionTypeNode(omittedProps)) {
omittedProps.types.forEach((prop) => {
omittedProperties.add(
prop.getText(this.sourceFile).replace(/['"]/g, ""),
);
});
} else if (ts.isLiteralTypeNode(omittedProps)) {
omittedProperties.add(
omittedProps
.getText(this.sourceFile)
.replace(/['"]/g, ""),
);
}
}
}
}
const extendedInterfaceFilePath = this.findTypeDeclaration(
extendedInterfaceName,
);
if (extendedInterfaceFilePath) {
// Parse the extended interface file and extract its definition
const extendedInterfaceSource = new SourceFile(
extendedInterfaceFilePath,
);
extendedInterfaceSource.parse();
const extendedDefinition =
extendedInterfaceSource.extractInterfaceDefinition(
extendedInterfaceName,
);
// Merge properties from the extended interface, excluding omitted properties
extendedDefinition.properties.forEach((prop) => {
if (!omittedProperties.has(prop.name)) {
definition.properties.push(prop);
}
});
}
});
}
node.members.forEach((member) => {
if (ts.isPropertySignature(member)) {
const propertyName = member.name.getText(this.sourceFile);
const comment = Comments.getCleanedCommentsForNode(
member,
this.sourceFile,
);
const defaultValue = Comments.getDefaultValueForNode(
member,
this.sourceFile,
);
definition.properties.push({
name: propertyName,
type: (member.type?.getText(this.sourceFile) || "unknown")
.replace(/\n/g, "")
.replace(/\s+/g, " "),
required: !member.questionToken,
comment,
defaultValue,
});
}
});
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return definition;
}
/**
* Finds the absolute declaration file path of a type if imported.
*/
findTypeDeclaration(typeName: string): string | null {
for (const statement of this.sourceFile.statements) {
if (ts.isImportDeclaration(statement) && statement.importClause) {
const namedBindings = statement.importClause.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
const imports = namedBindings.elements.filter(
(element) => element.name.text === typeName,
);
if (imports.length > 0) {
const moduleSpecifier = (
statement.moduleSpecifier as ts.StringLiteral
).text;
// Resolve the path relative to the directory of the current source file
let resolvedPath = resolve(
dirname(this.sourceFile.fileName),
moduleSpecifier,
);
if (existsSync(resolvedPath + ".ts")) {
return resolvedPath + ".ts";
} else if (existsSync(resolvedPath + ".tsx")) {
return resolvedPath + ".tsx";
}
return null;
}
}
}
}
return null;
}
/**
* Get the public method definitions of a class.
*/
getPublicMethodDefinitions(className: string): MethodDefinition[] {
const methodDefinitions: MethodDefinition[] = [];
const visit = (node: ts.Node) => {
if (ts.isClassDeclaration(node) && node.name?.getText() === className) {
node.members.forEach((member) => {
if (
ts.isMethodDeclaration(member) &&
member.modifiers?.every(
(modifier) => modifier.kind !== ts.SyntaxKind.PrivateKeyword,
)
) {
methodDefinitions.push(this.extractMethodDefinition(member));
}
});
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return methodDefinitions;
}
/**
* Extracts the method definition from a method declaration.
*/
private extractMethodDefinition(
member: ts.MethodDeclaration,
): MethodDefinition {
const functionComments = Comments.getTsDocCommentsForFunction(
member,
this.sourceFile,
);
const name = ts.isConstructorDeclaration(member)
? "constructor"
: member.name.getText();
let signature =
name +
"(" +
member.parameters.map((param) => param.getText()).join(", ") +
")";
signature = signature.replace(/</g, "&lt;").replace(/>/g, "&gt;");
return {
signature,
comment: functionComments.comment,
parameters: member.parameters.map((param) => {
return {
name: param.name.getText(),
type: param.type?.getText() || "unknown",
required: !param.questionToken,
comment: functionComments.params[param.name.getText()] || "",
};
}),
};
}
/**
* Get the constructor definition of a class.
*/
getConstructorDefinition(className: string): MethodDefinition | null {
let constructorDefinition: MethodDefinition | null = null;
const visit = (node: ts.Node) => {
if (ts.isClassDeclaration(node) && node.name?.getText() === className) {
const constr = node.members.find((member) =>
ts.isConstructorDeclaration(member),
) as any;
if (constr) {
constructorDefinition = this.extractMethodDefinition(constr);
}
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return constructorDefinition;
}
/**
* Get the block comment preceding a function definition.
*/
getFunctionComment(functionName: string): string | null {
let functionComment: string | null = null;
const visit = (node: ts.Node) => {
if (
ts.isFunctionDeclaration(node) &&
node.name?.getText() === functionName
) {
functionComment = Comments.getCleanedCommentsForNode(
node,
this.sourceFile,
);
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return functionComment;
}
/**
* Get the arguments of a function by its name.
*/
getFunctionArguments(functionName: string): Array<{
name: string;
type: string;
required: boolean;
defaultValue: string;
description: string;
}> | null {
let functionArguments: Array<{
name: string;
type: string;
required: boolean;
defaultValue: string;
description: string;
}> | null = null;
const visit = (node: ts.Node) => {
if (
ts.isFunctionDeclaration(node) &&
node.name?.getText() === functionName
) {
functionArguments = node.parameters.map((param) => {
const name = param.name.getText();
const type = param.type?.getText() || "unknown";
const required = !param.questionToken;
const defaultValue = param.initializer
? param.initializer.getText()
: "";
// Use the comment cleaning method
const description = Comments.getCleanedCommentsForNode(
param,
this.sourceFile,
);
return { name, type, required, defaultValue, description };
});
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return functionArguments;
}
}