Skip to main content
Glama

Motion.dev MCP Server

transformer.ts14.3 kB
/** * AST Transformer for Motion.dev code generation * Handles transformation and manipulation of AST nodes for different frameworks */ import * as babel from '@babel/core'; import * as t from '@babel/types'; import { Framework } from '../../types/motion.js'; import { logger } from '../../utils/logger.js'; import { ComponentAST, TransformationRule, CodeGenerationContext, MotionElement, AnimationSequence } from './types.js'; export class ASTTransformer { private transformationRules: TransformationRule[] = []; constructor() { this.initializeTransformationRules(); } transform(componentAST: ComponentAST, context: CodeGenerationContext): ComponentAST { logger.debug(`Transforming ${context.framework} component: ${context.componentName}`); try { let transformedAST = { ...componentAST }; // Apply transformation rules for (const rule of this.transformationRules) { if (this.shouldApplyRule(rule, context)) { transformedAST = this.applyTransformationRule(transformedAST, rule, context); } } // Framework-specific transformations transformedAST = this.applyFrameworkSpecificTransforms(transformedAST, context); logger.debug(`Successfully transformed ${context.framework} component`); return transformedAST; } catch (error) { logger.error('AST transformation failed', error as Error); throw error; } } private initializeTransformationRules(): void { this.transformationRules = [ // React-specific transformations { name: 'react-motion-imports', description: 'Add framer-motion imports for React components', framework: 'react', condition: (node, context) => context.framework === 'react' && this.hasMotionElements(node), transform: (node, context) => this.addFramerMotionImports(node, context) }, // JavaScript-specific transformations { name: 'js-motion-imports', description: 'Add Motion One imports for vanilla JavaScript', framework: 'js', condition: (node, context) => context.framework === 'js' && this.hasAnimationCalls(node), transform: (node, context) => this.addMotionOneImports(node, context) }, // Vue-specific transformations { name: 'vue-motion-imports', description: 'Add @vueuse/motion imports for Vue components', framework: 'vue', condition: (node, context) => context.framework === 'vue' && this.hasVueMotionDirectives(node), transform: (node, context) => this.addVueMotionImports(node, context) }, // Accessibility transformations { name: 'accessibility-enhancements', description: 'Add accessibility attributes and reduced motion support', condition: (_node, context) => context.optimization?.accessibility === true, transform: (_node, context) => this.addAccessibilityEnhancements(_node, context) }, // Performance optimizations { name: 'performance-optimizations', description: 'Apply performance optimizations to motion components', condition: (_node, context) => context.optimization?.performance === true, transform: (_node, context) => this.applyPerformanceOptimizations(_node, context) } ]; } private shouldApplyRule(rule: TransformationRule, context: CodeGenerationContext): boolean { // Check framework compatibility if (rule.framework) { const frameworks = Array.isArray(rule.framework) ? rule.framework : [rule.framework]; if (!frameworks.includes(context.framework)) { return false; } } return true; } private applyTransformationRule( componentAST: ComponentAST, rule: TransformationRule, context: CodeGenerationContext ): ComponentAST { const result = babel.transformSync('', { ast: true, plugins: [ () => ({ visitor: { Program: (path: any) => { if (rule.condition(path.node, context)) { const transformed = rule.transform(path.node, context); if (Array.isArray(transformed)) { // Replace with multiple nodes path.replaceWithMultiple(transformed as any[]); } else if (transformed !== path.node) { // Replace with single node path.replaceWith(transformed as any); } } } } }) ] }); return { ...componentAST, ast: result?.ast as any }; } private applyFrameworkSpecificTransforms( componentAST: ComponentAST, context: CodeGenerationContext ): ComponentAST { switch (context.framework) { case 'react': return this.applyReactTransforms(componentAST, context); case 'vue': return this.applyVueTransforms(componentAST, context); case 'js': return this.applyJavaScriptTransforms(componentAST, context); default: return componentAST; } } private applyReactTransforms( componentAST: ComponentAST, context: CodeGenerationContext ): ComponentAST { // Add React-specific optimizations if (context.optimization?.performance) { // Add React.memo, useCallback, useMemo where appropriate componentAST = this.addReactPerformanceOptimizations(componentAST, context); } return componentAST; } private applyVueTransforms( componentAST: ComponentAST, _context: CodeGenerationContext ): ComponentAST { // Add Vue-specific optimizations return componentAST; } private applyJavaScriptTransforms( componentAST: ComponentAST, _context: CodeGenerationContext ): ComponentAST { // Add JavaScript-specific optimizations return componentAST; } // Motion detection methods private hasMotionElements(node: any): boolean { let hasMotion = false; babel.traverse(node, { JSXElement(path) { const openingElement = path.node.openingElement; if (t.isJSXMemberExpression(openingElement.name) && t.isIdentifier(openingElement.name.object) && (openingElement.name.object as any).name === 'motion') { hasMotion = true; path.stop(); } } }); return hasMotion; } private hasAnimationCalls(node: any): boolean { let hasAnimation = false; babel.traverse(node, { CallExpression(path) { if (t.isIdentifier(path.node.callee) && ['animate', 'spring', 'timeline', 'scroll'].includes(path.node.callee.name)) { hasAnimation = true; path.stop(); } } }); return hasAnimation; } private hasVueMotionDirectives(_node: any): boolean { // Vue motion detection would require Vue SFC parsing // For now, return true if context suggests Vue motion usage return true; } // Import transformation methods private addFramerMotionImports(node: any, context: CodeGenerationContext): any { const imports = new Set<string>(); // Detect which framer-motion features are used babel.traverse(node, { JSXElement(path) { const openingElement = path.node.openingElement; if (t.isJSXMemberExpression(openingElement.name) && t.isIdentifier(openingElement.name.object) && (openingElement.name.object as any).name === 'motion') { imports.add('motion'); } }, JSXAttribute(path) { if (t.isIdentifier(path.node.name)) { const attrName = (path.node.name as any).name; if (attrName === 'initial' || attrName === 'animate' || attrName === 'exit') { imports.add('motion'); } if (attrName === 'variants') { imports.add('Variants'); } } }, Identifier(path) { const name = path.node.name; if (['AnimatePresence', 'useAnimation', 'useSpring', 'useTransform'].includes(name)) { imports.add(name); } } }); // Add import statement at the top const importSpecifiers = Array.from(imports).map(name => name === 'motion' ? t.importSpecifier(t.identifier('motion'), t.identifier('motion')) : t.importSpecifier(t.identifier(name), t.identifier(name)) ); const importDeclaration = t.importDeclaration( importSpecifiers, t.stringLiteral('framer-motion') ); // Insert at the beginning of the program if (t.isProgram(node)) { node.body.unshift(importDeclaration); } context.imports.add('framer-motion'); return node; } private addMotionOneImports(node: any, context: CodeGenerationContext): any { const imports = new Set<string>(); babel.traverse(node, { CallExpression(path) { if (t.isIdentifier(path.node.callee)) { const name = path.node.callee.name; if (['animate', 'spring', 'timeline', 'scroll', 'stagger'].includes(name)) { imports.add(name); } } } }); const importSpecifiers = Array.from(imports).map(name => t.importSpecifier(t.identifier(name), t.identifier(name)) ); const importDeclaration = t.importDeclaration( importSpecifiers, t.stringLiteral('motion') ); if (t.isProgram(node)) { node.body.unshift(importDeclaration); } context.imports.add('motion'); return node; } private addVueMotionImports(node: any, context: CodeGenerationContext): any { // Vue-specific motion imports const importDeclaration = t.importDeclaration( [t.importSpecifier(t.identifier('MotionPlugin'), t.identifier('MotionPlugin'))], t.stringLiteral('@vueuse/motion') ); if (t.isProgram(node)) { node.body.unshift(importDeclaration); } context.imports.add('@vueuse/motion'); return node; } // Optimization methods private addAccessibilityEnhancements(node: any, _context: CodeGenerationContext): any { const self = this; babel.traverse(node, { JSXElement(path) { const openingElement = path.node.openingElement; // Add reduced motion check for motion elements if (t.isJSXMemberExpression(openingElement.name) && t.isIdentifier(openingElement.name.object) && (openingElement.name.object as any).name === 'motion') { // Add aria-label if not present and element is interactive const hasAriaLabel = openingElement.attributes.some(attr => t.isJSXAttribute(attr) && t.isIdentifier(attr.name) && (attr.name as any).name === 'aria-label' ); if (!hasAriaLabel && self.isInteractiveElement(openingElement)) { openingElement.attributes.push( t.jsxAttribute( t.jsxIdentifier('aria-label'), t.stringLiteral('Interactive element') ) ); } } } }); return node; } private isInteractiveElement(openingElement: any): boolean { const elementName = openingElement.name; if (t.isJSXIdentifier(elementName)) { const tagName = elementName.name.toLowerCase(); return ['button', 'a', 'input', 'select', 'textarea'].includes(tagName); } return false; } private applyPerformanceOptimizations(node: any, _context: CodeGenerationContext): any { // Add will-change optimization const self = this; babel.traverse(node, { JSXElement(path) { const openingElement = path.node.openingElement; if (t.isJSXMemberExpression(openingElement.name) && t.isIdentifier(openingElement.name.object) && (openingElement.name.object as any).name === 'motion') { // Check if element has transform animations const hasTransformAnimation = openingElement.attributes.some(attr => t.isJSXAttribute(attr) && t.isIdentifier(attr.name) && ['animate', 'initial', 'whileHover'].includes((attr.name as any).name) ); if (hasTransformAnimation) { // Add layoutRoot if appropriate const hasLayoutRoot = openingElement.attributes.some(attr => t.isJSXAttribute(attr) && t.isIdentifier(attr.name) && (attr.name as any).name === 'layoutRoot' ); if (!hasLayoutRoot && self.shouldAddLayoutRoot(openingElement)) { openingElement.attributes.push( t.jsxAttribute( t.jsxIdentifier('layoutRoot'), t.jsxExpressionContainer(t.booleanLiteral(true)) ) ); } } } } }); return node; } private shouldAddLayoutRoot(openingElement: any): boolean { // Add layoutRoot for elements that may trigger layout changes return t.isJSXMemberExpression(openingElement.name) && t.isIdentifier(openingElement.name.object) && (openingElement.name.object as any).name === 'motion'; } private addReactPerformanceOptimizations( componentAST: ComponentAST, _context: CodeGenerationContext ): ComponentAST { // This would add React.memo, useCallback, etc. // Simplified implementation return componentAST; } // Helper methods removed - already defined above // Public utility methods for motion transformations addMotionElement( componentAST: ComponentAST, _motionElement: MotionElement, _context: CodeGenerationContext ): ComponentAST { // Add a motion element to the component return componentAST; } createAnimationSequence( _sequence: AnimationSequence, _context: CodeGenerationContext ): any { // Create AST nodes for animation sequence return null; } optimizeForFramework( componentAST: ComponentAST, _targetFramework: Framework, _context: CodeGenerationContext ): ComponentAST { // Convert between frameworks return componentAST; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Abhishekrajpurohit/motion-dev-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server