UNPKG

install-module-linked

Version:
92 lines (91 loc) 4.53 kB
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)); }); } }); }