@neurolint/cli
Version:
Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations
177 lines (162 loc) • 6.08 kB
JavaScript
#!/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 };