The function has been renamed to clearRequireCache and updated to recursively clear the require cache for a module and all its dependencies, preventing cycles with a visited set. This improves reliability after rebuilding native modules by ensuring complete cache invalidation. Removed unused createRequire import as it's no longer needed.
163 lines
5.1 KiB
JavaScript
163 lines
5.1 KiB
JavaScript
/**
|
|
* 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
|
|
};
|