/* Description: This script manages NPM dependencies for a project. It offers capabilities like doing a fresh install, retries on network errors, and other features. Usage: npm run install-deps [-- ] node scripts/npm-install.js [options] Options: --root-directory Specifies the root directory where package.json resides Defaults to the current working directory. Example: npm run install-deps -- --root-directory /your/path/here --no-errors Ignores errors and continues the execution. Example: npm run install-deps -- --no-errors --ci Uses 'npm ci' for dependency installation instead of 'npm install'. Example: npm run install-deps -- --ci --fresh Removes the existing node_modules directory before installing dependencies. Example: npm run install-deps -- --fresh --non-deterministic Removes package-lock.json for a non-deterministic installation. Example: npm run install-deps -- --non-deterministic Note: Flags can be combined as needed. Example: npm run install-deps -- --fresh --non-deterministic */ import { exec } from 'node:child_process'; import { resolve } from 'node:path'; import { access, rm, unlink } from 'node:fs/promises'; import { constants } from 'node:fs'; const MAX_RETRIES = 5; const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000; const ARG_NAMES = { rootDirectory: '--root-directory', ignoreErrors: '--no-errors', ci: '--ci', fresh: '--fresh', nonDeterministic: '--non-deterministic', }; async function main() { const options = getOptions(); console.log('Options:', options); await ensureNpmRootDirectory(options.rootDirectory); await ensureNpmIsAvailable(); if (options.fresh) { await removeNodeModules(options.rootDirectory); } if (options.nonDeterministic) { await removePackageLockJson(options.rootDirectory); } const command = buildCommand(options.ci, options.outputErrors); console.log('Starting dependency installation...'); const exitCode = await executeWithRetry( command, options.workingDirectory, MAX_RETRIES, RETRY_DELAY_IN_MS, ); if (exitCode === 0) { console.log('🎊 Installed dependencies...'); } else { console.error(`💀 Failed to install dependencies, exit code: ${exitCode}`); } process.exit(exitCode); } async function removeNodeModules(workingDirectory) { const nodeModulesDirectory = resolve(workingDirectory, 'node_modules'); if (await exists('./node_modules')) { console.log('Removing node_modules...'); await rm(nodeModulesDirectory, { recursive: true }); } } async function removePackageLockJson(workingDirectory) { const packageLockJsonFile = resolve(workingDirectory, 'package-lock.json'); if (await exists(packageLockJsonFile)) { console.log('Removing package-lock.json...'); await unlink(packageLockJsonFile); } } async function ensureNpmIsAvailable() { const exitCode = await executeCommand('npm --version'); if (exitCode !== 0) { throw new Error('`npm` in not available!'); } } async function ensureNpmRootDirectory(workingDirectory) { const packageJsonPath = resolve(workingDirectory, 'package.json'); if (!await exists(packageJsonPath)) { throw new Error(`Not an NPM project root: ${workingDirectory}`); } } function buildCommand(ci, outputErrors) { const baseCommand = ci ? 'npm ci' : 'npm install'; if (!outputErrors) { return `${baseCommand} --loglevel=error`; } return baseCommand; } function getOptions() { const processArgs = process.argv.slice(2); // Slice off the node and script name return { rootDirectory: processArgs.includes('--root-directory') ? processArgs[processArgs.indexOf('--root-directory') + 1] : process.cwd(), outputErrors: !processArgs.includes(ARG_NAMES.ignoreErrors), ci: processArgs.includes(ARG_NAMES.ci), fresh: processArgs.includes(ARG_NAMES.fresh), nonDeterministic: processArgs.includes(ARG_NAMES.nonDeterministic), }; } async function executeWithRetry( command, workingDirectory, maxRetries, retryDelayInMs, currentAttempt = 1, ) { const statusCode = await executeCommand(command, workingDirectory, true, true); if (statusCode === 0 || currentAttempt >= maxRetries) { return statusCode; } console.log(`⚠️🔄 Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`); await sleep(retryDelayInMs); const retryResult = await executeWithRetry( command, workingDirectory, maxRetries, retryDelayInMs, currentAttempt + 1, ); return retryResult; } async function executeCommand( command, workingDirectory = process.cwd(), logStdout = false, logCommand = false, ) { if (logCommand) { console.log(`▶️ Executing command "${command}" at "${workingDirectory}"`); } const process = exec( command, { cwd: workingDirectory, }, ); if (logStdout) { process.stdout.on('data', (data) => { console.log(data.toString()); }); } process.stderr.on('data', (data) => { console.error(data.toString()); }); return new Promise((resolve) => { process.on('exit', (code) => { resolve(code); }); }); } function sleep(milliseconds) { return new Promise((resolve) => { setTimeout(resolve, milliseconds); }); } async function exists(path) { try { await access(path, constants.F_OK); return true; } catch { return false; } } await main();