@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
JavaScript
#!/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