UNPKG

@yaseratiar/react-responsive-easy-cli

Version:

šŸš€ Enterprise-grade CLI tools with AI integration and performance monitoring for React Responsive Easy

1,279 lines (1,251 loc) • 967 kB
#!/usr/bin/env node 'use strict'; var commander = require('commander'); var chalk = require('chalk'); var ora = require('ora'); var inquirer = require('inquirer'); var fs$1 = require('fs-extra'); var path$1 = require('path'); var glob = require('glob'); var fs$2 = require('fs'); var events = require('events'); var reactResponsiveEasyAiOptimizer = require('@yaseratiar/react-responsive-easy-ai-optimizer'); var reactResponsiveEasyPerformanceDashboard = require('@yaseratiar/react-responsive-easy-performance-dashboard'); var uuid = require('uuid'); var node_crypto = require('node:crypto'); var winston = require('winston'); var Conf = require('conf'); var process$1 = require('node:process'); var crypto = require('crypto'); var child_process = require('child_process'); var util = require('util'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs$2); var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto); // Mock functions for now - will be replaced with actual imports later const createDefaultConfig = () => ({ base: { name: 'desktop', width: 1920, height: 1080, alias: 'base' }, breakpoints: [ { name: 'mobile', width: 390, height: 844, alias: 'mobile' }, { name: 'tablet', width: 768, height: 1024, alias: 'tablet' }, { name: 'laptop', width: 1366, height: 768, alias: 'laptop' }, { name: 'desktop', width: 1920, height: 1080, alias: 'base' } ], strategy: { origin: 'width', tokens: { fontSize: { scale: 0.85, min: 12, max: 22 }, spacing: { scale: 0.85, step: 2 }, radius: { scale: 0.9 } }, rounding: { mode: 'nearest', precision: 0.5 } } }); const configPresets = { conservative: createDefaultConfig(), aggressive: { ...createDefaultConfig(), strategy: { ...createDefaultConfig().strategy, tokens: { fontSize: { scale: 0.7, min: 10, max: 24 }, spacing: { scale: 0.7, step: 1 }, radius: { scale: 0.8 } } } }, 'mobile-first': { ...createDefaultConfig(), base: { name: 'mobile', width: 390, height: 844, alias: 'base' }, breakpoints: [ { name: 'mobile', width: 390, height: 844, alias: 'base' }, { name: 'tablet', width: 768, height: 1024, alias: 'tablet' }, { name: 'laptop', width: 1366, height: 768, alias: 'laptop' }, { name: 'desktop', width: 1920, height: 1080, alias: 'desktop' } ] } }; const initCommand = new commander.Command('init') .description('Initialize React Responsive Easy in your project') .option('-y, --yes', 'Skip prompts and use defaults') .option('-p, --preset <preset>', 'Use configuration preset (conservative|aggressive|mobile-first)') .option('-o, --output <path>', 'Output directory for config files', '.') .action(async (options) => { const spinner = ora('Initializing React Responsive Easy...').start(); try { // Determine output directory const outputDir = path$1.resolve(options.output); // Check if directory exists if (!fs$1.existsSync(outputDir)) { spinner.fail('Output directory does not exist'); process.exit(1); } // Check if already initialized const configPath = path$1.join(outputDir, 'rre.config.ts'); if (fs$1.existsSync(configPath) && !options.yes) { const { overwrite } = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: 'rre.config.ts already exists. Overwrite?', default: false } ]); if (!overwrite) { spinner.info('Initialization cancelled'); return; } } // Get configuration preset let preset = options.preset; if (!preset && !options.yes) { const { selectedPreset } = await inquirer.prompt([ { type: 'list', name: 'selectedPreset', message: 'Choose a configuration preset:', choices: [ { name: 'Conservative - Balanced scaling with accessibility focus', value: 'conservative' }, { name: 'Aggressive - Maximum scaling for dramatic effects', value: 'aggressive' }, { name: 'Mobile-First - Optimized for mobile experiences', value: 'mobile-first' }, { name: 'Custom - Start with defaults and customize later', value: 'custom' } ], default: 'conservative' } ]); preset = selectedPreset; } // Generate configuration let config; if (preset && preset !== 'custom') { config = configPresets[preset]; } else { config = createDefaultConfig(); } // Create configuration file const configContent = generateConfigFile(config, preset); await fs$1.writeFile(configPath, configContent); // Create example component const examplePath = path$1.join(outputDir, 'components', 'ExampleButton.tsx'); await fs$1.ensureDir(path$1.dirname(examplePath)); await fs$1.writeFile(examplePath, generateExampleComponent()); // Create package.json scripts await updatePackageJson(outputDir); // Create README section const readmePath = path$1.join(outputDir, 'RRE_SETUP.md'); await fs$1.writeFile(readmePath, generateSetupGuide()); spinner.succeed('React Responsive Easy initialized successfully!'); // Show next steps console.log(chalk.green('\nšŸŽ‰ Setup complete! Next steps:')); console.log(chalk.cyan('1. ') + 'Install dependencies: npm install @react-responsive-easy/core'); console.log(chalk.cyan('2. ') + 'Wrap your app with <ResponsiveProvider>'); console.log(chalk.cyan('3. ') + 'Use responsive hooks in your components'); console.log(chalk.cyan('4. ') + 'Run rre build to transform your code'); console.log(chalk.cyan('5. ') + 'Run rre analyze to check scaling'); console.log(chalk.yellow('\nšŸ“ Files created:')); console.log(chalk.gray(' • ') + configPath); console.log(chalk.gray(' • ') + examplePath); console.log(chalk.gray(' • ') + readmePath); } catch (error) { spinner.fail('Initialization failed'); console.error(chalk.red('\nāŒ Error:'), error); process.exit(1); } }); function generateConfigFile(config, preset) { return `import { defineConfig } from '@react-responsive-easy/core'; export default defineConfig(${JSON.stringify(config, null, 2)}); // Configuration preset: ${preset || 'custom'} // // Available options: // - base: Base breakpoint (largest screen size) // - breakpoints: Array of breakpoints with width/height // - strategy: Scaling strategy (origin, tokens, rounding) // - tokens: Specialized scaling rules for different CSS properties // // Run 'rre analyze' to validate your configuration // Run 'rre build' to transform your code `; } function generateExampleComponent() { return `import React from 'react'; import { ResponsiveProvider, useResponsiveValue, useBreakpointMatch } from '@react-responsive-easy/core'; import config from '../rre.config'; // Example responsive button component const ExampleButton: React.FC = () => { const fontSize = useResponsiveValue(18, { token: 'fontSize' }); const padding = useResponsiveValue(16, { token: 'spacing' }); const isMobile = useBreakpointMatch('mobile'); return ( <button style={{ fontSize: \`\${fontSize}px\`, padding: \`\${padding}px\`, borderRadius: '8px', border: 'none', backgroundColor: '#007bff', color: 'white', cursor: 'pointer' }} > {isMobile ? 'Tap me' : 'Click me'} </button> ); }; // Example app wrapper const App: React.FC = () => ( <ResponsiveProvider config={config}> <div style={{ padding: '20px' }}> <h1>React Responsive Easy Example</h1> <ExampleButton /> </div> </ResponsiveProvider> ); export default App; export { ExampleButton }; `; } async function updatePackageJson(outputDir) { const packagePath = path$1.join(outputDir, 'package.json'); if (!fs$1.existsSync(packagePath)) { return; // No package.json to update } try { const packageJson = await fs$1.readJson(packagePath); // Add RRE scripts if (!packageJson.scripts) { packageJson.scripts = {}; } packageJson.scripts['rre:build'] = 'rre build'; packageJson.scripts['rre:analyze'] = 'rre analyze'; packageJson.scripts['rre:dev'] = 'rre dev'; // Add RRE dependencies if not present if (!packageJson.dependencies) { packageJson.dependencies = {}; } if (!packageJson.dependencies['@react-responsive-easy/core']) { packageJson.dependencies['@react-responsive-easy/core'] = '^0.0.1'; } await fs$1.writeJson(packagePath, packageJson, { spaces: 2 }); } catch (error) { // Silently fail - package.json update is optional console.warn(chalk.yellow('Warning: Could not update package.json')); } } function generateSetupGuide() { return `# React Responsive Easy Setup Guide ## šŸš€ Quick Start 1. **Install dependencies:** \`\`\`bash npm install @react-responsive-easy/core \`\`\` 2. **Wrap your app:** \`\`\`tsx import { ResponsiveProvider } from '@react-responsive-easy/core'; import config from './rre.config'; const App = () => ( <ResponsiveProvider config={config}> <YourApp /> </ResponsiveProvider> ); \`\`\` 3. **Use responsive hooks:** \`\`\`tsx const Button = () => { const fontSize = useResponsiveValue(18, { token: 'fontSize' }); return <button style={{ fontSize }}>Click me</button>; }; \`\`\` ## šŸ› ļø CLI Commands - \`rre build\` - Transform your code for production - \`rre analyze\` - Check scaling and configuration - \`rre dev\` - Development server with live preview ## šŸ“š Documentation Visit: https://github.com/naaa-G/react-responsive-easy ## šŸ†˜ Need Help? - Check the configuration in \`rre.config.ts\` - Run \`rre analyze\` to validate setup - Review the example component in \`components/ExampleButton.tsx\` `; } // Mock validation function for now const validateConfig$4 = (config) => { const errors = []; if (!config.base) { errors.push('Missing base breakpoint configuration'); } if (!config.breakpoints || !Array.isArray(config.breakpoints)) { errors.push('Missing or invalid breakpoints configuration'); } if (!config.strategy) { errors.push('Missing scaling strategy configuration'); } return { valid: errors.length === 0, errors }; }; const buildCommand = new commander.Command('build') .description('Build and transform your React Responsive Easy code') .option('-c, --config <path>', 'Path to configuration file', 'rre.config.ts') .option('-i, --input <pattern>', 'Input file pattern', 'src/**/*.{ts,tsx,js,jsx}') .option('-o, --output <path>', 'Output directory', 'dist') .option('-w, --watch', 'Watch mode for development') .option('--clean', 'Clean output directory before building') .option('--verbose', 'Verbose output') .action(async (options) => { const spinner = ora('Building React Responsive Easy project...').start(); try { // Validate configuration spinner.text = 'Validating configuration...'; const configPath = path$1.resolve(options.config); if (!fs$1.existsSync(configPath)) { spinner.fail(`Configuration file not found: ${configPath}`); console.log(chalk.yellow('\nšŸ’” Run "rre init" to create a configuration file')); process.exit(1); } // Load and validate config const config = await loadConfig$3(configPath); const validationResult = validateConfig$4(config); if (!validationResult.valid) { spinner.fail('Configuration validation failed'); console.error(chalk.red('\nāŒ Configuration errors:')); validationResult.errors.forEach(error => { console.error(chalk.red(` • ${error}`)); }); process.exit(1); } spinner.succeed('Configuration validated successfully'); // Clean output directory if requested if (options.clean) { spinner.text = 'Cleaning output directory...'; const outputDir = path$1.resolve(options.output); if (fs$1.existsSync(outputDir)) { await fs$1.remove(outputDir); } await fs$1.ensureDir(outputDir); spinner.succeed('Output directory cleaned'); } // Find input files spinner.text = 'Scanning input files...'; const inputPattern = options.input; const inputFiles = glob.glob.sync(inputPattern, { ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: true }); if (inputFiles.length === 0) { spinner.warn('No input files found'); console.log(chalk.yellow(`\nšŸ’” Pattern: ${inputPattern}`)); console.log(chalk.yellow('šŸ’” Make sure you have source files in your project')); return; } spinner.succeed(`Found ${inputFiles.length} input files`); // Process files spinner.text = 'Processing files...'; const results = await processFiles(inputFiles, config, options); // Generate build report const report = generateBuildReport(results, config); // Save build report const outputDir = path$1.resolve(options.output); await fs$1.ensureDir(outputDir); const reportPath = path$1.join(outputDir, 'rre-build-report.json'); await fs$1.writeJson(reportPath, report, { spaces: 2 }); // Show results spinner.succeed('Build completed successfully!'); console.log(chalk.green('\nšŸŽ‰ Build Results:')); console.log(chalk.cyan(' • ') + `Files processed: ${results.totalFiles}`); console.log(chalk.cyan(' • ') + `Responsive values found: ${results.responsiveValues}`); console.log(chalk.cyan(' • ') + `Breakpoints configured: ${config.breakpoints.length}`); console.log(chalk.cyan(' • ') + `Build report: ${reportPath}`); if (results.warnings.length > 0) { console.log(chalk.yellow('\nāš ļø Warnings:')); results.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)); }); } if (options.verbose) { console.log(chalk.gray('\nšŸ“Š Detailed Results:')); console.log(JSON.stringify(results, null, 2)); } } catch (error) { spinner.fail('Build failed'); console.error(chalk.red('\nāŒ Error:'), error); process.exit(1); } }); async function loadConfig$3(configPath) { try { const configContent = await fs$1.readFile(configPath, 'utf-8'); // Extract the config object from the file const configMatch = configContent.match(/defineConfig\(([\s\S]*?)\)/); if (!configMatch) { throw new Error('Could not parse configuration file'); } // This is a simplified approach - in production you'd want proper TypeScript compilation const configString = configMatch[1]; return JSON.parse(configString); } catch (error) { throw new Error(`Failed to load configuration: ${error}`); } } async function processFiles(files, config, options) { const results = { totalFiles: files.length, responsiveValues: 0, processedFiles: 0, warnings: [], errors: [], fileResults: [] }; for (const file of files) { try { const content = await fs$1.readFile(file, 'utf-8'); const fileResult = await processFile(file, content, config); results.responsiveValues += fileResult.responsiveValues; results.processedFiles++; results.fileResults.push(fileResult); if (fileResult.warnings.length > 0) { results.warnings.push(...fileResult.warnings.map((w) => `${file}: ${w}`)); } } catch (error) { results.errors.push(`${file}: ${error}`); } } return results; } async function processFile(filePath, content, config) { const result = { file: filePath, responsiveValues: 0, warnings: [], transformations: [] }; // Look for responsive hook usage const hookPatterns = [ /useResponsiveValue\s*\(\s*(\d+)/g, /useScaledStyle\s*\(\s*\{/g, /useBreakpointMatch\s*\(\s*['"`]([^'"`]+)['"`]/g ]; for (const pattern of hookPatterns) { const matches = content.matchAll(pattern); for (const match of matches) { result.responsiveValues++; // Analyze the usage const transformation = analyzeResponsiveUsage(match); if (transformation) { result.transformations.push(transformation); } } } // Check for potential issues if (content.includes('useResponsiveValue') && !content.includes('ResponsiveProvider')) { result.warnings.push('useResponsiveValue used without ResponsiveProvider wrapper'); } return result; } function analyzeResponsiveUsage(match, config) { // This would contain logic to analyze how the responsive value is used // and suggest optimizations return { type: 'responsive_value', value: match[1], suggestions: [] }; } function generateBuildReport(results, config) { return { timestamp: new Date().toISOString(), config: { base: config.base, breakpoints: config.breakpoints.length, strategy: config.strategy.origin }, build: { totalFiles: results.totalFiles, processedFiles: results.processedFiles, responsiveValues: results.responsiveValues, warnings: results.warnings.length, errors: results.errors.length }, performance: { estimatedBundleSize: estimateBundleSize$1(results, config), scalingComplexity: calculateScalingComplexity$1(config) }, recommendations: generateRecommendations(results, config) }; } function estimateBundleSize$1(results, config) { // Simple estimation based on responsive values and breakpoints const baseSize = 50; // KB base size const responsiveOverhead = results.responsiveValues * 0.5; // KB per responsive value const breakpointOverhead = config.breakpoints.length * 0.2; // KB per breakpoint const totalKB = baseSize + responsiveOverhead + breakpointOverhead; return `${totalKB.toFixed(1)} KB`; } function calculateScalingComplexity$1(config) { const breakpointCount = config.breakpoints.length; const tokenCount = Object.keys(config.strategy.tokens || {}).length; if (breakpointCount <= 3 && tokenCount <= 3) return 'Low'; if (breakpointCount <= 5 && tokenCount <= 5) return 'Medium'; return 'High'; } function generateRecommendations(results, config) { const recommendations = []; if (results.responsiveValues === 0) { recommendations.push('No responsive values found. Consider using useResponsiveValue for dynamic scaling.'); } if (config.breakpoints.length > 6) { recommendations.push('Consider reducing breakpoints to 4-6 for optimal performance.'); } const hasUnusedBreakpoints = config.breakpoints.some((bp) => bp.width < 320 || bp.width > 2560); if (hasUnusedBreakpoints) { recommendations.push('Remove breakpoints outside common device ranges (320px - 2560px)'); } return recommendations; } // Mock validation function for now const validateConfig$3 = (config) => { const errors = []; if (!config.base) { errors.push('Missing base breakpoint configuration'); } if (!config.breakpoints || !Array.isArray(config.breakpoints)) { errors.push('Missing or invalid breakpoints configuration'); } if (!config.strategy) { errors.push('Missing scaling strategy configuration'); } return { valid: errors.length === 0, errors }; }; const analyzeCommand = new commander.Command('analyze') .description('Analyze your React Responsive Easy setup and provide insights') .option('-c, --config <path>', 'Path to configuration file', 'rre.config.ts') .option('-i, --input <pattern>', 'Input file pattern', 'src/**/*.{ts,tsx,js,jsx}') .option('--detailed', 'Show detailed analysis for each file') .option('--performance', 'Include performance analysis') .option('--accessibility', 'Include accessibility checks') .option('--export <path>', 'Export analysis report to file') .action(async (options) => { const spinner = ora('Analyzing React Responsive Easy setup...').start(); try { // Load configuration spinner.text = 'Loading configuration...'; const configPath = path$1.resolve(options.config); if (!fs$1.existsSync(configPath)) { spinner.fail(`Configuration file not found: ${configPath}`); console.log(chalk.yellow('\nšŸ’” Run "rre init" to create a configuration file')); process.exit(1); } const config = await loadConfig$2(configPath); // Validate configuration spinner.text = 'Validating configuration...'; const validationResult = validateConfig$3(config); if (!validationResult.valid) { spinner.fail('Configuration validation failed'); console.error(chalk.red('\nāŒ Configuration errors:')); validationResult.errors.forEach(error => { console.error(chalk.red(` • ${error}`)); }); process.exit(1); } spinner.succeed('Configuration loaded and validated'); // Analyze configuration const configAnalysis = analyzeConfiguration(config); // Find and analyze source files spinner.text = 'Analyzing source files...'; const inputPattern = options.input; const inputFiles = glob.glob.sync(inputPattern, { ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: true }); if (inputFiles.length === 0) { spinner.warn('No source files found for analysis'); } else { spinner.succeed(`Found ${inputFiles.length} source files`); } // Analyze files const fileAnalysis = await analyzeFiles(inputFiles, config, options); // Generate comprehensive analysis const analysis = generateAnalysis(configAnalysis, fileAnalysis, config, options); // Display results displayAnalysis(analysis, options); // Export if requested if (options.export) { await exportAnalysis(analysis, options.export); console.log(chalk.green(`\nšŸ“Š Analysis exported to: ${options.export}`)); } // Show summary showSummary(analysis); } catch (error) { spinner.fail('Analysis failed'); console.error(chalk.red('\nāŒ Error:'), error); process.exit(1); } }); async function loadConfig$2(configPath) { try { const configContent = await fs$1.readFile(configPath, 'utf-8'); const configMatch = configContent.match(/defineConfig\(([\s\S]*?)\)/); if (!configMatch) { throw new Error('Could not parse configuration file'); } const configString = configMatch[1]; return JSON.parse(configString); } catch (error) { throw new Error(`Failed to load configuration: ${error}`); } } function analyzeConfiguration(config) { const analysis = { breakpoints: { count: config.breakpoints.length, coverage: analyzeBreakpointCoverage(config.breakpoints), distribution: analyzeBreakpointDistribution(config.breakpoints) }, strategy: { origin: config.strategy.origin, complexity: calculateStrategyComplexity(config.strategy), tokens: analyzeTokens(config.strategy.tokens) }, performance: { estimatedOverhead: estimateConfigurationOverhead(config), optimizationOpportunities: findOptimizationOpportunities(config) } }; return analysis; } function analyzeBreakpointCoverage(breakpoints) { const widths = breakpoints.map(bp => bp.width).sort((a, b) => a - b); const heights = breakpoints.map(bp => bp.height).sort((a, b) => a - b); const widthRange = { min: widths[0], max: widths[widths.length - 1] }; const heightRange = { min: heights[0], max: heights[heights.length - 1] }; // Check for common device sizes const commonDevices = { mobile: breakpoints.some(bp => bp.width <= 480), tablet: breakpoints.some(bp => bp.width > 480 && bp.width <= 1024), desktop: breakpoints.some(bp => bp.width > 1024) }; return { widthRange, heightRange, commonDevices, gaps: findBreakpointGaps(widths) }; } function findBreakpointGaps(widths) { const gaps = []; for (let i = 1; i < widths.length; i++) { const gap = widths[i] - widths[i - 1]; if (gap > 200) { // Large gap threshold gaps.push({ from: widths[i - 1], to: widths[i], size: gap }); } } return gaps; } function analyzeBreakpointDistribution(breakpoints) { const sorted = breakpoints.sort((a, b) => a.width - b.width); const ratios = []; for (let i = 1; i < sorted.length; i++) { const ratio = sorted[i].width / sorted[i - 1].width; ratios.push(ratio); } const avgRatio = ratios.reduce((sum, r) => sum + r, 0) / ratios.length; return { ratios, averageRatio: avgRatio, consistency: avgRatio < 1.5 ? 'Good' : avgRatio < 2.0 ? 'Fair' : 'Poor' }; } function calculateStrategyComplexity(strategy) { const tokenCount = Object.keys(strategy.tokens || {}).length; const hasCustomRounding = strategy.rounding && strategy.rounding.mode !== 'nearest'; const hasCustomCurves = Object.values(strategy.tokens || {}).some((token) => token.curve && token.curve !== 'linear'); let complexity = 0; if (tokenCount > 3) complexity += 2; if (hasCustomRounding) complexity += 1; if (hasCustomCurves) complexity += 1; if (complexity <= 1) return 'Low'; if (complexity <= 3) return 'Medium'; return 'High'; } function analyzeTokens(tokens) { if (!tokens) return { count: 0, types: [] }; const types = Object.keys(tokens); const analysis = types.map(type => { const token = tokens[type]; return { type, hasMin: !!token.min, hasMax: !!token.max, hasStep: !!token.step, hasCurve: !!token.curve && token.curve !== 'linear', scale: token.scale || 1 }; }); return { count: types.length, types: analysis, accessibility: analyzeTokenAccessibility(tokens) }; } function analyzeTokenAccessibility(tokens) { const fontSize = tokens.fontSize; const spacing = tokens.spacing; const issues = []; if (fontSize && fontSize.min && fontSize.min < 12) { issues.push('Font size minimum below 12px may affect readability'); } if (spacing && spacing.min && spacing.min < 4) { issues.push('Spacing minimum below 4px may affect touch targets'); } return { issues, score: issues.length === 0 ? 'Good' : issues.length <= 2 ? 'Fair' : 'Poor' }; } function estimateConfigurationOverhead(config) { const breakpointCount = config.breakpoints.length; const tokenCount = Object.keys(config.strategy.tokens || {}).length; // Rough estimation of runtime overhead const baseOverhead = 5; // KB const breakpointOverhead = breakpointCount * 0.3; // KB per breakpoint const tokenOverhead = tokenCount * 0.2; // KB per token const totalKB = baseOverhead + breakpointOverhead + tokenOverhead; return `${totalKB.toFixed(1)} KB`; } function findOptimizationOpportunities(config) { const opportunities = []; if (config.breakpoints.length > 6) { opportunities.push('Consider reducing breakpoints to 4-6 for optimal performance'); } if (Object.keys(config.strategy.tokens || {}).length > 5) { opportunities.push('Consider consolidating similar token types'); } const hasUnusedBreakpoints = config.breakpoints.some((bp) => bp.width < 320 || bp.width > 2560); if (hasUnusedBreakpoints) { opportunities.push('Remove breakpoints outside common device ranges (320px - 2560px)'); } return opportunities; } async function analyzeFiles(files, config, options) { const analysis = { totalFiles: files.length, responsiveFiles: 0, totalResponsiveValues: 0, hookUsage: { useResponsiveValue: 0, useScaledStyle: 0, useBreakpoint: 0, useBreakpointMatch: 0 }, issues: [], recommendations: [], fileDetails: [] }; for (const file of files) { try { const content = await fs$1.readFile(file, 'utf-8'); const fileAnalysis = analyzeFile(file, content, config); if (fileAnalysis.hasResponsiveCode) { analysis.responsiveFiles++; } analysis.totalResponsiveValues += fileAnalysis.responsiveValues; analysis.hookUsage.useResponsiveValue += fileAnalysis.hooks.useResponsiveValue; analysis.hookUsage.useScaledStyle += fileAnalysis.hooks.useScaledStyle; analysis.hookUsage.useBreakpoint += fileAnalysis.hooks.useBreakpoint; analysis.hookUsage.useBreakpointMatch += fileAnalysis.hooks.useBreakpointMatch; if (fileAnalysis.issues.length > 0) { analysis.issues.push(...fileAnalysis.issues.map((issue) => ({ ...issue, file }))); } if (options.detailed) { analysis.fileDetails.push(fileAnalysis); } } catch (error) { analysis.issues.push({ type: 'error', message: `Failed to analyze file: ${error}`, file }); } } // Generate recommendations analysis.recommendations = generateFileRecommendations(analysis); return analysis; } function analyzeFile(file, content, config) { const analysis = { file, hasResponsiveCode: false, responsiveValues: 0, hooks: { useResponsiveValue: 0, useScaledStyle: 0, useBreakpoint: 0, useBreakpointMatch: 0 }, issues: [], suggestions: [] }; // Check for ResponsiveProvider usage const hasProvider = content.includes('ResponsiveProvider'); // Count hook usage const hookPatterns = [ { name: 'useResponsiveValue', pattern: /useResponsiveValue\s*\(/g }, { name: 'useScaledStyle', pattern: /useScaledStyle\s*\(/g }, { name: 'useBreakpoint', pattern: /useBreakpoint\s*\(/g }, { name: 'useBreakpointMatch', pattern: /useBreakpointMatch\s*\(/g } ]; for (const hook of hookPatterns) { const matches = content.match(hook.pattern); if (matches) { analysis.hooks[hook.name] = matches.length; analysis.responsiveValues += matches.length; analysis.hasResponsiveCode = true; } } // Check for issues if (analysis.hasResponsiveCode && !hasProvider) { analysis.issues.push({ type: 'warning', message: 'Responsive hooks used without ResponsiveProvider wrapper' }); } if (analysis.responsiveValues > 20) { analysis.suggestions.push('Consider breaking down large components with many responsive values'); } return analysis; } function generateFileRecommendations(analysis) { const recommendations = []; if (analysis.responsiveFiles === 0) { recommendations.push('No responsive code found. Consider using responsive hooks for better mobile experience.'); } if (analysis.hookUsage.useResponsiveValue === 0) { recommendations.push('Consider using useResponsiveValue for dynamic font sizes and spacing.'); } if (analysis.issues.length > 0) { recommendations.push('Fix identified issues to ensure proper responsive behavior.'); } if (analysis.totalResponsiveValues > 100) { recommendations.push('High number of responsive values detected. Consider performance optimization.'); } return recommendations; } function generateAnalysis(configAnalysis, fileAnalysis, config, options) { return { timestamp: new Date().toISOString(), summary: { status: fileAnalysis.issues.length === 0 ? 'Healthy' : 'Needs Attention', score: calculateOverallScore(configAnalysis, fileAnalysis), priority: determinePriority(configAnalysis, fileAnalysis) }, configuration: configAnalysis, files: fileAnalysis, performance: options.performance ? generatePerformanceAnalysis(config, fileAnalysis) : null, accessibility: options.accessibility ? generateAccessibilityAnalysis(config) : null }; } function calculateOverallScore(configAnalysis, fileAnalysis) { let score = 100; // Deduct points for issues score -= fileAnalysis.issues.length * 5; // Deduct points for configuration complexity if (configAnalysis.strategy.complexity === 'High') score -= 10; if (configAnalysis.breakpoints.count > 6) score -= 5; // Bonus for good practices if (fileAnalysis.responsiveFiles > 0) score += 10; if (configAnalysis.breakpoints.coverage.commonDevices.mobile) score += 5; return Math.max(0, Math.min(100, score)); } function determinePriority(configAnalysis, fileAnalysis) { if (fileAnalysis.issues.length > 5) return 'High'; if (fileAnalysis.issues.length > 2) return 'Medium'; if (configAnalysis.strategy.complexity === 'High') return 'Medium'; return 'Low'; } function generatePerformanceAnalysis(config, fileAnalysis) { return { estimatedBundleSize: estimateBundleSize(fileAnalysis, config), scalingComplexity: calculateScalingComplexity(config), optimizationOpportunities: findPerformanceOptimizations(fileAnalysis, config) }; } function generateAccessibilityAnalysis(config, fileAnalysis) { return { tokenAccessibility: analyzeTokenAccessibility(config.strategy.tokens), breakpointCoverage: config.breakpoints.some((bp) => bp.width <= 480), recommendations: generateAccessibilityRecommendations(config) }; } function estimateBundleSize(fileAnalysis, config) { const baseSize = 50; // KB const responsiveOverhead = fileAnalysis.totalResponsiveValues * 0.3; const breakpointOverhead = config.breakpoints.length * 0.2; const totalKB = baseSize + responsiveOverhead + breakpointOverhead; return `${totalKB.toFixed(1)} KB`; } function calculateScalingComplexity(config) { const breakpointCount = config.breakpoints.length; const tokenCount = Object.keys(config.strategy.tokens || {}).length; if (breakpointCount <= 3 && tokenCount <= 3) return 'Low'; if (breakpointCount <= 5 && tokenCount <= 5) return 'Medium'; return 'High'; } function findPerformanceOptimizations(fileAnalysis, config) { const optimizations = []; if (fileAnalysis.totalResponsiveValues > 50) { optimizations.push('Consider lazy loading responsive values for better initial load'); } if (config.breakpoints.length > 6) { optimizations.push('Reduce breakpoints to improve runtime performance'); } return optimizations; } function generateAccessibilityRecommendations(config, fileAnalysis) { const recommendations = []; if (!config.strategy.tokens?.fontSize?.min || config.strategy.tokens.fontSize.min < 12) { recommendations.push('Set minimum font size to 12px for better readability'); } if (!config.strategy.tokens?.spacing?.min || config.strategy.tokens.spacing.min < 4) { recommendations.push('Set minimum spacing to 4px for better touch targets'); } return recommendations; } function displayAnalysis(analysis, options) { console.log(chalk.blue('\nšŸ” React Responsive Easy Analysis Report')); console.log(chalk.gray('='.repeat(60))); // Summary console.log(chalk.green(`\nšŸ“Š Overall Status: ${analysis.summary.status}`)); console.log(chalk.cyan(` Score: ${analysis.summary.score}/100`)); console.log(chalk.cyan(` Priority: ${analysis.summary.priority}`)); // Configuration console.log(chalk.yellow('\nāš™ļø Configuration Analysis:')); console.log(chalk.gray(` Breakpoints: ${analysis.configuration.breakpoints.count}`)); console.log(chalk.gray(` Strategy Complexity: ${analysis.configuration.strategy.complexity}`)); console.log(chalk.gray(` Tokens: ${analysis.configuration.strategy.tokens.count}`)); console.log(chalk.gray(` Estimated Overhead: ${analysis.configuration.performance.estimatedOverhead}`)); // Files console.log(chalk.yellow('\nšŸ“ File Analysis:')); console.log(chalk.gray(` Total Files: ${analysis.files.totalFiles}`)); console.log(chalk.gray(` Responsive Files: ${analysis.files.responsiveFiles}`)); console.log(chalk.gray(` Total Responsive Values: ${analysis.files.totalResponsiveValues}`)); // Hook Usage console.log(chalk.yellow('\nšŸŖ Hook Usage:')); Object.entries(analysis.files.hookUsage).forEach(([hook, count]) => { if (count > 0) { console.log(chalk.gray(` ${hook}: ${count}`)); } }); // Issues if (analysis.files.issues.length > 0) { console.log(chalk.red('\nāš ļø Issues Found:')); analysis.files.issues.forEach((issue) => { const icon = issue.type === 'error' ? 'āŒ' : 'āš ļø'; console.log(chalk.red(` ${icon} ${issue.message}`)); if (issue.file) { console.log(chalk.gray(` File: ${issue.file}`)); } }); } // Recommendations if (analysis.files.recommendations.length > 0) { console.log(chalk.blue('\nšŸ’” Recommendations:')); analysis.files.recommendations.forEach((rec) => { console.log(chalk.blue(` • ${rec}`)); }); } // Performance (if requested) if (analysis.performance) { console.log(chalk.yellow('\n⚔ Performance Analysis:')); console.log(chalk.gray(` Estimated Bundle Size: ${analysis.performance.estimatedBundleSize}`)); console.log(chalk.gray(` Scaling Complexity: ${analysis.performance.scalingComplexity}`)); if (analysis.performance.optimizationOpportunities.length > 0) { console.log(chalk.blue(' Optimization Opportunities:')); analysis.performance.optimizationOpportunities.forEach((opt) => { console.log(chalk.blue(` • ${opt}`)); }); } } // Accessibility (if requested) if (analysis.accessibility) { console.log(chalk.yellow('\n♿ Accessibility Analysis:')); console.log(chalk.gray(` Token Accessibility: ${analysis.accessibility.tokenAccessibility.score}`)); console.log(chalk.gray(` Mobile Coverage: ${analysis.accessibility.breakpointCoverage ? 'Yes' : 'No'}`)); if (analysis.accessibility.recommendations.length > 0) { console.log(chalk.blue(' Accessibility Recommendations:')); analysis.accessibility.recommendations.forEach((rec) => { console.log(chalk.blue(` • ${rec}`)); }); } } } function showSummary(analysis) { console.log(chalk.gray('\n' + '='.repeat(60))); if (analysis.summary.score >= 80) { console.log(chalk.green('šŸŽ‰ Excellent! Your setup is well-optimized.')); } else if (analysis.summary.score >= 60) { console.log(chalk.yellow('šŸ‘ Good setup with room for improvement.')); } else { console.log(chalk.red('āš ļø Setup needs attention. Review issues and recommendations.')); } console.log(chalk.blue('\nšŸ’” Run "rre build" to transform your code for production')); console.log(chalk.blue('šŸ’” Run "rre dev" to start development server with live preview')); } async function exportAnalysis(analysis, exportPath) { await fs$1.writeJson(exportPath, analysis, { spaces: 2 }); } // Mock validation function for now const validateConfig$2 = (config) => { const errors = []; if (!config.base) { errors.push('Missing base breakpoint configuration'); } if (!config.breakpoints || !Array.isArray(config.breakpoints)) { errors.push('Missing or invalid breakpoints configuration'); } if (!config.strategy) { errors.push('Missing scaling strategy configuration'); } return { valid: errors.length === 0, errors }; }; const devCommand = new commander.Command('dev') .description('Start development server with live preview and responsive testing') .option('-c, --config <path>', 'Path to configuration file', 'rre.config.ts') .option('-p, --port <number>', 'Port for development server', '3000') .option('-h, --host <host>', 'Host for development server', 'localhost') .option('--open', 'Open browser automatically') .option('--live', 'Enable live reload') .option('--responsive', 'Show responsive preview panel') .action(async (options) => { const spinner = ora('Starting React Responsive Easy development server...').start(); try { // Validate configuration spinner.text = 'Validating configuration...'; const configPath = path$1.resolve(options.config); if (!fs$1.existsSync(configPath)) { spinner.fail(`Configuration file not found: ${configPath}`); console.log(chalk.yellow('\nšŸ’” Run "rre init" to create a configuration file')); process.exit(1); } // Load and validate config const config = await loadConfig$1(configPath); const validationResult = validateConfig$2(config); if (!validationResult.valid) { spinner.fail('Configuration validation failed'); console.error(chalk.red('\nāŒ Configuration errors:')); validationResult.errors.forEach(error => { console.error(chalk.red(` • ${error}`)); }); process.exit(1); } spinner.succeed('Configuration validated successfully'); // Check for package.json and dependencies spinner.text = 'Checking project setup...'; const projectSetup = await checkProjectSetup(); if (!projectSetup.hasPackageJson) { spinner.warn('No package.json found. Some features may not work.'); } if (!projectSetup.hasCoreDependency) { spinner.warn('@react-responsive-easy/core not found in dependencies.'); console.log(chalk.yellow('\nšŸ’” Install with: npm install @react-responsive-easy/core')); } // Generate development server files spinner.text = 'Setting up development server...'; const devFiles = await setupDevelopmentServer(config, options); // Start development server spinner.text = 'Starting server...'; await startDevelopmentServer(devFiles, options); spinner.succeed('Development server started successfully!'); // Show server information showServerInfo(options, config, devFiles); // Show responsive testing instructions showResponsiveTestingInstructions(config); // Keep the process running process.on('SIGINT', () => { console.log(chalk.yellow('\nšŸ›‘ Development server stopped')); process.exit(0); }); } catch (error) { spinner.fail('Failed to start development server'); console.error(chalk.red('\nāŒ Error:'), error); process.exit(1); } }); async function loadConfig$1(configPath) { try { const configContent = await fs$1.readFile(configPath, 'utf-8'); const configMatch = configContent.match(/defineConfig\(([\s\S]*?)\)/); if (!configMatch) { throw new Error('Could not parse configuration file'); } const configString = configMatch[1]; return JSON.parse(configString); } catch (error) { throw new Error(`Failed to load configuration: ${error}`); } } async function checkProjectSetup() { const hasPackageJson = fs$1.existsSync('package.json'); let hasCoreDependency = false; if (hasPackageJson) { try { const packageJson = await fs$1.readJson('package.json'); hasCoreDependency = packageJson.dependencies?.['@react-responsive-easy/core'] || packageJson.devDependencies?.['@react-responsive-easy/core']; } catch (error) { // Silently fail } } return { hasPackageJson, hasCoreDependency }; } async function setupDevelopmentServer(config, options) { const devDir = '.rre-dev'; await fs$1.ensureDir(devDir); // Generate HTML template const htmlContent = generateDevHTML(config, options); const htmlPath = path$1.join(devDir, 'index.html'); await fs$1.writeFile(htmlPath, htmlContent); // Generate development JavaScript const jsContent = generateDevJS(config, options); const jsPath = path$1.join(devDir, 'dev.js'); await fs$1.writeFile(jsPath, jsContent); // Generate CSS for responsive preview const cssContent = generateDevCSS(config); const cssPath = path$1.join(devDir, 'dev.css'); await fs$1.writeFile(cssPath, cssContent); return { htmlPath, jsPath, cssPath, devDir }; } function generateDevHTML(config, options) { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React Responsive Easy - Development Server</title> <link rel="stylesheet" href="dev.css"> </head> <body> <div id="app"> <div class="loading"> <div class="spinner"></div> <p>Loading React Responsive Easy...</p> </div> </div> ${options.responsive ? generateResponsivePreviewPanel(config) : ''} <script src="dev.js"></script> </body> </html>`; } function generateResponsivePreviewPanel(config) { const breakpoints = config.breakpoints.map((bp) => `<option value="${bp.name}">${bp.name} (${bp.width}Ɨ${bp.height})</option>`).join(''); return ` <div class="responsive-preview-panel"> <div class="panel-header"> <h3>šŸ“± Responsive Preview</h3> <button class="close-btn" onclick="togglePreviewPanel()">Ɨ</button> </div> <div class="panel-content"> <div class="breakpoint-selector"> <label for="breakpoint-select">Current Breakpoint:</label> <select id="breakpoint-select" onchange="changeBreakpoint(this.value)"> ${breakpoints} </select> </div> <div class="viewport-info"> <div class="info-item"> <span class="label">Width:</span> <span id="viewport-width">-</span> </div> <div class="info-item"> <span class="label">Height:</span> <span id="viewport-height">-</span> </div> <div class="info-item"> <span class="label">Scale:</span> <span id="viewport-scale">-</span> </div> </div> <div class="responsive-controls"> <button onclick="toggleDeviceFrame()">Toggle Device Frame</button> <button onclick="resetViewport()">Reset Viewport</button> </div> </div> </div>`; } function generateDevJS(config, options) { return ` // React Responsive Easy Development Server // This is a simplified development environment for testing responsive behavior let currentBreakpoint = '${config.base.name}'; let deviceFrameVisible = false; // Mock ResponsiveProvider for development class MockResponsiveProvider { co