File size: 5,258 Bytes
d613519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * 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
};