UNPKG

node-linker-pro

Version:

Keep node_modules out of cloud sync folders by linking them to a local cache. Cross-platform. Includes global npm setup command.

118 lines (98 loc) 5.15 kB
#!/usr/bin/env node import fsp from 'fs/promises'; import fs from 'fs'; import path from 'path'; import os from 'os'; import crypto from 'crypto'; import { spawn } from 'child_process'; import chalk from 'chalk'; const isWin = process.platform === 'win32'; // --- Helper Functions --- function fail(msg) { console.error(msg); process.exit(1); } async function exists(p) { try { await fsp.lstat(p); return true; } catch { return false; } } async function isLinkTo(linkPath, targetPath) { try { if (!(await fsp.lstat(linkPath)).isSymbolicLink()) return false; const resolvedLink = path.resolve(path.dirname(linkPath), await fsp.readlink(linkPath)); const resolvedTarget = path.resolve(targetPath); return isWin ? resolvedLink.toLowerCase() === resolvedTarget.toLowerCase() : resolvedLink === resolvedTarget; } catch { return false; } } async function findProjectRoot(startDir) { let currentDir = startDir; while (true) { if (await exists(path.join(currentDir, 'package.json'))) return currentDir; const parentDir = path.dirname(currentDir); if (parentDir === currentDir) return null; currentDir = parentDir; } } function defaultStoreDir() { if (isWin) { const base = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"); return path.join(base, "node_modules_store"); } if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Caches", "node_modules_store"); return path.join(os.homedir(), ".cache", "node_modules_store"); } // --- Main Logic --- async function main() { const projectDir = await findProjectRoot(process.cwd()); if (!projectDir) fail(chalk.red("❌ No `package.json` found.")); const nmLinkPath = path.join(projectDir, 'node_modules'); const pkgPath = path.join(projectDir, 'package.json'); const lockPath = path.join(projectDir, 'package-lock.json'); const storeRoot = defaultStoreDir(); const hash = crypto.createHash('sha1').update(projectDir).digest('hex'); const cacheProjDir = path.join(storeRoot, hash); const cacheNodeModules = path.join(cacheProjDir, 'node_modules'); console.log(chalk.blue(`Project: ${projectDir}`)); console.log(chalk.blue(`Cache: ${cacheProjDir}`)); await fsp.mkdir(cacheNodeModules, { recursive: true }); if (!(await isLinkTo(nmLinkPath, cacheNodeModules))) { console.log(chalk.yellow('Creating junction for node_modules...')); await fsp.rm(nmLinkPath, { recursive: true, force: true }).catch(() => {}); await fsp.symlink(cacheNodeModules, nmLinkPath, 'junction'); if (!(await isLinkTo(nmLinkPath, cacheNodeModules))) fail(chalk.red('❌ Failed to create or verify the node_modules junction.')); console.log(chalk.green('✅ Linked project node_modules to cache.')); } else { console.log(chalk.green('✅ Project node_modules is already linked correctly.')); } console.log(chalk.yellow('Syncing manifests to cache...')); await fsp.copyFile(pkgPath, path.join(cacheProjDir, 'package.json')); if (await exists(lockPath)) { await fsp.copyFile(lockPath, path.join(cacheProjDir, 'package-lock.json')); } await runNpmInstallInCache(cacheProjDir); console.log(chalk.bold.green('\n🎉 Success! Dependencies are installed in the external cache.')); console.log(chalk.cyan(`--> To run your project, use 'npm run dev' as usual.`)); console.log(chalk.cyan(`--> To install/update dependencies in the future, just run 'nlp' again.`)); } function runNpmInstallInCache(cacheProjDir) { return new Promise((resolve, reject) => { const hasLock = fs.existsSync(path.join(cacheProjDir, 'package-lock.json')); const npmAction = hasLock ? 'ci' : 'install'; const npmInstallArgs = [npmAction, '--prefix', cacheProjDir, '--no-audit', '--no-fund']; const command = isWin ? 'cmd.exe' : 'npm'; const args = isWin ? ['/c', 'npm', ...npmInstallArgs] : npmInstallArgs; console.log(chalk.yellow(`\nRunning: ${chalk.cyan(`${isWin ? 'cmd /c npm' : 'npm'} ${npmInstallArgs.join(' ')}`)}\n`)); // THE FIX FOR HANGING: // We use stdio: 'pipe' and manually drain the output buffers. const p = spawn(command, args, { stdio: 'pipe' }); p.stdout.on('data', (data) => process.stdout.write(data)); p.stderr.on('data', (data) => process.stderr.write(data)); p.on('close', (code) => { if (code === 0) { console.log('\n'); // Final newline for clean output resolve(); } else { reject(new Error(`Installation failed with exit code ${code}.`)); } }); p.on('error', (err) => { reject(new Error(`Failed to start subprocess: ${err.message}`)); }); }); } // --- Main Execution --- main().catch(err => fail(chalk.red(`\n❌ Operation failed: ${err.message}`)));