Files
pnpm/src/install/installMultiple.ts
T

409 lines
12 KiB
TypeScript

import path = require('path')
import npa = require('npm-package-arg')
import logger from 'pnpm-logger'
import pFilter = require('p-filter')
import fetch, {FetchedPackage} from './fetch'
import {InstallContext, InstalledPackages} from '../api/install'
import {Dependencies} from '../types'
import memoize from '../memoize'
import {Package} from '../types'
import linkDir from 'link-dir'
import installChecks = require('pnpm-install-checks')
import pnpmPkg from '../pnpmPkgJson'
import symlinkDir from 'symlink-dir'
import exists = require('exists-file')
import {Graph, GRAPH_ENTRY} from '../fs/graphController'
import logStatus from '../logging/logInstallStatus'
import rimraf = require('rimraf-then')
import fs = require('mz/fs')
import {PackageMeta} from '../resolve/utils/loadPackageMeta'
import {Got} from '../network/got'
import {
DependencyShrinkwrap,
ResolvedDependencies,
} from '../fs/shrinkwrap'
import {PackageSpec} from '../resolve'
import linkBins from '../install/linkBins'
const installCheckLogger = logger('install-check')
export type InstalledPackage = {
id: string,
pkg: Package,
srcPath?: string,
optional: boolean,
hardlinkedLocation: string,
modules: string,
}
export default async function installAll (
ctx: InstallContext,
dependencies: Dependencies,
optionalDependencies: Dependencies,
modules: string,
options: {
force: boolean,
root: string,
storePath: string,
metaCache: Map<string, PackageMeta>,
tag: string,
got: Got,
keypath?: string[],
resolvedDependencies?: ResolvedDependencies,
depth: number,
engineStrict: boolean,
nodeVersion: string,
baseNodeModules: string,
}
): Promise<InstalledPackage[]> {
const keypath = options.keypath || []
const nonOptionalDependencies = Object.keys(dependencies)
.filter(depName => !optionalDependencies[depName])
.reduce((nonOptionalDependencies, depName) => {
nonOptionalDependencies[depName] = dependencies[depName]
return nonOptionalDependencies
}, {})
const installedPkgs: InstalledPackage[] = Array.prototype.concat.apply([], await Promise.all([
installMultiple(ctx, nonOptionalDependencies, modules, Object.assign({}, options, {optional: false, keypath})),
installMultiple(ctx, optionalDependencies, modules, Object.assign({}, options, {optional: true, keypath})),
]))
await Promise.all(
installedPkgs
.map(async function (subdep) {
const dest = path.join(modules, subdep.pkg.name)
await symlinkDir(subdep.hardlinkedLocation, dest)
})
)
return installedPkgs
}
async function installMultiple (
ctx: InstallContext,
pkgsMap: Dependencies,
modules: string,
options: {
force: boolean,
root: string,
storePath: string,
metaCache: Map<string, PackageMeta>,
tag: string,
got: Got,
keypath: string[],
resolvedDependencies?: ResolvedDependencies,
optional: boolean,
depth: number,
engineStrict: boolean,
nodeVersion: string,
baseNodeModules: string,
}
): Promise<InstalledPackage[]> {
pkgsMap = pkgsMap || {}
const pkgs = Object.keys(pkgsMap).map(pkgName => getRawSpec(pkgName, pkgsMap[pkgName]))
ctx.graph = ctx.graph || {}
const specs = pkgs.map(npa)
const nonLinkedPkgs = modules === options.baseNodeModules
// only check modules on the first level
? await pFilter(specs, (spec: PackageSpec) => isInnerLink(modules, spec.name))
: specs
const installedPkgs: InstalledPackage[] = <InstalledPackage[]>(
await Promise.all(
nonLinkedPkgs
.map(async (spec: PackageSpec) => {
const pkgId = options.resolvedDependencies &&
options.resolvedDependencies[spec.name]
const dependencyShrinkwrap = pkgId && ctx.shrinkwrap.packages[pkgId]
try {
const pkg = await install(spec, ctx, Object.assign({}, options, {
pkgId,
dependencyShrinkwrap,
}))
if (options.keypath && options.keypath.indexOf(pkg.id) !== -1) {
return null
}
return pkg
} catch (err) {
if (options.optional) {
logger.warn({
message: `Skipping failed optional dependency ${pkgId || spec.raw}`,
err,
})
return null // is it OK to return null?
}
throw err
}
})
)
)
.filter(pkg => pkg)
return installedPkgs
}
async function isInnerLink (modules: string, depName: string) {
let link: string
try {
link = await fs.readlink(path.join(modules, depName))
} catch (err) {
return true
}
const absLink = path.isAbsolute(link) ? link : path.join(modules, link)
if (absLink.startsWith(modules)) {
return true
}
logger.info(`${depName} is linked to ${modules} from ${link}`)
return false
}
async function install (
spec: PackageSpec,
ctx: InstallContext,
options: {
force: boolean,
root: string,
storePath: string,
metaCache: Map<string, PackageMeta>,
tag: string,
got: Got,
keypath: string[],
pkgId?: string,
dependencyShrinkwrap?: DependencyShrinkwrap,
optional: boolean,
depth: number,
engineStrict: boolean,
nodeVersion: string,
baseNodeModules: string,
}
) {
const keypath = options.keypath || []
const update = keypath.length <= options.depth
const dependentId = keypath[keypath.length - 1]
const loggedPkg = {
rawSpec: spec.rawSpec,
name: spec.name,
dependentId,
}
logStatus({
status: 'installing',
pkg: loggedPkg,
})
const fetchedPkg = await fetch(spec, Object.assign({}, options, {
loggedPkg,
update,
shrinkwrapResolution: options.dependencyShrinkwrap && options.dependencyShrinkwrap.resolution,
fetchingLocker: ctx.fetchingLocker,
}))
if (keypath.indexOf(fetchedPkg.id) !== -1) {
return fetchedPkg
}
ctx.shrinkwrap.packages[fetchedPkg.id] = ctx.shrinkwrap.packages[fetchedPkg.id] || {}
ctx.shrinkwrap.packages[fetchedPkg.id].resolution = fetchedPkg.resolution
const pkg = await fetchedPkg.fetchingPkg
if (!options.force) {
await isInstallable(pkg, fetchedPkg, options)
}
const modules = path.join(options.baseNodeModules, `.${fetchedPkg.id}`, 'node_modules')
const dependency: InstalledPackage = {
id: fetchedPkg.id,
srcPath: fetchedPkg.srcPath,
optional: options.optional === true,
pkg,
hardlinkedLocation: path.join(modules, pkg.name),
modules,
}
addInstalledPkg(ctx.installs, dependency)
addToGraph(ctx.graph, dependentId, dependency)
const linking = ctx.linkingLocker(dependency.hardlinkedLocation, async function () {
const newlyFetched = await fetchedPkg.fetchingFiles
const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json')
if (newlyFetched || options.force || !await exists(pkgJsonPath) || !await pkgLinkedToStore()) {
await linkDir(fetchedPkg.path, dependency.hardlinkedLocation)
if (ctx.installationSequence.indexOf(dependency.id) === -1) {
ctx.installationSequence.push(dependency.id)
}
}
async function pkgLinkedToStore () {
const pkgJsonPathInStore = path.join(fetchedPkg.path, 'package.json')
if (await isSameFile(pkgJsonPath, pkgJsonPathInStore)) return true
logger.info(`Relinking ${dependency.hardlinkedLocation} from the store`)
return false
}
})
if (!ctx.installed.has(dependency.id)) {
ctx.installed.add(dependency.id)
const dependencies = await installDependencies(
pkg,
dependency,
ctx,
modules,
Object.assign({}, options, {
resolvedDependencies: options.dependencyShrinkwrap && options.dependencyShrinkwrap.dependencies
})
)
if (dependencies.length) {
ctx.shrinkwrap.packages[dependency.id].dependencies = dependencies
.reduce((resolutions, dep) => Object.assign(resolutions, {
[dep.pkg.name]: dep.id
}), {})
}
await linking
const binPath = path.join(dependency.hardlinkedLocation, 'node_modules', '.bin')
await linkBins(modules, binPath, pkg.name)
// link also the bundled dependencies` bins
if (pkg.bundledDependencies || pkg.bundleDependencies) {
const bundledModules = path.join(dependency.hardlinkedLocation, 'node_modules')
await linkBins(bundledModules, binPath)
}
logStatus({
status: 'installed',
pkg: Object.assign({}, loggedPkg, {version: pkg.version}),
})
} else {
await linking
}
return dependency
}
async function isSameFile (file1: string, file2: string) {
const stats = await Promise.all([fs.stat(file1), fs.stat(file2)])
return stats[0].ino === stats[1].ino
}
function addToGraph (graph: Graph, dependent: string, dependency: InstalledPackage) {
dependent = dependent || GRAPH_ENTRY
graph[dependent] = graph[dependent] || {}
graph[dependent].dependencies = graph[dependent].dependencies || {}
updateDependencyResolution(graph, dependent, dependency.pkg.name, dependency.id)
graph[dependency.id] = graph[dependency.id] || {}
graph[dependency.id].dependents = graph[dependency.id].dependents || []
if (graph[dependency.id].dependents.indexOf(dependent) === -1) {
graph[dependency.id].dependents.push(dependent)
}
}
function updateDependencyResolution (graph: Graph, dependent: string, depName: string, newDepId: string) {
if (graph[dependent].dependencies[depName] &&
graph[dependent].dependencies[depName] !== newDepId) {
removeIfNoDependents(graph, graph[dependent].dependencies[depName], dependent)
}
graph[dependent].dependencies[depName] = newDepId
}
function removeIfNoDependents(graph: Graph, id: string, removedDependent: string) {
if (graph[id] && graph[id].dependents && graph[id].dependents.length === 1 &&
graph[id].dependents[0] === removedDependent) {
Object.keys(graph[id].dependencies || {}).forEach(depName => removeIfNoDependents(graph, graph[id].dependencies[depName], id))
delete graph[id]
}
}
async function isInstallable (
pkg: Package,
fetchedPkg: FetchedPackage,
options: {
optional: boolean,
engineStrict: boolean,
nodeVersion: string,
}
): Promise<void> {
const warn = await installChecks.checkPlatform(pkg) || await installChecks.checkEngine(pkg, {
pnpmVersion: pnpmPkg.version,
nodeVersion: options.nodeVersion
})
if (!warn) return
installCheckLogger.warn(warn)
if (options.engineStrict || options.optional) {
await fetchedPkg.abort()
throw warn
}
}
async function installDependencies (
pkg: Package,
dependency: InstalledPackage,
ctx: InstallContext,
modules: string,
opts: {
force: boolean,
root: string,
storePath: string,
metaCache: Map<string, PackageMeta>,
tag: string,
got: Got,
keypath: string[],
resolvedDependencies?: ResolvedDependencies,
optional: boolean,
depth: number,
engineStrict: boolean,
nodeVersion: string,
baseNodeModules: string,
}
): Promise<InstalledPackage[]> {
const depsInstallOpts = Object.assign({}, opts, {
keypath: opts.keypath.concat([ dependency.id ]),
root: dependency.srcPath,
})
const bundledDeps = pkg.bundleDependencies || pkg.bundledDependencies || []
const filterDeps = getNotBundledDeps.bind(null, bundledDeps)
const deps = filterDeps(pkg.dependencies || {})
const optionalDeps = filterDeps(pkg.optionalDependencies || {})
const installedDeps: InstalledPackage[] = await installAll(ctx, deps, optionalDeps, modules, depsInstallOpts)
return installedDeps
}
function getNotBundledDeps (bundledDeps: string[], deps: Dependencies) {
return Object.keys(deps)
.filter(depName => bundledDeps.indexOf(depName) === -1)
.reduce((notBundledDeps, depName) => {
notBundledDeps[depName] = deps[depName]
return notBundledDeps
}, {})
}
function addInstalledPkg (installs: InstalledPackages, newPkg: InstalledPackage) {
if (!installs[newPkg.id]) {
installs[newPkg.id] = newPkg
return
}
installs[newPkg.id].optional = installs[newPkg.id].optional && newPkg.optional
}
function getRawSpec (name: string, version: string) {
return version === '*' ? name : `${name}@${version}`
}