diff --git a/README.md b/README.md index cbffbe3a2a..2d3fb403d5 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,15 @@ Can be passed in via a CLI option. `--no-lock` to set it to false. E.g.: `pnpm i > If you experience issues similar to the ones described in [#594](https://github.com/pnpm/pnpm/issues/594), use this option to disable locking. > In the meanwhile, we'll try to find a solution that will make locking work for everyone. +#### independent-leaves + +* Default: **false** +* Type: **Boolean** + +If true, symlinks leaf dependencies directly from the global store. Leaf dependencies are +packages that have no dependencies of their own. Setting this config to `true` might break some packages +that rely on location but gives an average of **8% installation speed improvement**. + ## Benchmark pnpm is faster than npm and yarn. See [this](https://github.com/zkochan/node-package-manager-benchmark) diff --git a/src/api/extendOptions.ts b/src/api/extendOptions.ts index c1e569b154..a20643d7ca 100644 --- a/src/api/extendOptions.ts +++ b/src/api/extendOptions.ts @@ -38,6 +38,7 @@ const defaults = (opts: PnpmOptions) => { update: false, repeatInstallDepth: -1, optional: true, + independentLeaves: false, } } @@ -66,7 +67,8 @@ export default (opts?: PnpmOptions): StrictPnpmOptions => { } extendedOpts.registry = normalizeRegistryUrl(extendedOpts.registry) if (extendedOpts.global) { - extendedOpts.prefix = path.join(extendedOpts.prefix, LAYOUT_VERSION.toString()) + const subfolder = LAYOUT_VERSION.toString() + (extendedOpts.independentLeaves ? '_independent_leaves' : '') + extendedOpts.prefix = path.join(extendedOpts.prefix, subfolder) } return extendedOpts } diff --git a/src/api/getContext.ts b/src/api/getContext.ts index 2f52bdb11a..dd8558bbb2 100644 --- a/src/api/getContext.ts +++ b/src/api/getContext.ts @@ -41,6 +41,14 @@ export default async function getContext (opts: StrictPnpmOptions, installType?: if (modules) { try { + if (Boolean(modules.independentLeaves) !== opts.independentLeaves) { + if (modules.independentLeaves) { + throw new Error(`This node_modules was installed with --independent-leaves option. + Use this option or run same command with --force to recreated node_modules`) + } + throw new Error(`This node_modules was not installed with the --independent-leaves option. + Don't use --independent-leaves run same command with --force to recreated node_modules`) + } checkCompatibility(modules, {storePath, modulesPath}) } catch (err) { if (!opts.force) throw err diff --git a/src/api/install.ts b/src/api/install.ts index 724b4948ef..d3a0618887 100644 --- a/src/api/install.ts +++ b/src/api/install.ts @@ -377,6 +377,7 @@ async function installInContext ( storePath: ctx.storePath, skipped: ctx.skipped, pkg: newPkg || ctx.pkg, + independentLeaves: opts.independentLeaves }) await saveShrinkwrap(ctx.root, result.shrinkwrap) @@ -385,6 +386,7 @@ async function installInContext ( storePath: ctx.storePath, skipped: Array.from(installCtx.skipped), layoutVersion: LAYOUT_VERSION, + independentLeaves: opts.independentLeaves, }) // postinstall hooks diff --git a/src/api/uninstall.ts b/src/api/uninstall.ts index 3d078d37e5..26839b864b 100644 --- a/src/api/uninstall.ts +++ b/src/api/uninstall.ts @@ -60,6 +60,7 @@ export async function uninstallInContext (pkgsToUninstall: string[], ctx: PnpmCo storePath: ctx.storePath, skipped: Array.from(ctx.skipped).filter(pkgId => removedPkgIds.indexOf(pkgId) === -1), layoutVersion: LAYOUT_VERSION, + independentLeaves: opts.independentLeaves, }) await removeOuterLinks(pkgsToUninstall, path.join(ctx.root, 'node_modules'), {storePath: ctx.storePath}) } diff --git a/src/bin/pnpm.ts b/src/bin/pnpm.ts index 3a2f6cf8c2..ec4194a25b 100755 --- a/src/bin/pnpm.ts +++ b/src/bin/pnpm.ts @@ -63,6 +63,7 @@ async function run (argv: string[]) { 'child-concurrency': Number, 'offline': Boolean, 'reporter': String, + 'independent-leaves': Boolean, } const types = R.merge(npmDefaults.types, pnpmTypes) const cliConf = nopt( diff --git a/src/fs/modulesController.ts b/src/fs/modulesController.ts index 43c8df9389..d1c1d2ff81 100644 --- a/src/fs/modulesController.ts +++ b/src/fs/modulesController.ts @@ -13,6 +13,7 @@ export type Modules = { storePath: string, skipped: string[], layoutVersion: number, + independentLeaves: boolean, } export async function read (modulesPath: string): Promise { diff --git a/src/link/index.ts b/src/link/index.ts index dd646cd629..f1a6cdcb9b 100644 --- a/src/link/index.ts +++ b/src/link/index.ts @@ -40,6 +40,7 @@ export default async function ( storePath: string, skipped: Set, pkg: Package, + independentLeaves: boolean, } ): Promise<{ linkedPkgsMap: DependencyTreeNodeMap, @@ -47,7 +48,7 @@ export default async function ( newPkgResolvedIds: string[], }> { const topPkgIds = topPkgs.map(pkg => pkg.id) - const pkgsToLink = await resolvePeers(tree, rootNodeIds, topPkgIds, opts.topParents) + const pkgsToLink = await resolvePeers(tree, rootNodeIds, topPkgIds, opts.topParents, opts.independentLeaves) const newShr = updateShrinkwrap(pkgsToLink, opts.shrinkwrap, opts.pkg) await removeOrphanPkgs(opts.privateShrinkwrap, newShr, opts.root, opts.storePath) diff --git a/src/link/resolvePeers.ts b/src/link/resolvePeers.ts index 9cff3a8286..05b554cd74 100644 --- a/src/link/resolvePeers.ts +++ b/src/link/resolvePeers.ts @@ -41,7 +41,8 @@ export default function ( topPkgIds: string[], // only the top dependencies that were already installed // to avoid warnings about unresolved peer dependencies - topParents: {name: string, version: string}[] + topParents: {name: string, version: string}[], + independentLeaves: boolean ): DependencyTreeNodeMap { const pkgsByName = R.fromPairs( topParents.map((parent: {name: string, version: string}): R.KeyValuePair => [ @@ -55,7 +56,7 @@ export default function ( const nodeIdToResolvedId = {} const resolvedTree: DependencyTreeNodeMap = {} - resolvePeersOfChildren(rootNodeIds, pkgsByName, tree, nodeIdToResolvedId, resolvedTree) + resolvePeersOfChildren(rootNodeIds, pkgsByName, tree, nodeIdToResolvedId, resolvedTree, independentLeaves) R.values(resolvedTree).forEach(node => { node.children = node.children.map(child => nodeIdToResolvedId[child]) @@ -68,11 +69,12 @@ function resolvePeersOfNode ( parentPkgs: ParentRefs, tree: TreeNodeMap, nodeIdToResolvedId: {[nodeId: string]: string}, - resolvedTree: DependencyTreeNodeMap + resolvedTree: DependencyTreeNodeMap, + independentLeaves: boolean ): string[] { const node = tree[nodeId] - const unknownResolvedPeersOfChildren = resolvePeersOfChildren(node.children, parentPkgs, tree, nodeIdToResolvedId, resolvedTree) + const unknownResolvedPeersOfChildren = resolvePeersOfChildren(node.children, parentPkgs, tree, nodeIdToResolvedId, resolvedTree, independentLeaves) const resolvedPeers = R.isEmpty(node.pkg.peerDependencies) ? [] @@ -97,7 +99,7 @@ function resolvePeersOfNode ( nodeIdToResolvedId[nodeId] = resolvedId if (!resolvedTree[resolvedId] || resolvedTree[resolvedId].depth > node.depth) { - const independent = !node.children.length && R.isEmpty(node.pkg.peerDependencies) + const independent = independentLeaves && !node.children.length && R.isEmpty(node.pkg.peerDependencies) const pathToUnpacked = path.join(node.pkg.path, 'node_modules', node.pkg.name) const hardlinkedLocation = !independent ? path.join(modules, node.pkg.name) @@ -129,7 +131,8 @@ function resolvePeersOfChildren ( parentParentPkgs: ParentRefs, tree: {[nodeId: string]: TreeNode}, nodeIdToResolvedId: {[nodeId: string]: string}, - resolvedTree: DependencyTreeNodeMap + resolvedTree: DependencyTreeNodeMap, + independentLeaves: boolean ): string[] { const trees: DependencyTreeNodeMap[] = [] const unknownResolvedPeersOfChildren: string[] = [] @@ -138,7 +141,7 @@ function resolvePeersOfChildren ( ) for (const child of children) { - const allResolvedPeers = resolvePeersOfNode(child, parentPkgs, tree, nodeIdToResolvedId, resolvedTree) + const allResolvedPeers = resolvePeersOfNode(child, parentPkgs, tree, nodeIdToResolvedId, resolvedTree, independentLeaves) const unknownResolvedPeersOfChild = allResolvedPeers .filter((resolvedPeerNodeId: string) => children.indexOf(resolvedPeerNodeId) === -1) diff --git a/src/types.ts b/src/types.ts index 6dcf4cf2e1..9f862ff247 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,7 @@ export type PnpmOptions = { lock?: boolean, childConcurrency?: number, repeatInstallDepth?: number, + independentLeaves?: boolean, // cannot be specified via configs update?: boolean, @@ -103,6 +104,7 @@ export type StrictPnpmOptions = { lock: boolean, childConcurrency: number, repeatInstallDepth: number, + independentLeaves: boolean, // cannot be specified via configs update: boolean, diff --git a/test/breakingChanges.ts b/test/breakingChanges.ts index d1f299131d..375fd4ff27 100644 --- a/test/breakingChanges.ts +++ b/test/breakingChanges.ts @@ -77,7 +77,7 @@ test('fail on non-compatible store when forced during named installation', async async function saveModulesYaml (pnpmVersion: string, storePath: string) { await mkdirp('node_modules') - await fs.writeFile('node_modules/.modules.yaml', `packageManager: pnpm@${pnpmVersion}\nstorePath: ${storePath}`) + await fs.writeFile('node_modules/.modules.yaml', `packageManager: pnpm@${pnpmVersion}\nstorePath: ${storePath}\nindependentLeaves: false`) } test('fail on non-compatible shrinkwrap.yaml', async t => { diff --git a/test/install/independentLeaves.ts b/test/install/independentLeaves.ts new file mode 100644 index 0000000000..3e23e04621 --- /dev/null +++ b/test/install/independentLeaves.ts @@ -0,0 +1,60 @@ +import tape = require('tape') +import path = require('path') +import promisifyTape from 'tape-promise' +import { + prepare, + testDefaults, +} from '../utils' +import {installPkgs} from '../../src' + +const test = promisifyTape(tape) + +test('install with --independent-leaves', async function (t: tape.Test) { + const project = prepare(t) + await installPkgs(['rimraf@2.5.1'], testDefaults({independentLeaves: true})) + + const m = project.requireModule('rimraf') + t.ok(typeof m === 'function', 'rimraf() is available') + await project.isExecutable('.bin/rimraf') +}) + +test('--independent-leaves throws exception when executed on node_modules installed w/o the option', async function (t: tape.Test) { + const project = prepare(t) + await installPkgs(['is-positive'], testDefaults({independentLeaves: false})) + + try { + await installPkgs(['is-negative'], testDefaults({independentLeaves: true})) + t.fail('installation should have failed') + } catch (err) { + t.ok(err.message.indexOf('This node_modules was not installed with the --independent-leaves option.') === 0) + } +}) + +test('--no-independent-leaves throws exception when executed on node_modules installed with --independent-leaves', async function (t: tape.Test) { + const project = prepare(t) + await installPkgs(['is-positive'], testDefaults({independentLeaves: true})) + + try { + await installPkgs(['is-negative'], testDefaults({independentLeaves: false})) + t.fail('installation should have failed') + } catch (err) { + t.ok(err.message.indexOf('This node_modules was installed with --independent-leaves option.') === 0) + } +}) + +test('global installation with --independent-leaves', async function (t: tape.Test) { + prepare(t) + const globalPrefix = path.resolve('..', 'global') + const opts = testDefaults({global: true, prefix: globalPrefix, independentLeaves: true}) + await installPkgs(['is-positive'], opts) + + // there was an issue when subsequent installations were removing everything installed prior + // https://github.com/pnpm/pnpm/issues/808 + await installPkgs(['is-negative'], opts) + + const isPositive = require(path.join(globalPrefix, '1_independent_leaves', 'node_modules', 'is-positive')) + t.ok(typeof isPositive === 'function', 'isPositive() is available') + + const isNegative = require(path.join(globalPrefix, '1_independent_leaves', 'node_modules', 'is-negative')) + t.ok(typeof isNegative === 'function', 'isNegative() is available') +}) diff --git a/test/install/index.ts b/test/install/index.ts index 52af342c31..4d3dc5e3ed 100644 --- a/test/install/index.ts +++ b/test/install/index.ts @@ -9,3 +9,4 @@ import './auth' import './local' import './updatingPkgJson' import './global' +import './independentLeaves'