UIFlowchartCreator
by umshere
- src
- utils
import path from "path";
import axios from "axios";
import {
RepoContents,
fetchLocalRepoContents,
fetchGitHubRepoContents,
} from "./repoHandlers.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
export interface ComponentInfo {
name: string;
type: "page" | "layout" | "component";
filePath: string;
imports: string[];
children: ComponentInfo[];
}
export async function parseUIFlow(
contents: RepoContents[],
isLocal: boolean,
fileExtensions: string[] = ["js", "jsx", "ts", "tsx"]
): Promise<string> {
console.log(
`[MCP] Parsing UI flow with extensions: ${fileExtensions.join(", ")}`
);
const components: { [key: string]: ComponentInfo } = {};
async function processContents(
currentContents: RepoContents[],
currentPath: string = ""
) {
for (const item of currentContents) {
if (
item.type === "file" &&
fileExtensions.some((ext) => item.name.endsWith(`.${ext}`))
) {
let content: string;
if (isLocal) {
content = item.content || "";
} else {
try {
const response = await axios.get(item.download_url || "");
content = response.data;
} catch (error) {
console.warn(
`[MCP] Failed to fetch content for ${item.name}: ${error}`
);
continue;
}
}
const componentName = item.name.split(".")[0];
const componentPath = path.join(currentPath, componentName);
const componentType = getComponentType(componentPath);
components[componentPath] = {
name: componentName,
type: componentType,
filePath: path.join(currentPath, item.name),
imports: [],
children: [],
};
// Analyze import statements
const importMatches = content.match(
/import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g
);
if (importMatches) {
importMatches.forEach((match) => {
const [, importedComponent, importPath] =
match.match(
/import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/
) || [];
if (importedComponent) {
const cleanedImport = importedComponent
.replace(/[{}]/g, "")
.trim();
const resolvedPath = path.join(
currentPath,
path.dirname(importPath),
cleanedImport
);
components[componentPath].imports.push(resolvedPath);
}
});
}
} else if (item.type === "dir") {
const subContents = isLocal
? await fetchLocalRepoContents(item.path)
: await fetchGitHubRepoContents(
item.owner || "",
item.repo || "",
item.path
);
await processContents(subContents, path.join(currentPath, item.name));
}
}
}
await processContents(contents);
// Build component hierarchy
const rootComponents: ComponentInfo[] = [];
Object.values(components).forEach((component) => {
component.imports.forEach((importPath) => {
if (components[importPath]) {
components[importPath].children.push(component);
}
});
if (component.imports.length === 0) {
rootComponents.push(component);
}
});
return JSON.stringify(rootComponents, null, 2);
}
function getComponentType(
componentPath: string
): "page" | "layout" | "component" {
// Special handling for App component
if (componentPath.toLowerCase().includes("app")) {
return "page";
}
// Detect pages based on common patterns
if (
componentPath.includes("pages") ||
componentPath.includes("views") ||
componentPath.toLowerCase().includes("meals")
) {
return "page";
}
// Detect layouts based on common patterns
if (
componentPath.includes("layouts") ||
componentPath.toLowerCase().includes("header")
) {
return "layout";
}
// Default to component
return "component";
}
export function generateMermaidFlowchart(components: ComponentInfo[]): string {
let chart = "flowchart TD\n";
// Create a map of all components for quick lookup
const componentMap = new Map<string, ComponentInfo>();
components.forEach((component) => {
componentMap.set(component.name, component);
});
// Create nodes with proper styling and hierarchy
const createNode = (component: ComponentInfo, depth: number = 0): string => {
const nodeId = component.name.replace(/[^a-zA-Z0-9]/g, "_");
const indent = " ".repeat(depth);
// Determine node style based on type
let nodeStyle = "";
switch (component.type) {
case "page":
nodeStyle = "(( ))";
break;
case "layout":
nodeStyle = "{{ }}";
break;
default:
nodeStyle = "[/ /]";
}
// Add node with proper indentation
chart += `${indent}${nodeId}${nodeStyle}["${component.name} (${component.type})"]\n`;
// Recursively process children
component.children.forEach((child) => {
const childComponent = componentMap.get(child.name);
if (childComponent) {
createNode(childComponent, depth + 1);
}
});
return nodeId;
};
// Find root components (those with no parents)
const rootComponents = components.filter(
(component) =>
!components.some((c) =>
c.children.some((child) => child.name === component.name)
)
);
// Start building the chart from root components
rootComponents.forEach((component) => {
createNode(component);
});
// Create relationships with labels
components.forEach((component) => {
const parentId = component.name.replace(/[^a-zA-Z0-9]/g, "_");
component.children.forEach((child) => {
const childId = child.name.replace(/[^a-zA-Z0-9]/g, "_");
const relationshipType = determineRelationshipType(component, child);
chart += ` ${parentId} -->|${relationshipType}| ${childId}\n`;
});
});
// Validate Mermaid.js syntax
try {
// Basic validation - check for required elements
if (!chart.includes("flowchart TD")) {
throw new Error("Missing flowchart declaration");
}
if (!chart.match(/\[.*\]/)) {
throw new Error("Missing node definitions");
}
if (!chart.match(/-->|--/)) {
throw new Error("Missing relationship definitions");
}
} catch (error) {
console.error("[MCP] Mermaid.js validation error:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to generate valid Mermaid.js chart: ${error}`
);
}
return chart;
}
function determineRelationshipType(
parent: ComponentInfo,
child: ComponentInfo
): string {
if (parent.type === "layout" && child.type === "page") {
return "contains";
}
if (parent.type === "page" && child.type === "component") {
return "uses";
}
if (parent.type === "component" && child.type === "component") {
return "composes";
}
return "relates to";
}