/** * Native Module Helper * Detects and auto-rebuilds native Node.js modules when they become * incompatible after a Node.js version update. */ import { execSync } from 'child_process'; import { dirname, join } from 'path'; import { existsSync } from 'fs'; import { logger } from './logger.js'; /** * Check if an error is a NODE_MODULE_VERSION mismatch error * @param {Error} error - The error to check * @returns {boolean} True if it's a version mismatch error */ export function isModuleVersionError(error) { const message = error?.message || ''; return message.includes('NODE_MODULE_VERSION') && message.includes('was compiled against a different Node.js version'); } /** * Extract the module path from a NODE_MODULE_VERSION error message * @param {Error} error - The error containing the module path * @returns {string|null} The path to the .node file, or null if not found */ export function extractModulePath(error) { const message = error?.message || ''; // Match pattern like: "The module '/path/to/module.node'" const match = message.match(/The module '([^']+\.node)'/); return match ? match[1] : null; } /** * Find the package root directory from a .node file path * @param {string} nodeFilePath - Path to the .node file * @returns {string|null} Path to the package root, or null if not found */ export function findPackageRoot(nodeFilePath) { // Walk up from the .node file to find package.json let dir = dirname(nodeFilePath); while (dir) { const packageJsonPath = join(dir, 'package.json'); if (existsSync(packageJsonPath)) { return dir; } const parentDir = dirname(dir); // Stop when we've reached the filesystem root (dirname returns same path) if (parentDir === dir) { break; } dir = parentDir; } return null; } /** * Attempt to rebuild a native module * @param {string} packagePath - Path to the package root directory * @returns {boolean} True if rebuild succeeded, false otherwise */ export function rebuildModule(packagePath) { try { logger.info(`[NativeModule] Rebuilding native module at: ${packagePath}`); // Run npm rebuild in the package directory const output = execSync('npm rebuild', { cwd: packagePath, stdio: 'pipe', // Capture output instead of printing timeout: 120000 // 2 minute timeout }); // Log rebuild output for debugging const outputStr = output?.toString().trim(); if (outputStr) { logger.debug(`[NativeModule] Rebuild output:\n${outputStr}`); } logger.success('[NativeModule] Rebuild completed successfully'); return true; } catch (error) { // Include stdout/stderr from the failed command for troubleshooting const stdout = error.stdout?.toString().trim(); const stderr = error.stderr?.toString().trim(); let errorDetails = `[NativeModule] Rebuild failed: ${error.message}`; if (stdout) { errorDetails += `\n[NativeModule] stdout: ${stdout}`; } if (stderr) { errorDetails += `\n[NativeModule] stderr: ${stderr}`; } logger.error(errorDetails); return false; } } /** * Attempt to auto-rebuild a native module from an error * @param {Error} error - The NODE_MODULE_VERSION error * @returns {boolean} True if rebuild succeeded, false otherwise */ export function attemptAutoRebuild(error) { const nodePath = extractModulePath(error); if (!nodePath) { logger.error('[NativeModule] Could not extract module path from error'); return false; } const packagePath = findPackageRoot(nodePath); if (!packagePath) { logger.error('[NativeModule] Could not find package root'); return false; } logger.warn('[NativeModule] Native module version mismatch detected'); logger.info('[NativeModule] Attempting automatic rebuild...'); return rebuildModule(packagePath); } /** * Recursively clear a module and its dependencies from the require cache * This is needed after rebuilding a native module to force re-import * @param {string} modulePath - Resolved path to the module * @param {object} cache - The require.cache object * @param {Set} [visited] - Set of already-visited paths to prevent cycles */ export function clearRequireCache(modulePath, cache, visited = new Set()) { if (visited.has(modulePath)) return; visited.add(modulePath); const mod = cache[modulePath]; if (!mod) return; // Recursively clear children first if (mod.children) { for (const child of mod.children) { clearRequireCache(child.id, cache, visited); } } // Remove from parent's children array if (mod.parent && mod.parent.children) { const idx = mod.parent.children.indexOf(mod); if (idx !== -1) { mod.parent.children.splice(idx, 1); } } // Delete from cache delete cache[modulePath]; } export default { isModuleVersionError, extractModulePath, findPackageRoot, rebuildModule, attemptAutoRebuild, clearRequireCache };