install-module-linked
Version:
Installs and symlinks a module into node_modules
92 lines (91 loc) • 4.53 kB
JavaScript
import fs from 'fs';
import { safeRm } from 'fs-remove-compat';
import lockfile from 'lockfile';
import mkdirp from 'mkdirp-classic';
import path from 'path';
import Queue from 'queue-cb';
import tempSuffix from 'temp-suffix';
import getSpecifier from './getSpecifier.js';
import install from './install.js';
import parse from './parseInstallString.js';
import renameWithFallback from './renameWithFallback.js';
const LOCK_OPTIONS = {
wait: 600000,
pollPeriod: 100,
stale: 660000,
retries: 3
};
export default function ensureCached(installString, cachePath, callback) {
getSpecifier(installString, (err, specifier)=>{
if (err) return callback(err);
const cachedAt = path.join(cachePath, specifier);
const lockPath = `${cachedAt}.lock`;
const readyPath = path.join(cachedAt, '.ready');
const { name } = parse(installString);
// Check if already cached AND complete (fast path, no lock needed)
fs.stat(readyPath, (readyErr)=>{
if (!readyErr) return callback(undefined, cachedAt); // Valid cache
// Not cached or incomplete - acquire lock and install
doLockedInstall();
});
function doLockedInstall() {
// Ensure lock file's parent directory exists (handles scoped packages like @scope/pkg)
mkdirp(path.dirname(lockPath), (mkdirErr)=>{
if (mkdirErr) return callback(mkdirErr);
const startTime = Date.now();
// Retry loop for Windows EPERM - treat it like EEXIST (lock held by another process)
function tryLock(cb) {
lockfile.lock(lockPath, {
...LOCK_OPTIONS
}, (lockErr)=>{
if (lockErr && lockErr.code === 'EPERM' && Date.now() - startTime < LOCK_OPTIONS.wait) {
setTimeout(()=>tryLock(cb), LOCK_OPTIONS.pollPeriod);
return;
}
cb(lockErr);
});
}
tryLock((lockErr)=>{
if (lockErr) return callback(lockErr);
// Single exit point - always unlock before calling back
function done(err) {
lockfile.unlock(lockPath, ()=>callback(err, err ? undefined : cachedAt));
}
// Re-check cache (another process may have completed while we waited for lock)
fs.stat(readyPath, (readyErr)=>{
if (!readyErr) return done(); // Cache appeared while we waited
// Clean up any incomplete cache before installing
safeRm(cachedAt, (rmErr)=>{
if (rmErr) return done(rmErr);
doInstall(done);
});
});
});
});
}
function doInstall(cb) {
const tmp = `${cachedAt}-${tempSuffix()}`;
const tmpModulePath = path.join(tmp, 'node_modules', ...name.split('/'));
const queue = new Queue(1);
queue.defer((cb)=>mkdirp(tmp, (err)=>cb(err)));
queue.defer((cb)=>fs.writeFile(path.join(tmp, 'package.json'), '{}', 'utf8', (err)=>cb(err)));
queue.defer(install.bind(null, specifier, tmp));
queue.defer((qcb)=>{
// Verify npm actually created the package directory - npm may silently skip
// installation (exit 0) when platform doesn't match (os/cpu/libc fields)
fs.stat(tmpModulePath, (statErr)=>{
if (statErr) return qcb(new Error(`Package directory not created by npm. This may happen when the package has platform restrictions (os/cpu/libc) that don't match the current system: ${tmpModulePath}`));
qcb();
});
});
queue.defer(renameWithFallback.bind(null, tmpModulePath, cachedAt));
queue.defer(renameWithFallback.bind(null, path.join(tmp, 'node_modules'), path.join(cachedAt, 'node_modules')));
// Write .ready marker after both renames succeed
queue.defer((cb)=>fs.writeFile(readyPath, '', 'utf8', (err)=>cb(err)));
queue.await((queueErr)=>{
// Clean up temp directory whether installed or not
safeRm(tmp, ()=>cb(queueErr));
});
}
});
}