From a72c7540d0021ba2fb9ab818fb7cf27a4cdd8531 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 2 Sep 2025 09:45:29 +0500 Subject: [PATCH 1/3] local setup: streamable http for better logging --- CONTRIBUTING.md | 202 +++++++++++++++++++++++++++++++++++++++++++ README.md | 33 ++++++- package.json | 6 +- src/cli.mts | 7 +- src/config.mts | 42 +++++++++ src/server.mts | 195 +++++++++++++++++++++++++++++++++++++++++ src/utils/logger.mts | 17 ++++ 7 files changed, 498 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 src/utils/logger.mts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..13b37be --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# Contributing to Figma Flutter MCP + +Thank you for your interest in contributing to Figma Flutter MCP! This guide will help you get started with development and testing. + +## ๐Ÿš€ Quick Start for Contributors + +### Prerequisites +- Node.js 18+ +- npm or yarn +- Figma API Key (for testing) +- Git + +### Setup Development Environment + +1. **Fork and Clone** + ```bash + git clone https://github.com/your-username/figma-flutter-mcp.git + cd figma-flutter-mcp + npm install + ``` + +2. **Create .env file** + ```bash + # Create .env file with your Figma API key + echo "FIGMA_API_KEY=your-figma-api-key-here" > .env + ``` + +3. **Start Development Server** + ```bash + npm run dev + ``` + +## ๐Ÿ“‹ Development Guidelines + +### Code Style +- Use TypeScript for all new code +- Follow existing code patterns and conventions +- Use meaningful variable and function names +- Add docs in `docs/` if you think its neccessary + +### Project Structure +``` +src/ +โ”œโ”€โ”€ cli.mts # CLI entry point +โ”œโ”€โ”€ server.mts # MCP server implementation +โ”œโ”€โ”€ config.mts # Configuration handling +โ”œโ”€โ”€ extractors/ # Figma data extractors +โ”‚ โ”œโ”€โ”€ colors/ +โ”‚ โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ screens/ +โ”‚ โ””โ”€โ”€ typography/ +โ”œโ”€โ”€ tools/ # Flutter code generators +โ”œโ”€โ”€ services/ # External service integrations +โ”œโ”€โ”€ types/ # TypeScript type definitions +โ””โ”€โ”€ utils/ # Utility functions +``` + +### Making Changes + +1. **Create a Branch** + ```bash + git checkout -b feature/your-feature-name + # or + git checkout -b fix/issue-description + ``` + +2. **Make Your Changes** + - Write clean, documented code + - Add tests for new features + - Update documentation as needed + +3. **Test Your Changes** + ```bash + # Build and check for errors + npm run build + + # Test locally + npm run dev + ``` + +4. **Commit and Push** + ```bash + git add . + git commit -m "feat: add new feature description" + git push origin feature/your-feature-name + ``` + +5. **Create Pull Request** + - Use descriptive titles and descriptions + - Reference any related issues + - Include screenshots/examples if applicable + +## ๐Ÿงช Local Testing & Development + +The project supports HTTP server mode for easier development and testing. This allows you to test MCP tools without setting up a full MCP client. + +### Setting Up Your Environment + +If you haven't already set up your Figma API key: +```bash +# Create .env file +echo "FIGMA_API_KEY=your-figma-api-key-here" > .env +``` + +**Get your Figma API Key:** +1. Go to [Figma Settings > Personal Access Tokens](https://www.figma.com/developers/api#access-tokens) +2. Generate a new personal access token +3. Copy the token and add it to your `.env` file + +โš ๏ธ **Important**: Never commit your `.env` file to version control. It's already included in `.gitignore`. + +### Development Server Options + +#### Using npm scripts (recommended) +```bash +# Start HTTP server on default port 3333 +npm run dev + +# Start HTTP server on a specific port +npm run dev:port 4000 + +# Start in stdio mode (for MCP clients) +npm run dev:stdio +``` + +#### Using direct commands +```bash +# Start HTTP server +npx tsx src/cli.mts --http + +# Start HTTP server on specific port +npx tsx src/cli.mts --http --port 4000 + +# Start in stdio mode +npx tsx src/cli.mts --stdio +``` + +#### Using built version +```bash +# Build first +npm run build + +# Start HTTP server +node dist/cli.mjs --http + +# Start HTTP server on specific port +node dist/cli.mjs --http --port 4000 +``` + +## Connecting to the Server + +### MCP Client Configuration + +To connect an MCP client to the local HTTP server, add this configuration to your MCP JSON config file: + +```json +{ + "mcpServers": { + "local-figma-flutter-mcp": { + "url": "http://localhost:3333/mcp" + } + } +} +``` + +## Available Endpoints + +When the HTTP server is running, the following endpoints are available: + +- **POST /mcp** - Main Streamable HTTP endpoint for MCP communication +- **GET /mcp** - Session management for StreamableHTTP +- **DELETE /mcp** - Session termination for StreamableHTTP +- **GET /sse** - Server-Sent Events endpoint (alternative transport) +- **POST /messages** - Message endpoint for SSE transport + +## Environment Variables + +You can configure the server using environment variables. The recommended approach is to use a `.env` file: + +### Using .env file (Recommended) +```env +# Required: Your Figma API key +FIGMA_API_KEY=your-figma-api-key-here + +# Optional: Enable HTTP mode by default +HTTP_MODE=true + +# Optional: Set default HTTP port +HTTP_PORT=3333 +``` + +## ๐Ÿ“‹ Pull Request Checklist + +Before submitting a PR: +- [ ] Code builds without errors (`npm run build`) +- [ ] Tests pass (if applicable) +- [ ] Documentation updated +- [ ] PR description explains changes +- [ ] Related issues referenced +- [ ] Follows existing code style + +Thank you for contributing to Figma Flutter MCP! ๐Ÿš€ diff --git a/README.md b/README.md index d9add57..c311922 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,37 @@ Once you've the FIGMA API KEY, you can setup the MCP in cursor as follows: > NOTE: If you've installed this MCP as `npm` package make sure to keep it updated to latest version. Sometimes, it caches the old version and keep showing you error like "Not being able to use tool call" or "Figma API key setup is not working" etc. -### ๐Ÿง‘๐Ÿผโ€๐Ÿ’ป Local Setup -Please ensure that in local setup your version remains updated with your local server, sometimes `npm i` has installed the server globally for you and the keeps on overriding your local changes because of which you might not see any update. + +### ๐Ÿš€ Quick Start for Local Testing + +For quick local testing, you can run the server via HTTP instead of stdio: + +```bash +# Clone and setup +git clone figma-flutter-mcp +cd figma-flutter-mcp +npm install + +# Create .env file with your Figma API key +echo "FIGMA_API_KEY=your-figma-api-key-here" > .env + +# Start HTTP server for local testing +npm run dev +``` + +Then add this to your MCP client configuration: + +```json +{ + "mcpServers": { + "local-figma-flutter": { + "url": "http://localhost:3333/mcp" + } + } +} +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions. #### 0. Prerequisites - Node.js 18+ diff --git a/package.json b/package.json index 89d158d..9d77516 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "scripts": { "build": "tsc", "start": "node dist/cli.mjs", - "dev": "tsx src/cli.mts", + "dev": "tsx src/cli.mts --http", + "dev:stdio": "tsx src/cli.mts --stdio", + "dev:port": "tsx src/cli.mts --http --port", "changeset": "changeset add", "version": "changeset version && npm install --lockfile-only", "release": "changeset publish" @@ -23,6 +25,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "dotenv": "^17.2.1", + "express": "^4.18.2", "node-fetch": "^3.3.2", "yargs": "^17.7.2", "zod": "^3.22.4" @@ -30,6 +33,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.6", + "@types/express": "^4.17.21", "@types/node": "^20.10.0", "@types/node-fetch": "^2.6.9", "@types/yargs": "^17.0.32", diff --git a/src/cli.mts b/src/cli.mts index 4a19012..252d15d 100644 --- a/src/cli.mts +++ b/src/cli.mts @@ -1,15 +1,20 @@ #!/usr/bin/env node import {getServerConfig} from './config.mjs'; -import {startMcpServer} from './server.mjs'; +import {startMcpServer, startHttpServer} from './server.mjs'; async function startServer(): Promise { const config = getServerConfig(); if (config.isStdioMode) { await startMcpServer(config.figmaApiKey); + } else if (config.isHttpMode) { + console.log('Starting Figma Flutter MCP Server in HTTP mode...'); + await startHttpServer(config.httpPort, config.figmaApiKey); } else { console.log('Starting Figma Flutter MCP Server...'); console.log('Use --stdio flag for MCP client communication'); + console.log('Use --http flag for local testing via HTTP'); + console.log('Use --help for more options'); } } diff --git a/src/config.mts b/src/config.mts index 11edd5e..5b0c7ba 100644 --- a/src/config.mts +++ b/src/config.mts @@ -7,10 +7,14 @@ export interface ServerConfig { figmaApiKey: string; outputFormat: "yaml" | "json"; isStdioMode: boolean; + isHttpMode: boolean; + httpPort: number; configSources: { figmaApiKey: "cli" | "env"; envFile: "cli" | "default"; stdio: "cli" | "env" | "default"; + http: "cli" | "env" | "default"; + port: "cli" | "env" | "default"; }; } @@ -23,6 +27,8 @@ interface CliArgs { "figma-api-key"?: string; env?: string; stdio?: boolean; + http?: boolean; + port?: number; } export function getServerConfig(): ServerConfig { @@ -42,6 +48,16 @@ export function getServerConfig(): ServerConfig { description: "Run in stdio mode for MCP client communication", default: false, }, + http: { + type: "boolean", + description: "Run in HTTP mode for local testing", + default: false, + }, + port: { + type: "number", + description: "Port number for HTTP server", + default: 3333, + }, }) .help() .version(process.env.npm_package_version || "0.0.1") @@ -66,10 +82,14 @@ export function getServerConfig(): ServerConfig { figmaApiKey: "", outputFormat: "json", isStdioMode: false, + isHttpMode: false, + httpPort: 3333, configSources: { figmaApiKey: "env", envFile: envFileSource, stdio: "default", + http: "default", + port: "default", }, }; @@ -91,6 +111,24 @@ export function getServerConfig(): ServerConfig { config.configSources.stdio = "env"; } + // Handle HTTP mode + if (argv.http) { + config.isHttpMode = true; + config.configSources.http = "cli"; + } else if (process.env.HTTP_MODE === "true") { + config.isHttpMode = true; + config.configSources.http = "env"; + } + + // Handle port configuration + if (argv.port) { + config.httpPort = argv.port; + config.configSources.port = "cli"; + } else if (process.env.HTTP_PORT) { + config.httpPort = parseInt(process.env.HTTP_PORT, 10); + config.configSources.port = "env"; + } + // Validate configuration if (!config.figmaApiKey) { console.error("Error: FIGMA_API_KEY is required (via CLI argument or .env file)"); @@ -105,6 +143,10 @@ export function getServerConfig(): ServerConfig { `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})` ); console.log(`- STDIO_MODE: ${config.isStdioMode} (source: ${config.configSources.stdio})`); + console.log(`- HTTP_MODE: ${config.isHttpMode} (source: ${config.configSources.http})`); + if (config.isHttpMode) { + console.log(`- HTTP_PORT: ${config.httpPort} (source: ${config.configSources.port})`); + } console.log(); // Empty line for better readability } diff --git a/src/server.mts b/src/server.mts index ee40b56..bb439f2 100644 --- a/src/server.mts +++ b/src/server.mts @@ -1,6 +1,13 @@ +import { randomUUID } from "node:crypto"; +import express, { type Request, type Response } from "express"; +import { Server } from "http"; import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import {registerAllTools} from "./tools/index.mjs"; +import { Logger } from "./utils/logger.mjs"; export function createServer(figmaApiKey: string) { const server = new McpServer({ @@ -12,6 +19,12 @@ export function createServer(figmaApiKey: string) { return server; } +let httpServer: Server | null = null; +const transports = { + streamable: {} as Record, + sse: {} as Record, +}; + export async function startMcpServer(figmaApiKey: string): Promise { try { const server = createServer(figmaApiKey); @@ -22,4 +35,186 @@ export async function startMcpServer(figmaApiKey: string): Promise { console.error("Failed to start MCP server:", error); process.exit(1); } +} + +export async function startHttpServer(port: number, figmaApiKey: string): Promise { + const mcpServer = createServer(figmaApiKey); + const app = express(); + + // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint + app.use("/mcp", express.json()); + + // Modern Streamable HTTP endpoint + app.post("/mcp", async (req, res) => { + Logger.log("Received StreamableHTTP request"); + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports.streamable[sessionId]) { + // Reuse existing transport + Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId); + transport = transports.streamable[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + Logger.log("New initialization request for StreamableHTTP sessionId", sessionId); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + // Store the transport by session ID + transports.streamable[sessionId] = transport; + }, + }); + transport.onclose = () => { + if (transport.sessionId) { + delete transports.streamable[transport.sessionId]; + } + }; + await mcpServer.connect(transport); + } else { + // Invalid request + Logger.log("Invalid request:", req.body); + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: null, + }); + return; + } + + let progressInterval: NodeJS.Timeout | null = null; + const progressToken = req.body.params?._meta?.progressToken; + let progress = 0; + if (progressToken) { + Logger.log( + `Setting up progress notifications for token ${progressToken} on session ${sessionId}`, + ); + progressInterval = setInterval(async () => { + Logger.log("Sending progress notification", progress); + await mcpServer.server.notification({ + method: "notifications/progress", + params: { + progress, + progressToken, + }, + }); + progress++; + }, 1000); + } + + Logger.log("Handling StreamableHTTP request"); + await transport.handleRequest(req, res, req.body); + + if (progressInterval) { + clearInterval(progressInterval); + } + Logger.log("StreamableHTTP request handled"); + }); + + // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) + const handleSessionRequest = async (req: Request, res: Response) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + if (!sessionId || !transports.streamable[sessionId]) { + res.status(400).send("Invalid or missing session ID"); + return; + } + + console.log(`Received session termination request for session ${sessionId}`); + + try { + const transport = transports.streamable[sessionId]; + await transport.handleRequest(req, res); + } catch (error) { + console.error("Error handling session termination:", error); + if (!res.headersSent) { + res.status(500).send("Error processing session termination"); + } + } + }; + + // Handle GET requests for server-to-client notifications via SSE + app.get("/mcp", handleSessionRequest); + + // Handle DELETE requests for session termination + app.delete("/mcp", handleSessionRequest); + + app.get("/sse", async (req, res) => { + Logger.log("Establishing new SSE connection"); + const transport = new SSEServerTransport("/messages", res); + Logger.log(`New SSE connection established for sessionId ${transport.sessionId}`); + + transports.sse[transport.sessionId] = transport; + res.on("close", () => { + delete transports.sse[transport.sessionId]; + }); + + await mcpServer.connect(transport); + }); + + app.post("/messages", async (req, res) => { + const sessionId = req.query.sessionId as string; + const transport = transports.sse[sessionId]; + if (transport) { + Logger.log(`Received SSE message for sessionId ${sessionId}`); + await transport.handlePostMessage(req, res); + } else { + res.status(400).send(`No transport found for sessionId ${sessionId}`); + return; + } + }); + + httpServer = app.listen(port, () => { + Logger.log(`HTTP server listening on port ${port}`); + Logger.log(`SSE endpoint available at http://localhost:${port}/sse`); + Logger.log(`Message endpoint available at http://localhost:${port}/messages`); + Logger.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`); + }); + + process.on("SIGINT", async () => { + Logger.log("Shutting down server..."); + + // Close all active transports to properly clean up resources + await closeTransports(transports.sse); + await closeTransports(transports.streamable); + + Logger.log("Server shutdown complete"); + process.exit(0); + }); +} + +async function closeTransports( + transports: Record, +) { + for (const sessionId in transports) { + try { + await transports[sessionId]?.close(); + delete transports[sessionId]; + } catch (error) { + console.error(`Error closing transport for session ${sessionId}:`, error); + } + } +} + +export async function stopHttpServer(): Promise { + if (!httpServer) { + throw new Error("HTTP server is not running"); + } + + return new Promise((resolve, reject) => { + httpServer!.close((err: Error | undefined) => { + if (err) { + reject(err); + return; + } + httpServer = null; + const closing = Object.values(transports.sse).map((transport) => { + return transport.close(); + }); + Promise.all(closing).then(() => { + resolve(); + }); + }); + }); } \ No newline at end of file diff --git a/src/utils/logger.mts b/src/utils/logger.mts new file mode 100644 index 0000000..0c6ab4c --- /dev/null +++ b/src/utils/logger.mts @@ -0,0 +1,17 @@ +export class Logger { + static log(...args: any[]): void { + console.error('[MCP Server]', ...args); + } + + static error(...args: any[]): void { + console.error('[MCP Server ERROR]', ...args); + } + + static warn(...args: any[]): void { + console.error('[MCP Server WARN]', ...args); + } + + static info(...args: any[]): void { + console.error('[MCP Server INFO]', ...args); + } +} From 990dfc6070a64131182df9371ad94b7d5fb114ab Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 2 Sep 2025 11:30:54 +0500 Subject: [PATCH 2/3] docs(changeset): Break-even point: ~22 component analyses --- .changeset/giant-crabs-rescue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/giant-crabs-rescue.md diff --git a/.changeset/giant-crabs-rescue.md b/.changeset/giant-crabs-rescue.md new file mode 100644 index 0000000..9a0e380 --- /dev/null +++ b/.changeset/giant-crabs-rescue.md @@ -0,0 +1,5 @@ +--- +"figma-flutter-mcp": minor +--- + +Break-even point: ~22 component analyses From 0e5864e5250e97f3cff9ab3653d4e327eb6671d1 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 2 Sep 2025 11:31:17 +0500 Subject: [PATCH 3/3] deduplication added for components based reducing tokens by 45% almost --- .gitignore | 3 +- .../components/deduplicated-extractor.mts | 186 +++++++++++ src/extractors/components/index.mts | 7 + src/extractors/flutter/index.mts | 7 + src/extractors/flutter/style-library.mts | 161 ++++++++++ .../flutter/components/component-tool.mts | 297 ++++++++++++++++-- .../components/deduplicated-helpers.mts | 295 +++++++++++++++++ 7 files changed, 923 insertions(+), 33 deletions(-) create mode 100644 src/extractors/components/deduplicated-extractor.mts create mode 100644 src/extractors/flutter/index.mts create mode 100644 src/extractors/flutter/style-library.mts create mode 100644 src/tools/flutter/components/deduplicated-helpers.mts diff --git a/.gitignore b/.gitignore index 66c5638..553aeac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .env package-lock.json dist/ -output/ \ No newline at end of file +output/ +reports/ \ No newline at end of file diff --git a/src/extractors/components/deduplicated-extractor.mts b/src/extractors/components/deduplicated-extractor.mts new file mode 100644 index 0000000..92cec6f --- /dev/null +++ b/src/extractors/components/deduplicated-extractor.mts @@ -0,0 +1,186 @@ +// src/extractors/components/deduplicated-extractor.mts + +import type { FigmaNode } from '../../types/figma.mjs'; +import type { FlutterStyleDefinition } from '../flutter/style-library.mjs'; +import { FlutterStyleLibrary } from '../flutter/style-library.mjs'; +import { + extractStylingInfo, + extractLayoutInfo, + extractMetadata, + extractTextInfo, + createNestedComponentInfo +} from './extractor.mjs'; +import type { + ComponentMetadata, + LayoutInfo, + StylingInfo, + NestedComponentInfo, + TextInfo +} from './types.mjs'; + +export interface DeduplicatedComponentAnalysis { + metadata: ComponentMetadata; + styleRefs: Record; + children: DeduplicatedComponentChild[]; + nestedComponents: NestedComponentInfo[]; + newStyleDefinitions?: Record; +} + +export interface DeduplicatedComponentChild { + nodeId: string; + name: string; + type: string; + styleRefs: string[]; + semanticType?: string; + textContent?: string; +} + +export class DeduplicatedComponentExtractor { + private styleLibrary = FlutterStyleLibrary.getInstance(); + + async analyzeComponent(node: FigmaNode, trackNewStyles = false): Promise { + const styling = extractStylingInfo(node); + const layout = extractLayoutInfo(node); + const metadata = extractMetadata(node, false); // assuming not user-defined unless specified + + const styleRefs: Record = {}; + const newStyles = new Set(); + + // Process decoration styles + if (this.hasDecorationProperties(styling)) { + const beforeCount = this.styleLibrary.getAllStyles().length; + styleRefs.decoration = this.styleLibrary.addStyle('decoration', { + fills: styling.fills, + cornerRadius: styling.cornerRadius, + effects: styling.effects + }); + if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) { + newStyles.add(styleRefs.decoration); + } + } + + // Process padding styles + if (layout.padding) { + const beforeCount = this.styleLibrary.getAllStyles().length; + styleRefs.padding = this.styleLibrary.addStyle('padding', { padding: layout.padding }); + if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) { + newStyles.add(styleRefs.padding); + } + } + + // Process children with deduplication + const children = await this.analyzeChildren(node); + const nestedComponents = this.extractNestedComponents(node); + + const result: DeduplicatedComponentAnalysis = { + metadata, + styleRefs, + children, + nestedComponents + }; + + if (trackNewStyles && newStyles.size > 0) { + result.newStyleDefinitions = this.getStyleDefinitions(Array.from(newStyles)); + } + + return result; + } + + private async analyzeChildren(node: FigmaNode): Promise { + if (!node.children) return []; + + const children: DeduplicatedComponentChild[] = []; + + for (const child of node.children) { + if (!child.visible) continue; + + const childStyleRefs: string[] = []; + + // Extract child styling + const childStyling = extractStylingInfo(child); + if (this.hasDecorationProperties(childStyling)) { + const decorationRef = this.styleLibrary.addStyle('decoration', { + fills: childStyling.fills, + cornerRadius: childStyling.cornerRadius, + effects: childStyling.effects + }); + childStyleRefs.push(decorationRef); + } + + // Extract text styling for text nodes + let textContent: string | undefined; + if (child.type === 'TEXT') { + const textInfo = extractTextInfo(child); + if (textInfo) { + textContent = textInfo.content; + + // Add text style to library + if (child.style) { + const textStyleRef = this.styleLibrary.addStyle('text', { + fontFamily: child.style.fontFamily, + fontSize: child.style.fontSize, + fontWeight: child.style.fontWeight + }); + childStyleRefs.push(textStyleRef); + } + } + } + + children.push({ + nodeId: child.id, + name: child.name, + type: child.type, + styleRefs: childStyleRefs, + semanticType: this.detectSemanticType(child), + textContent + }); + } + + return children; + } + + private hasDecorationProperties(styling: StylingInfo): boolean { + return !!(styling.fills?.length || styling.cornerRadius !== undefined || styling.effects?.dropShadows?.length); + } + + private extractTextContent(node: any): string { + return node.characters || node.name || ''; + } + + private detectSemanticType(node: any): string | undefined { + // Simplified semantic detection + if (node.type === 'TEXT') { + const content = this.extractTextContent(node).toLowerCase(); + if (['click', 'submit', 'save', 'cancel'].some(word => content.includes(word))) { + return 'button'; + } + return 'text'; + } + return undefined; + } + + private extractNestedComponents(node: FigmaNode): NestedComponentInfo[] { + if (!node.children) return []; + + const nestedComponents: NestedComponentInfo[] = []; + + for (const child of node.children) { + if (child.type === 'COMPONENT' || child.type === 'INSTANCE' || child.type === 'COMPONENT_SET') { + nestedComponents.push(createNestedComponentInfo(child)); + } + } + + return nestedComponents; + } + + private getStyleDefinitions(styleIds: string[]): Record { + const definitions: Record = {}; + styleIds.forEach(id => { + const definition = this.styleLibrary.getStyle(id); + if (definition) { + definitions[id] = definition; + } + }); + return definitions; + } +} diff --git a/src/extractors/components/index.mts b/src/extractors/components/index.mts index 567c29c..3312614 100644 --- a/src/extractors/components/index.mts +++ b/src/extractors/components/index.mts @@ -31,6 +31,13 @@ export { export {VariantAnalyzer} from './variant-analyzer.mjs'; +// Deduplicated extractor +export { + DeduplicatedComponentExtractor, + type DeduplicatedComponentAnalysis, + type DeduplicatedComponentChild +} from './deduplicated-extractor.mjs'; + // Types export type { ComponentAnalysis, diff --git a/src/extractors/flutter/index.mts b/src/extractors/flutter/index.mts new file mode 100644 index 0000000..bda724e --- /dev/null +++ b/src/extractors/flutter/index.mts @@ -0,0 +1,7 @@ +// src/extractors/flutter/index.mts + +export { + FlutterStyleLibrary, + FlutterCodeGenerator, + type FlutterStyleDefinition +} from './style-library.mjs'; diff --git a/src/extractors/flutter/style-library.mts b/src/extractors/flutter/style-library.mts new file mode 100644 index 0000000..b8b906f --- /dev/null +++ b/src/extractors/flutter/style-library.mts @@ -0,0 +1,161 @@ +// src/extractors/flutter/style-library.mts + +export interface FlutterStyleDefinition { + id: string; + category: 'decoration' | 'text' | 'layout' | 'padding'; + properties: Record; + flutterCode: string; + hash: string; + usageCount: number; +} + +export class FlutterStyleLibrary { + private static instance: FlutterStyleLibrary; + private styles = new Map(); + private hashToId = new Map(); + + static getInstance(): FlutterStyleLibrary { + if (!this.instance) { + this.instance = new FlutterStyleLibrary(); + } + return this.instance; + } + + addStyle(category: string, properties: any): string { + const hash = this.generateHash(properties); + + if (this.hashToId.has(hash)) { + const existingId = this.hashToId.get(hash)!; + const style = this.styles.get(existingId)!; + style.usageCount++; + return existingId; + } + + const generatedId = this.generateId(); + const styleId = `${category}${generatedId.charAt(0).toUpperCase()}${generatedId.slice(1)}`; + const definition: FlutterStyleDefinition = { + id: styleId, + category: category as any, + properties, + flutterCode: this.generateFlutterCode(category, properties), + hash, + usageCount: 1 + }; + + this.styles.set(styleId, definition); + this.hashToId.set(hash, styleId); + return styleId; + } + + getStyle(id: string): FlutterStyleDefinition | undefined { + return this.styles.get(id); + } + + getAllStyles(): FlutterStyleDefinition[] { + return Array.from(this.styles.values()); + } + + reset(): void { + this.styles.clear(); + this.hashToId.clear(); + } + + private generateHash(properties: any): string { + return JSON.stringify(properties, Object.keys(properties).sort()); + } + + private generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2, 4); + } + + private generateFlutterCode(category: string, properties: any): string { + switch (category) { + case 'decoration': + return FlutterCodeGenerator.generateDecoration(properties); + case 'text': + return FlutterCodeGenerator.generateTextStyle(properties); + case 'padding': + return FlutterCodeGenerator.generatePadding(properties); + case 'layout': + // Layout code generation can be added later + return `// ${category} implementation`; + default: + return `// ${category} implementation`; + } + } +} + +export class FlutterCodeGenerator { + static generateDecoration(properties: any): string { + let code = 'BoxDecoration(\n'; + + if (properties.fills?.length > 0) { + const fill = properties.fills[0]; + if (fill.hex) { + code += ` color: Color(0xFF${fill.hex.substring(1)}),\n`; + } + } + + if (properties.cornerRadius !== undefined) { + if (typeof properties.cornerRadius === 'number') { + code += ` borderRadius: BorderRadius.circular(${properties.cornerRadius}),\n`; + } else { + const r = properties.cornerRadius; + code += ` borderRadius: BorderRadius.only(\n`; + code += ` topLeft: Radius.circular(${r.topLeft}),\n`; + code += ` topRight: Radius.circular(${r.topRight}),\n`; + code += ` bottomLeft: Radius.circular(${r.bottomLeft}),\n`; + code += ` bottomRight: Radius.circular(${r.bottomRight}),\n`; + code += ` ),\n`; + } + } + + if (properties.effects?.dropShadows?.length > 0) { + code += ` boxShadow: [\n`; + properties.effects.dropShadows.forEach((shadow: any) => { + code += ` BoxShadow(\n`; + code += ` color: Color(0xFF${shadow.hex.substring(1)}).withOpacity(${shadow.opacity}),\n`; + code += ` offset: Offset(${shadow.offset.x}, ${shadow.offset.y}),\n`; + code += ` blurRadius: ${shadow.radius},\n`; + if (shadow.spread) { + code += ` spreadRadius: ${shadow.spread},\n`; + } + code += ` ),\n`; + }); + code += ` ],\n`; + } + + code += ')'; + return code; + } + + static generatePadding(properties: any): string { + const p = properties.padding; + if (!p) return 'EdgeInsets.zero'; + + if (p.isUniform) { + return `EdgeInsets.all(${p.top})`; + } + return `EdgeInsets.fromLTRB(${p.left}, ${p.top}, ${p.right}, ${p.bottom})`; + } + + static generateTextStyle(properties: any): string { + const parts: string[] = []; + + if (properties.fontFamily) { + parts.push(`fontFamily: '${properties.fontFamily}'`); + } + if (properties.fontSize) { + parts.push(`fontSize: ${properties.fontSize}`); + } + if (properties.fontWeight && properties.fontWeight !== 400) { + const weight = properties.fontWeight >= 700 ? 'FontWeight.bold' : + properties.fontWeight >= 600 ? 'FontWeight.w600' : + properties.fontWeight >= 500 ? 'FontWeight.w500' : + 'FontWeight.normal'; + parts.push(`fontWeight: ${weight}`); + } + + return `TextStyle(${parts.join(', ')})`; + } +} diff --git a/src/tools/flutter/components/component-tool.mts b/src/tools/flutter/components/component-tool.mts index 3dbcbec..b87fbd7 100644 --- a/src/tools/flutter/components/component-tool.mts +++ b/src/tools/flutter/components/component-tool.mts @@ -8,14 +8,23 @@ import { VariantAnalyzer, parseComponentInput, type ComponentAnalysis, - type ComponentVariant + type ComponentVariant, + DeduplicatedComponentExtractor, + type DeduplicatedComponentAnalysis } from "../../../extractors/components/index.mjs"; +import {FlutterStyleLibrary} from "../../../extractors/flutter/style-library.mjs"; import { generateVariantSelectionPrompt, generateComponentAnalysisReport, generateStructureInspectionReport } from "./helpers.mjs"; +import { + generateDeduplicatedReport, + generateFlutterImplementation, + generateComprehensiveDeduplicatedReport, + generateStyleLibraryReport +} from "./deduplicated-helpers.mjs"; import { createAssetsDirectory, @@ -45,10 +54,13 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { includeVariants: z.boolean().optional().describe("Include variant analysis for component sets (default: true)"), variantSelection: z.array(z.string()).optional().describe("Specific variant names to analyze (if >3 variants)"), projectPath: z.string().optional().describe("Path to Flutter project for asset export (defaults to current directory)"), - exportAssets: z.boolean().optional().describe("Automatically export image assets found in component (default: true)") + exportAssets: z.boolean().optional().describe("Automatically export image assets found in component (default: true)"), + useDeduplication: z.boolean().optional().describe("Use style deduplication for token efficiency (default: true)"), + generateFlutterCode: z.boolean().optional().describe("Generate full Flutter implementation code (default: false)"), + resetStyleLibrary: z.boolean().optional().describe("Reset style library before analysis (default: false)") } }, - async ({input, nodeId, userDefinedComponent = false, maxChildNodes = 10, includeVariants = true, variantSelection, projectPath = process.cwd(), exportAssets = true}) => { + async ({input, nodeId, userDefinedComponent = false, maxChildNodes = 10, includeVariants = true, variantSelection, projectPath = process.cwd(), exportAssets = true, useDeduplication = true, generateFlutterCode = false, resetStyleLibrary = false}) => { const token = figmaApiKey; if (!token) { return { @@ -60,6 +72,11 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { } try { + // Reset style library if requested + if (resetStyleLibrary) { + FlutterStyleLibrary.getInstance().reset(); + } + // Parse input to get file ID and node ID const parsedInput = parseComponentInput(input, nodeId); @@ -73,11 +90,6 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { } const figmaService = new FigmaService(token); - const componentExtractor = new ComponentExtractor({ - maxChildNodes, - extractTextContent: true, - prioritizeComponents: true - }); // Get the component node const componentNode = await figmaService.getNode(parsedInput.fileId, parsedInput.nodeId); @@ -155,21 +167,64 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { } // Analyze the main component - let componentAnalysis: ComponentAnalysis; - - if (componentNode.type === 'COMPONENT_SET') { - // For component sets, analyze the default variant or first selected variant - const targetVariant = selectedVariants.find(v => v.isDefault) || selectedVariants[0]; - if (targetVariant) { - const variantNode = await figmaService.getNode(parsedInput.fileId, targetVariant.nodeId); - componentAnalysis = await componentExtractor.analyzeComponent(variantNode, userDefinedComponent); + let analysisReport: string; + + if (useDeduplication) { + // Use deduplicated extractor + const deduplicatedExtractor = new DeduplicatedComponentExtractor(); + let deduplicatedAnalysis: DeduplicatedComponentAnalysis; + + if (componentNode.type === 'COMPONENT_SET') { + // For component sets, analyze the default variant or first selected variant + const targetVariant = selectedVariants.find(v => v.isDefault) || selectedVariants[0]; + if (targetVariant) { + const variantNode = await figmaService.getNode(parsedInput.fileId, targetVariant.nodeId); + deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(variantNode, true); + } else { + // Fallback to analyzing the component set itself + deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(componentNode, true); + } } else { - // Fallback to analyzing the component set itself - componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent); + // Regular component, instance, or user-defined frame + deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(componentNode, true); + } + + analysisReport = generateComprehensiveDeduplicatedReport(deduplicatedAnalysis, true); + + if (generateFlutterCode) { + analysisReport += "\n\n" + generateFlutterImplementation(deduplicatedAnalysis); } } else { - // Regular component, instance, or user-defined frame - componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent); + // Use original extractor + const componentExtractor = new ComponentExtractor({ + maxChildNodes, + extractTextContent: true, + prioritizeComponents: true + }); + + let componentAnalysis: ComponentAnalysis; + + if (componentNode.type === 'COMPONENT_SET') { + // For component sets, analyze the default variant or first selected variant + const targetVariant = selectedVariants.find(v => v.isDefault) || selectedVariants[0]; + if (targetVariant) { + const variantNode = await figmaService.getNode(parsedInput.fileId, targetVariant.nodeId); + componentAnalysis = await componentExtractor.analyzeComponent(variantNode, userDefinedComponent); + } else { + // Fallback to analyzing the component set itself + componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent); + } + } else { + // Regular component, instance, or user-defined frame + componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent); + } + + analysisReport = generateComponentAnalysisReport( + componentAnalysis, + variantAnalysis, + selectedVariants, + parsedInput + ); } // Detect and export image assets if enabled @@ -192,14 +247,6 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { } } - // Generate analysis report - const analysisReport = generateComponentAnalysisReport( - componentAnalysis, - variantAnalysis, - selectedVariants, - parsedInput - ); - return { content: [{ type: "text", @@ -389,6 +436,103 @@ export function registerComponentTools(server: McpServer, figmaApiKey: string) { } } ); + + // Dedicated Flutter code generation tool + server.registerTool( + "generate_flutter_implementation", + { + title: "Generate Flutter Implementation", + description: "Generate complete Flutter widget code using cached style definitions", + inputSchema: { + componentNodeId: z.string().describe("Node ID of the analyzed component"), + includeStyleDefinitions: z.boolean().optional().describe("Include style definitions in output (default: true)"), + widgetName: z.string().optional().describe("Custom widget class name") + } + }, + async ({ componentNodeId, includeStyleDefinitions = true, widgetName }) => { + try { + const styleLibrary = FlutterStyleLibrary.getInstance(); + const styles = styleLibrary.getAllStyles(); + + let output = "๐Ÿ—๏ธ Flutter Implementation\n"; + output += `${'='.repeat(50)}\n\n`; + + if (includeStyleDefinitions && styles.length > 0) { + output += "๐Ÿ“‹ Style Definitions:\n"; + output += `${'โ”€'.repeat(30)}\n`; + styles.forEach(style => { + output += `// ${style.id} (${style.category}, used ${style.usageCount} times)\n`; + output += `final ${style.id} = ${style.flutterCode};\n\n`; + }); + output += "\n"; + } else if (styles.length === 0) { + output += "โš ๏ธ No cached styles found. Please analyze a component first.\n\n"; + } + + output += generateWidgetClass(componentNodeId, widgetName || 'CustomWidget', styles); + + // Add usage summary + if (styles.length > 0) { + output += "\n\n๐Ÿ“Š Style Library Summary:\n"; + output += `${'โ”€'.repeat(30)}\n`; + output += `โ€ข Total unique styles: ${styles.length}\n`; + + const categoryStats = styles.reduce((acc, style) => { + acc[style.category] = (acc[style.category] || 0) + 1; + return acc; + }, {} as Record); + + Object.entries(categoryStats).forEach(([category, count]) => { + output += `โ€ข ${category}: ${count} style(s)\n`; + }); + + const totalUsage = styles.reduce((sum, style) => sum + style.usageCount, 0); + output += `โ€ข Total style usage: ${totalUsage}\n`; + const efficiency = styles.length > 0 ? ((totalUsage - styles.length) / totalUsage * 100).toFixed(1) : '0.0'; + output += `โ€ข Deduplication efficiency: ${efficiency}% reduction\n`; + } + + return { + content: [{ type: "text", text: output }] + }; + + } catch (error) { + return { + content: [{ + type: "text", + text: `Error generating Flutter implementation: ${error instanceof Error ? error.message : String(error)}` + }] + }; + } + } + ); + + // Style library status tool + server.registerTool( + "style_library_status", + { + title: "Style Library Status", + description: "Get comprehensive status report of the cached style library", + inputSchema: {} + }, + async () => { + try { + const report = generateStyleLibraryReport(); + + return { + content: [{ type: "text", text: report }] + }; + + } catch (error) { + return { + content: [{ + type: "text", + text: `Error generating style library report: ${error instanceof Error ? error.message : String(error)}` + }] + }; + } + } + ); } /** @@ -562,10 +706,12 @@ function generateAssetExportReport(exportedAssets: AssetInfo[]): string { exportedAssets.forEach(asset => { const constantName = asset.filename .replace(/\.[^/.]+$/, '') // Remove extension - .replace(/[^a-zA-Z0-9]/g, '_') // Replace special chars with underscore - .replace(/_+/g, '_') // Replace multiple underscores with single - .replace(/^_|_$/g, '') // Remove leading/trailing underscores - .toLowerCase(); + .replace(/[^a-zA-Z0-9]/g, ' ') // Replace special chars with space + .replace(/\s+/g, ' ') // Replace multiple spaces with single + .trim() + .split(' ') + .map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); report += ` Image.asset(Assets.${constantName}) // ${asset.nodeName}\n`; }); @@ -574,3 +720,90 @@ function generateAssetExportReport(exportedAssets: AssetInfo[]): string { return report; } + + +/** + * Generate widget class implementation + */ +function generateWidgetClass(componentNodeId: string, widgetName: string, styles: Array): string { + let output = `๐ŸŽฏ Widget Implementation:\n`; + output += `${'โ”€'.repeat(30)}\n`; + output += `class ${widgetName} extends StatelessWidget {\n`; + output += ` const ${widgetName}({Key? key}) : super(key: key);\n\n`; + output += ` @override\n`; + output += ` Widget build(BuildContext context) {\n`; + + // Find relevant styles for this component + const decorationStyles = styles.filter(s => s.category === 'decoration'); + const paddingStyles = styles.filter(s => s.category === 'padding'); + const textStyles = styles.filter(s => s.category === 'text'); + + if (decorationStyles.length > 0 || paddingStyles.length > 0) { + output += ` return Container(\n`; + + // Add decoration if available + if (decorationStyles.length > 0) { + const decorationStyle = decorationStyles[0]; // Use first decoration style + output += ` decoration: ${decorationStyle.id},\n`; + } + + // Add padding if available + if (paddingStyles.length > 0) { + const paddingStyle = paddingStyles[0]; // Use first padding style + output += ` padding: ${paddingStyle.id},\n`; + } + + // Add child content + if (textStyles.length > 0) { + const textStyle = textStyles[0]; // Use first text style + output += ` child: Text(\n`; + output += ` 'Sample Text', // TODO: Replace with actual content\n`; + output += ` style: ${textStyle.id},\n`; + output += ` ),\n`; + } else { + output += ` child: Column(\n`; + output += ` children: [\n`; + output += ` // TODO: Add your widget content here\n`; + output += ` Text('Component Content'),\n`; + output += ` ],\n`; + output += ` ),\n`; + } + + output += ` );\n`; + } else if (textStyles.length > 0) { + // Just a text widget if only text styles are available + const textStyle = textStyles[0]; + output += ` return Text(\n`; + output += ` 'Sample Text', // TODO: Replace with actual content\n`; + output += ` style: ${textStyle.id},\n`; + output += ` );\n`; + } else { + // Fallback for when no cached styles are available + output += ` return Container(\n`; + output += ` // TODO: Implement widget using component node ID: ${componentNodeId}\n`; + output += ` // No cached styles found - please analyze a component first\n`; + output += ` child: Text('Widget Placeholder'),\n`; + output += ` );\n`; + } + + output += ` }\n`; + output += `}\n`; + + // Add usage instructions + output += `\n๐Ÿ’ก Usage Instructions:\n`; + output += `${'โ”€'.repeat(30)}\n`; + output += `1. Import this widget in your Flutter app\n`; + output += `2. Replace 'Sample Text' with actual content\n`; + output += `3. Customize the widget structure as needed\n`; + output += `4. Add any missing properties or methods\n\n`; + + if (styles.length > 0) { + output += `๐Ÿ“ฆ Available Style References:\n`; + output += `${'โ”€'.repeat(30)}\n`; + styles.forEach(style => { + output += `โ€ข ${style.id} (${style.category})\n`; + }); + } + + return output; +} diff --git a/src/tools/flutter/components/deduplicated-helpers.mts b/src/tools/flutter/components/deduplicated-helpers.mts new file mode 100644 index 0000000..a06942c --- /dev/null +++ b/src/tools/flutter/components/deduplicated-helpers.mts @@ -0,0 +1,295 @@ +// src/tools/flutter/components/deduplicated-helpers.mts + +import type { DeduplicatedComponentAnalysis } from '../../../extractors/components/deduplicated-extractor.mjs'; +import { FlutterStyleLibrary } from '../../../extractors/flutter/style-library.mjs'; + +export function generateDeduplicatedReport(analysis: DeduplicatedComponentAnalysis): string { + let output = `Component Analysis (Deduplicated)\n\n`; + + output += `Component: ${analysis.metadata.name}\n`; + output += `Type: ${analysis.metadata.type}\n`; + output += `Node ID: ${analysis.metadata.nodeId}\n\n`; + + // Style references + if (Object.keys(analysis.styleRefs).length > 0) { + output += `Style References:\n`; + Object.entries(analysis.styleRefs).forEach(([category, styleId]) => { + output += `- ${category}: ${styleId}\n`; + }); + output += `\n`; + } + + // Children with their style references + if (analysis.children.length > 0) { + output += `Children (${analysis.children.length}):\n`; + analysis.children.forEach((child, index) => { + const semanticMark = child.semanticType ? ` [${child.semanticType.toUpperCase()}]` : ''; + output += `${index + 1}. ${child.name} (${child.type})${semanticMark}\n`; + + if (child.textContent) { + output += ` Text: "${child.textContent}"\n`; + } + + if (child.styleRefs.length > 0) { + output += ` Styles: ${child.styleRefs.join(', ')}\n`; + } + }); + output += `\n`; + } + + // New style definitions (only show if this analysis created new styles) + if (analysis.newStyleDefinitions && Object.keys(analysis.newStyleDefinitions).length > 0) { + output += `New Style Definitions:\n`; + Object.entries(analysis.newStyleDefinitions).forEach(([id, definition]) => { + output += `${id}: ${definition.category} style\n`; + }); + output += `\nUse generate_flutter_implementation tool for complete Flutter code.\n`; + } + + return output; +} + +export function generateFlutterImplementation(analysis: DeduplicatedComponentAnalysis): string { + const styleLibrary = FlutterStyleLibrary.getInstance(); + let implementation = `Flutter Implementation:\n\n`; + + // Widget structure + const widgetName = toPascalCase(analysis.metadata.name); + implementation += `class ${widgetName} extends StatelessWidget {\n`; + implementation += ` const ${widgetName}({Key? key}) : super(key: key);\n\n`; + implementation += ` @override\n`; + implementation += ` Widget build(BuildContext context) {\n`; + implementation += ` return Container(\n`; + + // Apply decoration if exists + if (analysis.styleRefs.decoration) { + implementation += ` decoration: ${analysis.styleRefs.decoration},\n`; + } + + // Apply padding if exists + if (analysis.styleRefs.padding) { + implementation += ` padding: ${analysis.styleRefs.padding},\n`; + } + + // Add child widget structure + if (analysis.children.length > 0) { + implementation += ` child: Column(\n`; + implementation += ` children: [\n`; + + analysis.children.forEach(child => { + if (child.semanticType === 'button' && child.textContent) { + implementation += ` ElevatedButton(\n`; + implementation += ` onPressed: () {},\n`; + implementation += ` child: Text('${child.textContent}'),\n`; + implementation += ` ),\n`; + } else if (child.type === 'TEXT' && child.textContent) { + implementation += ` Text('${child.textContent}'),\n`; + } + }); + + implementation += ` ],\n`; + implementation += ` ),\n`; + } + + implementation += ` );\n`; + implementation += ` }\n`; + implementation += `}\n`; + + return implementation; +} + +/** + * Generate comprehensive deduplicated report with style library statistics + */ +export function generateComprehensiveDeduplicatedReport( + analysis: DeduplicatedComponentAnalysis, + includeStyleStats: boolean = true +): string { + let output = `๐Ÿ“Š Comprehensive Component Analysis (Deduplicated)\n`; + output += `${'='.repeat(60)}\n\n`; + + // Component metadata + output += `๐Ÿท๏ธ Component Metadata:\n`; + output += ` โ€ข Name: ${analysis.metadata.name}\n`; + output += ` โ€ข Type: ${analysis.metadata.type}\n`; + output += ` โ€ข Node ID: ${analysis.metadata.nodeId}\n`; + if (analysis.metadata.componentKey) { + output += ` โ€ข Component Key: ${analysis.metadata.componentKey}\n`; + } + output += `\n`; + + // Style references with usage information + if (Object.keys(analysis.styleRefs).length > 0) { + output += `๐ŸŽจ Style References (Deduplicated):\n`; + const styleLibrary = FlutterStyleLibrary.getInstance(); + + Object.entries(analysis.styleRefs).forEach(([category, styleId]) => { + const style = styleLibrary.getStyle(styleId); + if (style) { + output += ` โ€ข ${category}: ${styleId} (used ${style.usageCount} times)\n`; + } else { + output += ` โ€ข ${category}: ${styleId} (style not found)\n`; + } + }); + output += `\n`; + } else { + output += `๐ŸŽจ Style References: None detected\n\n`; + } + + // Children analysis with enhanced details + if (analysis.children.length > 0) { + output += `๐Ÿ‘ถ Children Analysis (${analysis.children.length} children):\n`; + analysis.children.forEach((child, index) => { + const semanticMark = child.semanticType ? ` [${child.semanticType.toUpperCase()}]` : ''; + output += ` ${index + 1}. ${child.name} (${child.type})${semanticMark}\n`; + + if (child.textContent) { + output += ` ๐Ÿ“ Text: "${child.textContent}"\n`; + } + + if (child.styleRefs.length > 0) { + output += ` ๐ŸŽจ Style refs: ${child.styleRefs.join(', ')}\n`; + } + + if (child.semanticType) { + output += ` ๐Ÿท๏ธ Semantic type: ${child.semanticType}\n`; + } + }); + output += `\n`; + } else { + output += `๐Ÿ‘ถ Children: No children detected\n\n`; + } + + // Nested components + if (analysis.nestedComponents.length > 0) { + output += `๐Ÿ”— Nested Components (${analysis.nestedComponents.length}):\n`; + analysis.nestedComponents.forEach((nested, index) => { + output += ` ${index + 1}. ${nested.name}\n`; + output += ` ๐Ÿ†” Node ID: ${nested.nodeId}\n`; + if (nested.componentKey) { + output += ` ๐Ÿ”‘ Component Key: ${nested.componentKey}\n`; + } + output += ` ๐Ÿ”ง Needs separate analysis: ${nested.needsSeparateAnalysis ? 'Yes' : 'No'}\n`; + }); + output += `\n`; + } + + // New style definitions created in this analysis + if (analysis.newStyleDefinitions && Object.keys(analysis.newStyleDefinitions).length > 0) { + output += `โœจ New Style Definitions Created:\n`; + Object.entries(analysis.newStyleDefinitions).forEach(([id, definition]) => { + output += ` โ€ข ${id} (${definition.category})\n`; + output += ` Usage count: ${definition.usageCount}\n`; + output += ` Properties: ${Object.keys(definition.properties).join(', ')}\n`; + }); + output += `\n`; + } + + // Style library statistics (if requested) + if (includeStyleStats) { + const styleLibrary = FlutterStyleLibrary.getInstance(); + const allStyles = styleLibrary.getAllStyles(); + + output += `๐Ÿ“š Style Library Summary:\n`; + output += ` โ€ข Total unique styles: ${allStyles.length}\n`; + + if (allStyles.length > 0) { + const categoryStats = allStyles.reduce((acc, style) => { + acc[style.category] = (acc[style.category] || 0) + 1; + return acc; + }, {} as Record); + + Object.entries(categoryStats).forEach(([category, count]) => { + output += ` โ€ข ${category}: ${count} style(s)\n`; + }); + + const totalUsage = allStyles.reduce((sum, style) => sum + style.usageCount, 0); + output += ` โ€ข Total style usage: ${totalUsage}\n`; + + if (totalUsage > allStyles.length) { + const efficiency = ((totalUsage - allStyles.length) / totalUsage * 100).toFixed(1); + output += ` โ€ข Deduplication efficiency: ${efficiency}% reduction\n`; + } + } + output += `\n`; + } + + // Quick actions + output += `๐Ÿš€ Quick Actions:\n`; + output += ` โ€ข Use 'generate_flutter_implementation' tool for complete Flutter code\n`; + output += ` โ€ข Use 'analyze_figma_component' with different components to build style library\n`; + output += ` โ€ข Use 'resetStyleLibrary: true' to start fresh analysis\n`; + + return output; +} + +/** + * Generate style library status report + */ +export function generateStyleLibraryReport(): string { + const styleLibrary = FlutterStyleLibrary.getInstance(); + const allStyles = styleLibrary.getAllStyles(); + + let output = `๐Ÿ“š Style Library Status Report\n`; + output += `${'='.repeat(40)}\n\n`; + + if (allStyles.length === 0) { + output += `โš ๏ธ Style library is empty.\n`; + output += ` โ€ข Analyze components with 'useDeduplication: true' to populate\n`; + output += ` โ€ข Use 'analyze_figma_component' tool to start building your style library\n`; + return output; + } + + output += `๐Ÿ“Š Library Statistics:\n`; + output += ` โ€ข Total unique styles: ${allStyles.length}\n`; + + // Category breakdown + const categoryStats = allStyles.reduce((acc, style) => { + acc[style.category] = (acc[style.category] || 0) + 1; + return acc; + }, {} as Record); + + output += ` โ€ข Style categories:\n`; + Object.entries(categoryStats).forEach(([category, count]) => { + output += ` - ${category}: ${count} style(s)\n`; + }); + + // Usage statistics + const totalUsage = allStyles.reduce((sum, style) => sum + style.usageCount, 0); + output += ` โ€ข Total style usage: ${totalUsage}\n`; + + if (totalUsage > allStyles.length) { + const efficiency = ((totalUsage - allStyles.length) / totalUsage * 100).toFixed(1); + output += ` โ€ข Deduplication efficiency: ${efficiency}% reduction\n`; + } + + // Most used styles + const sortedByUsage = [...allStyles].sort((a, b) => b.usageCount - a.usageCount); + const topStyles = sortedByUsage.slice(0, 5); + + if (topStyles.length > 0) { + output += `\n๐Ÿ”ฅ Most Used Styles:\n`; + topStyles.forEach((style, index) => { + output += ` ${index + 1}. ${style.id} (${style.category}) - used ${style.usageCount} times\n`; + }); + } + + // Detailed style list + output += `\n๐Ÿ“‹ All Styles:\n`; + Object.entries(categoryStats).forEach(([category, count]) => { + const categoryStyles = allStyles.filter(s => s.category === category); + output += `\n ${category.toUpperCase()} (${count}):\n`; + categoryStyles.forEach(style => { + output += ` โ€ข ${style.id} (used ${style.usageCount} times)\n`; + }); + }); + + return output; +} + +function toPascalCase(str: string): string { + return str + .replace(/[^a-zA-Z0-9]/g, ' ') + .replace(/\w+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .replace(/\s/g, ''); +}