Files
pnpm/src/api/install.ts
2017-02-02 23:49:27 +02:00

275 lines
8.7 KiB
TypeScript

import rimraf = require('rimraf-then')
import path = require('path')
import seq = require('promisequence')
import RegClient = require('npm-registry-client')
import logger from 'pnpm-logger'
import cloneDeep = require('lodash.clonedeep')
import globalBinPath = require('global-bin-path')
import {PnpmOptions, StrictPnpmOptions, Dependencies} from '../types'
import createGot from '../network/got'
import getContext, {PnpmContext} from './getContext'
import installMultiple, {InstalledPackage} from '../install/installMultiple'
import save from '../save'
import linkPeers from '../install/linkPeers'
import runtimeError from '../runtimeError'
import getSaveType from '../getSaveType'
import {sync as runScriptSync} from '../runScript'
import postInstall from '../install/postInstall'
import extendOptions from './extendOptions'
import pnpmPkgJson from '../pnpmPkgJson'
import lock from './lock'
import {save as saveGraph, Graph} from '../fs/graphController'
import {read as readStore, save as saveStore} from '../fs/storeController'
import {save as saveShrinkwrap, Shrinkwrap} from '../fs/shrinkwrap'
import {save as saveModules} from '../fs/modulesController'
import {tryUninstall, removePkgFromStore} from './uninstall'
import mkdirp from '../fs/mkdirp'
import {CachedPromises} from '../memoize'
import linkBins from '../install/linkBins'
export type InstalledPackages = {
[name: string]: InstalledPackage
}
export type InstallContext = {
installs: InstalledPackages,
installationSequence: string[],
fetchLocks: CachedPromises<void>,
graph: Graph,
shrinkwrap: Shrinkwrap,
resolutionLinked: CachedPromises<void>,
installed: Set<string>,
}
export async function install (maybeOpts?: PnpmOptions) {
const opts = extendOptions(maybeOpts)
const ctx = await getContext(opts)
const installCtx = await createInstallCmd(opts, ctx.graph, ctx.shrinkwrap)
if (!ctx.pkg) throw runtimeError('No package.json found')
const packagesToInstall = Object.assign({}, ctx.pkg.dependencies || {})
if (!opts.production) Object.assign(packagesToInstall, ctx.pkg.devDependencies || {})
return lock(ctx.storePath, () => installInContext('general', packagesToInstall, ctx, installCtx, opts))
}
/**
* Perform installation.
*
* @example
* install({'lodash': '1.0.0', 'foo': '^2.1.0' }, { silent: true })
*/
export async function installPkgs (fuzzyDeps: string[] | Dependencies, maybeOpts?: PnpmOptions) {
let packagesToInstall = mapify(fuzzyDeps)
if (!Object.keys(packagesToInstall).length) {
throw new Error('At least one package has to be installed')
}
const opts = extendOptions(maybeOpts)
const ctx = await getContext(opts)
const installCtx = await createInstallCmd(opts, ctx.graph, ctx.shrinkwrap)
return lock(ctx.storePath, () => installInContext('named', packagesToInstall, ctx, installCtx, opts))
}
async function installInContext (installType: string, packagesToInstall: Dependencies, ctx: PnpmContext, installCtx: InstallContext, opts: StrictPnpmOptions) {
// TODO: ctx.graph should not be muted. installMultiple should return a new graph
const oldGraph: Graph = cloneDeep(ctx.graph)
const nodeModulesPath = path.join(ctx.root, 'node_modules')
const client = new RegClient(adaptConfig(opts))
const installOpts = {
linkLocal: opts.linkLocal,
dependent: ctx.root,
root: ctx.root,
storePath: ctx.storePath,
force: opts.force,
depth: opts.depth,
tag: opts.tag,
engineStrict: opts.engineStrict,
nodeVersion: opts.nodeVersion,
got: createGot(client, {networkConcurrency: opts.networkConcurrency}),
fetchingFiles: Promise.resolve(),
baseNodeModules: nodeModulesPath,
metaCache: opts.metaCache,
}
const pkgs: InstalledPackage[] = await installMultiple(
installCtx,
packagesToInstall,
ctx.pkg && ctx.pkg && ctx.pkg.optionalDependencies || {},
nodeModulesPath,
installOpts
)
const binPath = opts.global ? globalBinPath() : path.join(nodeModulesPath, '.bin')
await linkBins(nodeModulesPath, binPath)
if (installType === 'named') {
const saveType = getSaveType(opts)
if (saveType) {
if (!ctx.pkg) {
throw new Error('Cannot save because no package.json found')
}
const inputNames = Object.keys(packagesToInstall)
const savedPackages = pkgs.filter((pkg: InstalledPackage) => inputNames.indexOf(pkg.pkg.name) > -1)
const pkgJsonPath = path.join(ctx.root, 'package.json')
await save(pkgJsonPath, savedPackages, saveType, opts.saveExact)
}
}
const newGraph = Object.assign({}, ctx.graph)
await removeOrphanPkgs(oldGraph, newGraph, ctx.root, ctx.storePath)
await saveGraph(path.join(ctx.root, 'node_modules'), newGraph)
await saveShrinkwrap(ctx.root, ctx.shrinkwrap)
if (ctx.isFirstInstallation) {
await saveModules(path.join(ctx.root, 'node_modules'), {
packageManager: `${pnpmPkgJson.name}@${pnpmPkgJson.version}`,
storePath: ctx.storePath,
})
}
await linkPeers(installCtx.installs)
// postinstall hooks
if (!(opts.ignoreScripts || !installCtx.installationSequence || !installCtx.installationSequence.length)) {
await seq(
installCtx.installationSequence.map(async pkgId => {
try {
await postInstall(installCtx.installs[pkgId].hardlinkedLocation, installLogger(pkgId))
} catch (err) {
if (installCtx.installs[pkgId].optional) {
logger.warn({
message: `Skipping failed optional dependency ${pkgId}`,
err,
})
return
}
throw err
}
})
)
}
if (!opts.ignoreScripts && ctx.pkg) {
const scripts = ctx.pkg && ctx.pkg.scripts || {}
if (scripts['postinstall']) {
npmRun('postinstall', ctx.root)
}
if (installType === 'general' && scripts['prepublish']) {
npmRun('prepublish', ctx.root)
}
}
}
async function removeOrphanPkgs (oldGraphJson: Graph, newGraphJson: Graph, root: string, storePath: string) {
const oldPkgIds = new Set(Object.keys(oldGraphJson))
const newPkgIds = new Set(Object.keys(newGraphJson))
const store = await readStore(storePath) || {}
const notDependents = difference(oldPkgIds, newPkgIds)
await Promise.all(Array.from(notDependents).map(async function (notDependent) {
if (store[notDependent]) {
store[notDependent].splice(store[notDependent].indexOf(root), 1)
if (!store[notDependent].length) {
delete store[notDependent]
await rimraf(path.join(storePath, notDependent))
}
}
}))
const newDependents = difference(newPkgIds, oldPkgIds)
newDependents.forEach(newDependent => {
store[newDependent] = store[newDependent] || []
if (store[newDependent].indexOf(root) === -1) {
store[newDependent].push(root)
}
})
await saveStore(storePath, store)
}
function difference<T> (setA: Set<T>, setB: Set<T>) {
const difference = new Set(setA)
for (const elem of setB) {
difference.delete(elem)
}
return difference
}
async function createInstallCmd (opts: StrictPnpmOptions, graph: Graph, shrinkwrap: Shrinkwrap): Promise<InstallContext> {
return {
fetchLocks: {},
installLocks: {},
installs: {},
graph,
shrinkwrap,
resolutionLinked: {},
installed: new Set(),
installationSequence: [],
}
}
function adaptConfig (opts: StrictPnpmOptions) {
const registryLog = logger('registry')
return {
proxy: {
http: opts.proxy,
https: opts.httpsProxy,
localAddress: opts.localAddress
},
ssl: {
certificate: opts.cert,
key: opts.key,
ca: opts.ca,
strict: opts.strictSsl
},
retry: {
count: opts.fetchRetries,
factor: opts.fetchRetryFactor,
minTimeout: opts.fetchRetryMintimeout,
maxTimeout: opts.fetchRetryMaxtimeout
},
userAgent: opts.userAgent,
log: Object.assign({}, registryLog, {
verbose: registryLog.debug.bind(null, 'http'),
http: registryLog.debug.bind(null, 'http'),
}),
defaultTag: opts.tag
}
}
function npmRun (scriptName: string, pkgRoot: string) {
const result = runScriptSync('npm', ['run', scriptName], {
cwd: pkgRoot,
stdio: 'inherit'
})
if (result.status !== 0) {
process.exit(result.status)
}
}
const lifecycleLogger = logger('lifecycle')
function installLogger (pkgId: string) {
return (stream: string, line: string) => {
const logLevel = stream === 'stderr' ? 'error' : 'info'
lifecycleLogger[logLevel]({pkgId, line})
}
}
function mapify (pkgs: string[] | Dependencies): Dependencies {
if (!pkgs) return {}
if (Array.isArray(pkgs)) {
return pkgs.reduce((pkgsMap: Dependencies, pkgRequest: string) => {
const matches = /(@?[^@]+)@(.*)/.exec(pkgRequest)
if (!matches) {
pkgsMap[pkgRequest] = '*'
} else {
pkgsMap[matches[1]] = matches[2]
}
return pkgsMap
}, {})
}
return pkgs
}