UNPKG

@neurolint/cli

Version:

Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations

177 lines (162 loc) 6.08 kB
#!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); const BackupManager = require('../backup-manager'); /** * Layer 4: Hydration and SSR Fixes * - Add window/document guards * - Add mounted state for theme providers * - Fix hydration mismatches in useEffect */ async function isRegularFile(filePath) { try { const stat = await fs.stat(filePath); return stat.isFile(); } catch { return false; } } async function transform(code, options = {}) { const { dryRun = false, verbose = false, filePath = process.cwd() } = options; const results = []; let changeCount = 0; let updatedCode = code; try { // Create centralized backup if it exists and is regular file const existsAsFile = await isRegularFile(filePath); if (existsAsFile && !dryRun) { try { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const backupResult = await backupManager.createBackup(filePath, 'layer-4-hydration'); if (backupResult.success) { results.push({ type: 'backup', file: filePath, success: true, backupPath: backupResult.backupPath }); if (verbose) process.stdout.write(`Created centralized backup: ${path.basename(backupResult.backupPath)}\n`); } else { if (verbose) process.stderr.write(`Warning: Could not create backup: ${backupResult.error}\n`); } } catch (error) { if (verbose) process.stderr.write(`Warning: Backup creation failed: ${error.message}\n`); } } // Check for empty input if (!code.trim()) { results.push({ type: 'empty', file: filePath, success: false, error: 'No changes were made' }); return { success: false, code, originalCode: code, changeCount: 0, results }; } // Hydration Fixes const hydrationFixes = [ { name: 'LocalStorage SSR Guard', pattern: /localStorage\.(getItem|setItem|removeItem)\(([^)]+)\)/g, replacement: (match, method, args) => `typeof window !== "undefined" ? localStorage.${method}(${args}) : null`, fileTypes: ['ts', 'tsx', 'js', 'jsx'] }, { name: 'Window SSR Guard', pattern: /window\.matchMedia\(([^)]+)\)/g, replacement: (match, args) => `typeof window !== "undefined" ? window.matchMedia(${args}) : null`, fileTypes: ['ts', 'tsx', 'js', 'jsx'] }, { name: 'Document SSR Guard', pattern: /document\.(documentElement|body|querySelector)\b(\([^)]*\))?/g, replacement: (match, method, call = '') => `typeof document !== "undefined" ? document.${method}${call} : null`, fileTypes: ['ts', 'tsx', 'js', 'jsx'] } ]; const fileExt = path.extname(filePath).slice(1); hydrationFixes.forEach(fix => { if (fix.fileTypes.includes(fileExt)) { const matches = updatedCode.match(fix.pattern) || []; if (matches.length) { updatedCode = updatedCode.replace(fix.pattern, fix.replacement); changeCount += matches.length; results.push({ type: 'hydration', file: filePath, success: true, changes: matches.length, details: `Applied ${fix.name}` }); } } }); // Theme Provider Hydration if (fileExt === 'tsx' && updatedCode.includes('ThemeProvider') && !updatedCode.includes('mounted')) { const mountedStatePattern = /const \[theme, setTheme\] = useState<Theme>\('light'\);/; if (mountedStatePattern.test(updatedCode)) { updatedCode = updatedCode.replace( mountedStatePattern, `const [theme, setTheme] = useState<Theme>('light');\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);` ).replace( /return \(\s*<ThemeContext\.Provider/, `if (!mounted) {\n return <ThemeContext.Provider value={{ theme: 'light', setTheme: () => {} }}>{children}</ThemeContext.Provider>;\n }\n\n return (\n <ThemeContext.Provider` ); changeCount++; results.push({ type: 'theme_provider', file: filePath, success: true, changes: 1, details: 'Added mounted state to ThemeProvider' }); } } // Add 'use client' directive for client-only components if (fileExt === 'tsx' && updatedCode.includes('useTheme') && !updatedCode.includes("'use client'")) { updatedCode = "'use client';\n\n" + updatedCode; changeCount++; results.push({ type: 'client_directive', file: filePath, success: true, changes: 1, details: "Added 'use client' directive" }); } if (dryRun) { return { success: true, code: updatedCode, originalCode: code, changeCount, results }; } // Write changes if not dry-run if (changeCount > 0 && existsAsFile) { await fs.writeFile(filePath, updatedCode); results.push({ type: 'write', file: filePath, success: true, changes: changeCount }); } if (verbose && changeCount > 0) { process.stdout.write(`Layer 4 applied ${changeCount} changes to ${path.basename(filePath)}\n`); } return { success: results.every(r => r.success !== false), code: updatedCode, originalCode: code, changeCount, results }; } catch (error) { if (verbose) process.stderr.write(`Layer 4 failed: ${error.message}\n`); results.push({ type: 'error', file: filePath, success: false, error: error.message }); return { success: false, code, originalCode: code, changeCount: 0, results }; } } module.exports = { transform };