Created
October 10, 2022 14:46
-
-
Save enten/07ac10870edf64eb31dead6fa65af42a to your computer and use it in GitHub Desktop.
Install npm dependencies in a lazy-dumb-one-by-one way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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