diff --git a/lib/cmd/install.js b/lib/cmd/install.js index 07d43d0dbf..1d4da904db 100644 --- a/lib/cmd/install.js +++ b/lib/cmd/install.js @@ -8,6 +8,7 @@ var logger = require('../logger') var installMultiple = require('../install_multiple') var config = require('../config') var save = require('../save') +var linkPeers = require('../install/link_peers') /* * Perform @@ -20,9 +21,10 @@ function run (cli) { var installType return readPkgUp() - .then(pkg_ => { pkg = pkg_ }) + .then(_ => { pkg = _ }) .then(_ => updateContext(pkg.path)) .then(_ => install()) + .then(_ => linkPeers(pkg, ctx.store, ctx.installs)) function install () { installType = cli.input && cli.input.length ? 'named' : 'general' diff --git a/lib/fs/unsymlink.js b/lib/fs/unsymlink.js new file mode 100644 index 0000000000..d3a0480bda --- /dev/null +++ b/lib/fs/unsymlink.js @@ -0,0 +1,16 @@ +var fs = require('mz/fs') + +/* + * Removes a symlink + */ + +module.exports = function unsymlink (path) { + return fs.lstat(path) + .then(stat => { + if (stat.isSymbolicLink()) return fs.unlink(path) + throw new Error('Can\'t unlink ' + path) + }) + .catch(err => { + if (err.code !== 'ENOENT') throw err + }) +} diff --git a/lib/install/link_peers.js b/lib/install/link_peers.js new file mode 100644 index 0000000000..093e1eb511 --- /dev/null +++ b/lib/install/link_peers.js @@ -0,0 +1,39 @@ +var Promise = require('../promise') +var mkdirp = require('../mkdirp') +var unsymlink = require('../fs/unsymlink') +var relSymlink = require('../rel_symlink') +var join = require('path').join +var semver = require('semver') + +/* + * Links into `.store/node_modules` + */ + +module.exports = function linkPeers (pkg, store, installs) { + var peers = {} + var roots = {} + + Object.keys(installs).forEach(name => { + var pkgData = installs[name] + var realname = pkgData.name + + if (pkgData.keypath.length === 0) { + roots[realname] = pkgData + } else if (!peers[realname] || + semver.gt(pkgData.version, peers[realname].version)) { + peers[realname] = pkgData + } + }) + + var modules = join(store, 'node_modules') + return mkdirp(modules) + .then(_ => Promise.all(Object.keys(roots).map(name => + unsymlink(join(modules, roots[name].name)) + ))) + .then(_ => Promise.all(Object.keys(peers).map(name => + relSymlink( + join('..', peers[name].fullname), + join(modules, peers[name].name)) + ))) +} + diff --git a/test/index.js b/test/index.js index b9e0ed5973..c009fa982c 100644 --- a/test/index.js +++ b/test/index.js @@ -4,7 +4,8 @@ var fs = require('fs') var prepare = require('./support/prepare') var install = require('../lib/cmd/install') require('./support/sepia') -var stat + +var stat, _ test('eslint', require('tape-eslint')()) @@ -30,7 +31,7 @@ test('no dependencies (lodash)', function (t) { prepare() install({ input: ['lodash@4.0.0'], flags: { quiet: true } }) .then(function () { - var _ = require(join(process.cwd(), 'node_modules', 'lodash')) + _ = require(join(process.cwd(), 'node_modules', 'lodash')) t.ok(typeof _ === 'function', '_ is available') t.ok(typeof _.clone === 'function', '_.clone is available') t.end() @@ -41,7 +42,7 @@ test('scoped modules without version spec (@rstacruz/tap-spec)', function (t) { prepare() install({ input: ['@rstacruz/tap-spec'], flags: { quiet: true } }) .then(function () { - var _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) + _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) t.ok(typeof _ === 'function', 'tap-spec is available') t.end() }, t.end) @@ -51,7 +52,7 @@ test('scoped modules with versions (@rstacruz/tap-spec@4.1.1)', function (t) { prepare() install({ input: ['@rstacruz/tap-spec@4.1.1'], flags: { quiet: true } }) .then(function () { - var _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) + _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) t.ok(typeof _ === 'function', 'tap-spec is available') t.end() }, t.end) @@ -61,7 +62,7 @@ test('scoped modules (@rstacruz/tap-spec@*)', function (t) { prepare() install({ input: ['@rstacruz/tap-spec@*'], flags: { quiet: true } }) .then(function () { - var _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) + _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) t.ok(typeof _ === 'function', 'tap-spec is available') t.end() }, t.end) @@ -71,7 +72,7 @@ test('multiple scoped modules (@rstacruz/...)', function (t) { prepare() install({ input: ['@rstacruz/tap-spec@*', '@rstacruz/travis-encrypt@*'], flags: { quiet: true } }) .then(function () { - var _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) + _ = require(join(process.cwd(), 'node_modules', '@rstacruz/tap-spec')) t.ok(typeof _ === 'function', 'tap-spec is available') _ = require(join(process.cwd(), 'node_modules', '@rstacruz/travis-encrypt')) t.ok(typeof _ === 'function', 'travis-encrypt is available') @@ -215,3 +216,38 @@ test('multiple save to package.json with `exact` versions (@rstacruz/tap-spec & t.end() }, t.end) }) + +test('flattening symlinks (minimatch@3.0.0)', function (t) { + prepare() + install({ input: ['minimatch@3.0.0'], flags: { quiet: true } }) + .then(function () { + stat = fs.lstatSync(join(process.cwd(), 'node_modules', '.store', 'node_modules', 'balanced-match')) + t.ok(stat.isSymbolicLink(), 'balanced-match is linked into store node_modules') + + _ = exists(join(process.cwd(), 'node_modules', 'balanced-match')) + t.ok(!_, 'balanced-match is not linked into main node_modules') + t.end() + }, t.end) +}) + +test('flattening symlinks (minimatch + balanced-match)', function (t) { + prepare() + install({ input: ['minimatch@3.0.0'], flags: { quiet: true } }) + .then(_ => install({ input: ['balanced-match@^0.3.0'], flags: { quiet: true } })) + .then(function () { + _ = exists(join(process.cwd(), 'node_modules', '.store', 'node_modules', 'balanced-match')) + t.ok(!_, 'balanced-match is removed from store node_modules') + + _ = exists(join(process.cwd(), 'node_modules', 'balanced-match')) + t.ok(_, 'balanced-match now in main node_modules') + t.end() + }, t.end) +}) + +function exists (path) { + try { + return fs.statSync(path) + } catch (err) { + if (err.code !== 'ENOENT') throw err + } +}