Skip to content

Instantly share code, notes, and snippets.

@enten
Created October 10, 2022 14:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enten/07ac10870edf64eb31dead6fa65af42a to your computer and use it in GitHub Desktop.
Save enten/07ac10870edf64eb31dead6fa65af42a to your computer and use it in GitHub Desktop.
Install npm dependencies in a lazy-dumb-one-by-one way
// Copyright (c) 2022 Steven Enten. All rights reserved. Licensed under the MIT license.
/**
* Install npm dependencies in a lazy-dumb-one-by-one way
*
* Usage: node npm-install-dumber.js [<package-name|package-type> ...]
*
* If `npm install` never succeeds: try to run this script as much as necessary.
*
* @example
* ```shell
* # Install all dependencies
* node npm-install-dumber.js
*
* # Install dependencies only
* node npm-install-dumber.js dependencies
*
* # Install dev dependencies
* node npm-install-dumber.js devDependencies
*
* # Install optional dependencies
* node npm-install-dumber.js optionalDependencies
* ```
*
* @remarks
*
* Why use it?
*
* When a package.json has lot of dependencies, the first `npm install` will take a long time.
* On hostile networks, long install might fails, and when it happens all staging packages are lost.
* If you fall into an infernal failure install loop: give this script a chance to break it.
*
* How it works?
*
* 1. Check path ./node_modules/<package-name>/package.json of each known dependency
* 2. Compute the list of installed packages
* 3. Generate a package.json which specifiy only installed dependencies
* 4. Run npm install for each uninstalled dependency
* 5. Restore original package.json
*
*/
const childProcess = require('child_process');
const fs = require('fs');
const path = require('path');
const DEP_TYPE_INSTALL_FLAGS = {
dependencies: '--save',
devDependencies: '--save-dev',
optionalDependencies: '--save-optional',
};
const SHUTDOWN_EVENTS = ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException'];
function npmInstallDumber(proc = process) {
let workdir = proc.cwd();
let pkgJsonPath;
for (const pathsTested = []; !pathsTested.includes(workdir); workdir = path.join(workdir, '..')) {
pathsTested.push(workdir);
const mayPkgJsonPath = path.join(workdir, 'package.json');
if (fs.existsSync(mayPkgJsonPath)) {
pkgJsonPath = mayPkgJsonPath;
break;
}
}
if (!pkgJsonPath) {
console.error('package.json not found');
proc.exit(1);
}
proc.chdir(workdir);
const nodeModulesPath = path.join(workdir, 'node_modules');
const nodeModulesExists = fs.existsSync(nodeModulesPath);
const pkgJsonAsString = fs.readFileSync(pkgJsonPath, 'utf-8');
const pkgJson = JSON.parse(pkgJsonAsString);
const partialPkgJson = { ...pkgJson };
const allPackages = Object.keys(DEP_TYPE_INSTALL_FLAGS).reduce((acc, depType) => {
partialPkgJson[depType] = {};
Object.keys(pkgJson[depType] || {}).forEach(depName => {
const depPkgJsonPath = path.join(nodeModulesPath, depName, 'package.json');
const depInstalled = nodeModulesExists && fs.existsSync(depPkgJsonPath);
const depVersion = pkgJson[depType][depName];
acc[depName] = {
type: depType,
version: depVersion,
installed: depInstalled,
};
if (depInstalled) {
partialPkgJson[depType][depName] = depVersion;
}
});
return acc;
}, {});
const depsUnknown = [];
let depsToInstall = proc.argv.slice(2);
if (!depsToInstall.length) {
depsToInstall = Object.keys(allPackages);
} else {
depsToInstall = depsToInstall.reduce((acc, dep) => {
if (dep in DEP_TYPE_INSTALL_FLAGS) {
acc.push(...Object.keys(pkgJson[dep] || {}));
} else if (dep in allPackages) {
acc.push(dep);
} else {
depsUnknown.push(dep);
}
return acc;
}, []);
}
depsUnknown.forEach(depName => console.warn(`WARN | unknown dependency: ${depName}`));
depsToInstall = depsToInstall.filter(depName => !allPackages[depName].installed);
if (!depsToInstall.length) {
console.log('nothing to do');
proc.exit(0);
}
const operationTitle = [
'install',
depsToInstall.length,
`dependenc${depsToInstall.length === 1 ? 'y' : 'ies'}:`,
depsToInstall.join(' '),
].join(' ');
console.log(`INFO | start: ${operationTitle}`);
console.log('DEBUG | --');
console.log(`DEBUG | workdir=${workdir}`);
console.log(`DEBUG | pkgJsonPath=${workdir}`);
let shutdownHandlerAlreadyCalled = false;
const shutdownHookHandler = (shutdownEvent, x) => {
if (shutdownHandlerAlreadyCalled) {
return;
}
shutdownHandlerAlreadyCalled = true;
try {
if (x) {
console.error(`ERROR | ${shutdownEvent}`, x);
} else {
console.log(`INFO | ${shutdownEvent}`, x);
}
console.log('INFO | restore package.json');
fs.writeFileSync(pkgJsonPath, pkgJsonAsString, 'utf-8');
console.log('INFO | done');
} catch (err) {
console.error('ERROR | an error occurred during call of shutdown function:');
console.error(err);
proc.exit(99);
}
};
SHUTDOWN_EVENTS.forEach(shutdownEvent => {
proc.on(shutdownEvent, x => shutdownHookHandler(shutdownEvent, x));
});
fs.writeFileSync(pkgJsonPath, JSON.stringify(partialPkgJson, null, 2), 'utf-8');
const installTotalCount = depsToInstall.length;
let installCounter = 0;
for (const depName of depsToInstall) {
installCounter++;
const { version: depVersion, type: depType } = allPackages[depName];
const depInstallFlag = DEP_TYPE_INSTALL_FLAGS[depType];
const npmCommand = ['npm', 'install', `${depName}@${depVersion}`, depInstallFlag].join(' ');
console.log('DEBUG | --');
console.log(`INFO | (${installCounter}/${installTotalCount}) \$ ${npmCommand}`);
childProcess.execSync(npmCommand, { stdio: 'inherit' });
}
console.log('DEBUG | --');
console.log(`INFO | end: ${operationTitle}`);
}
if (require.main === module) {
try {
npmInstallDumber(process);
} catch (err) {
console.error(err);
process.exit(1);
}
}
module.exports = { npmInstallDumber };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment