UNPKG

veendor

Version:

a tool for stroing your npm dependencies in arbitraty storage

328 lines (268 loc) 12.4 kB
'use strict'; const fsExtra = require('fs-extra'); const path = require('path'); const objectDiff = require('deep-object-diff'); const _ = require('lodash'); const assert = require('assert'); const pkgJsonUtils = require('../pkgjson'); const errors = require('../errors'); const gitWrapper = require('../commandWrappers/gitWrapper'); const npmWrapper = require('../commandWrappers/npmWrapper'); const pushBackends = require('./pushBackends'); const {createCleanCacheDir} = require('./helpers'); const resolveLockfile = require('../resolveLockfile'); const nodeModules = path.resolve(process.cwd(), 'node_modules'); const pkgJsonPath = path.resolve(process.cwd(), 'package.json'); const getLogger = require('../logger').getLogger; /** * @param {boolean} force - remove node_modules if exist * @param {Object} config * @param {boolean} rePull - if true, catching BundleAlreadyExistsError from backend will reject result. * Just to make sure, we won't fall into infinite loop here. * @param {null|string} lockfile - path to lockfile, detected at startup. null, if no lockfile detected * @returns Promise */ function install({force = false, config, rePull = false, lockfile = null}) { return new Promise((resolve, reject) => { const logger = getLogger(); let newPkgJson; let newPkgJsonHash; let newLockfileContents = null; let done = false; let missingBackends = []; checkNodeModules(force) .then(() => { const result = []; logger.trace('Reading package.json'); result.push(fsExtra.readFile(pkgJsonPath)); if (lockfile !== null) { logger.trace(`Reading ${lockfile}`); result.push(fsExtra.readFile(lockfile)); } return Promise.all(result); }) .then(([pkgJsonBuf, lockfileBuf]) => { const pkgJsonString = pkgJsonBuf.toString(); if (lockfileBuf) { newLockfileContents = lockfileBuf.toString(); } logger.trace('Parsing package.json'); return pkgJsonUtils.parsePkgJson(pkgJsonString); }) .then(pkgJson => { newPkgJson = pkgJson; logger.debug(`Got dependencies:\t${JSON.stringify(pkgJson.dependencies)}`); logger.debug(`Got devDependencies:\t${JSON.stringify(pkgJson.devDependencies)}`); logger.trace('Calculating hash'); newPkgJsonHash = pkgJsonUtils.calcHash(pkgJson, newLockfileContents, config.packageHash); logger.info(`Got hash:\t${newPkgJsonHash}`); return pullBackends(newPkgJsonHash, config).then((info) => { missingBackends = info.missingBackends; done = true; }); }) .catch(error => { if (error instanceof BundlesNotFoundError) { if (config.useGitHistory && config.useGitHistory.depth > 0) { logger.trace(`Looking in git history with depth ${config.useGitHistory.depth}`); return tryOlderBundles(config, newPkgJson, newPkgJsonHash, lockfile); } else if (config.fallbackToNpm === true) { return npmInstallAll(); } logger.error( `Couldn't find bundle with hash '${newPkgJsonHash}'. 'fallbackToNpm' isn't set. Exiting` ); done = true; throw error; } else if (error.code === 'ENOENT' && error.message.indexOf('package.json') !== -1) { throw new PkgJsonNotFoundError(error.message); } throw error; }) .catch(error => { if (error instanceof BundlesNotFoundError && config.fallbackToNpm === true) { return npmInstallAll(); } throw error; }) .then(() => { if (lockfile === null) { return resolveLockfile().then(resolvedLockfile => lockfile = resolvedLockfile); } }) .then(() => { if (lockfile === null) { return; } return fsExtra.readFile(lockfile) .then(lockfileBuf => { newLockfileContents = lockfileBuf.toString(); newPkgJsonHash = pkgJsonUtils.calcHash(newPkgJson, newLockfileContents, config.packageHash); logger.info(`New hash is:\t${newPkgJsonHash}`); }) }) .then(() => { if (done) { if (missingBackends.length > 0) { return pushBackends(missingBackends, newPkgJsonHash, config, rePull) .catch(error => { if (error instanceof errors.RePullNeeded) { return install({force: true, rePull: true, config}); } throw error; }); } return; } return pushBackends(config.backends, newPkgJsonHash, config, rePull) .catch(error => { if (error instanceof errors.RePullNeeded) { return install({force: true, rePull: true, config}); } throw error; }); }) .then(resolve, reject); }); } function tryOlderBundles(config, newPkgJson, newPkgJsonHash, lockfile = null) { return new Promise((resolve, reject) => { const logger = getLogger(); logger.info('Trying backends with older bundles'); return gitWrapper.isGitRepo(path.dirname(pkgJsonPath)) .then(() => { return pullOlderBundle(config, newPkgJsonHash, lockfile); }) .then(oldPkgJson => { return installDiff(oldPkgJson, newPkgJson); }) .then(resolve) .catch(reject); }); } function checkNodeModules(force) { return new Promise((resolve, reject) => { const logger = getLogger(); logger.trace('Checking node_modules'); fsExtra.access(nodeModules).then(() => { logger.trace('\'node_modules\' directory already exists'); if (force) { logger.info('Removing node_modules'); fsExtra.remove(nodeModules, error => { if (!error) { resolve(); } else { reject(error) } }); } else { reject(new NodeModulesAlreadyExistError); } }, resolve); }); } function pullBackends(hash, config, backendIndex = 0) { return new Promise((resolve, reject) => { const logger = getLogger(); if (!config.backends[backendIndex]) { return reject(new BundlesNotFoundError(`Backends don't have bundle ${hash}`)); } logger.info(`Trying backend '${config.backends[backendIndex].alias}' with hash ${hash}`); createCleanCacheDir(config.backends[backendIndex]).then( cacheDirPath => { logger.trace(`Cache directory for backend '${config.backends[backendIndex].alias}' is set`); return config.backends[backendIndex].backend .pull(hash, config.backends[backendIndex].options, cacheDirPath); }, reject ) .then( () => { logger.info(`Pulled ${hash} from backend '${config.backends[backendIndex].alias}'`); resolve({missingBackends: config.backends.slice(0, backendIndex)}); }, error => { if (error instanceof errors.BundleNotFoundError) { pullBackends(hash, config, backendIndex + 1).then(resolve, reject) } else { reject(error); } } ); }); } function pullOlderBundle(config, prevHash, lockfile = null, historyIndex = 0) { return new Promise((resolve, reject) => { const logger = getLogger(); let oldPkgJson; let newHash = prevHash; let newLockfileContents = null; if (historyIndex > config.useGitHistory.depth) { return reject(new BundlesNotFoundError(`Backends don't have bundles up to ${config.useGitHistory.depth} entries in git history of ${pkgJsonPath}`)); } gitWrapper.olderRevision(process.cwd(), [pkgJsonPath, lockfile], historyIndex + 1) .then(([pkgJsonString, lockfileString]) => { newLockfileContents = lockfileString; return pkgJsonUtils.parsePkgJson(pkgJsonString); }) .then(pkgJson => { oldPkgJson = pkgJson; logger.trace('Calculating hash'); const hash = pkgJsonUtils.calcHash(pkgJson, newLockfileContents, config.packageHash); if (hash === newHash) { const message = `Hash at index '${historyIndex}' is still '${hash}'. Incrementing history depth`; logger.trace(message); config.useGitHistory.depth++; throw new Error(message); } newHash = hash; logger.info(`Got older hash: ${hash}`); return pullBackends(hash, config); }) .then( () => {resolve(oldPkgJson)}, () => {pullOlderBundle(config, newHash, lockfile, historyIndex + 1).then(resolve, reject)} ); }) } function installDiff(oldPkgJson, newPkgJson) { return new Promise((resolve, reject) => { const logger = getLogger(); const allDepsOld = Object.assign({}, oldPkgJson.devDependencies, oldPkgJson.dependencies); const allDepsNew = Object.assign({}, newPkgJson.devDependencies, newPkgJson.dependencies); const depsDiff = objectDiff.diff(allDepsOld, allDepsNew); const depsToInstall = _.omitBy(depsDiff, _.isUndefined); const depsToUninstall = _.keys(_.pickBy(depsDiff, _.isUndefined)); const loggingDepsToInstall = 'Installing dependencies: ' + Object.keys(depsToInstall).map(pkg => `${pkg}@${depsToInstall[pkg]}`).join(' '); const loggingDepsToUninstall = 'Uninstalling dependencies: ' + depsToUninstall.join(' '); if (_.keys(depsToInstall).length) { logger.info(loggingDepsToInstall); return npmWrapper.install(depsToInstall) .then(() => { if (depsToUninstall.length) { logger.info(loggingDepsToUninstall); return npmWrapper.uninstall(depsToUninstall).then(resolve, reject); } resolve(); }, reject); } else if (depsToUninstall.length) { logger.info(loggingDepsToUninstall); return npmWrapper.uninstall(depsToUninstall).then(resolve, reject); } assert(false, 'Unreachable. install#installDiff is called without proper diff to install'); }); } function npmInstallAll() { const logger = getLogger(); logger.info('Couldn\'t find bundles. Running npm install'); return npmWrapper.installAll(); } class PkgJsonNotFoundError extends Error {} class NodeModulesAlreadyExistError extends Error {} class BundlesNotFoundError extends Error {} module.exports = install; module.exports.NodeModulesAlreadyExistError = NodeModulesAlreadyExistError; module.exports.BundlesNotFoundError = BundlesNotFoundError; module.exports.PkgJsonNotFoundError = PkgJsonNotFoundError;