diff --git a/lib/dedupe.js b/lib/dedupe.js index 9e6dd1ef25245..ded309c21ac7e 100644 --- a/lib/dedupe.js +++ b/lib/dedupe.js @@ -13,13 +13,13 @@ var earliestInstallable = require('./install/deps.js').earliestInstallable var checkPermissions = require('./install/check-permissions.js') var decomposeActions = require('./install/decompose-actions.js') var loadExtraneous = require('./install/deps.js').loadExtraneous -var filterInvalidActions = require('./install/filter-invalid-actions.js') -var recalculateMetadata = require('./install/deps.js').recalculateMetadata +var computeMetadata = require('./install/deps.js').computeMetadata var sortActions = require('./install/diff-trees.js').sortActions var moduleName = require('./utils/module-name.js') var packageId = require('./utils/package-id.js') var childPath = require('./utils/child-path.js') var usage = require('./utils/usage') +var getRequested = require('./install/get-requested.js') module.exports = dedupe module.exports.Deduper = Deduper @@ -65,10 +65,16 @@ Deduper.prototype.loadIdealTree = function (cb) { } ], [this, this.finishTracker, 'loadAllDepsIntoIdealTree'], - [this, function (next) { recalculateMetadata(this.idealTree, log, next) }] + [this, andComputeMetadata(this.idealTree)] ], cb) } +function andComputeMetadata (tree) { + return function (next) { + next(null, computeMetadata(tree)) + } +} + Deduper.prototype.generateActionsToTake = function (cb) { validate('F', arguments) log.silly('dedupe', 'generateActionsToTake') @@ -82,7 +88,6 @@ Deduper.prototype.generateActionsToTake = function (cb) { next() }], [this, this.finishTracker, 'sort-actions'], - [filterInvalidActions, this.where, this.differences], [checkPermissions, this.differences], [decomposeActions, this.differences, this.todo] ], cb) @@ -130,18 +135,18 @@ function hoistChildren_ (tree, diff, seen, next) { seen[tree.path] = true asyncMap(tree.children, function (child, done) { if (!tree.parent) return hoistChildren_(child, diff, seen, done) - var better = findRequirement(tree.parent, moduleName(child), child.package._requested || npa(packageId(child))) + var better = findRequirement(tree.parent, moduleName(child), getRequested(child) || npa(packageId(child))) if (better) { return chain([ [remove, child, diff], - [recalculateMetadata, tree, log] + [andComputeMetadata(tree)] ], done) } var hoistTo = earliestInstallable(tree, tree.parent, child.package) if (hoistTo) { move(child, hoistTo, diff) chain([ - [recalculateMetadata, hoistTo, log], + [andComputeMetadata(hoistTo)], [hoistChildren_, child, diff, seen], [ function (next) { moveRemainingChildren(child, diff) diff --git a/lib/fetch-package-metadata.js b/lib/fetch-package-metadata.js index 2951cb47a9863..fcd548de8f23f 100644 --- a/lib/fetch-package-metadata.js +++ b/lib/fetch-package-metadata.js @@ -1,6 +1,7 @@ 'use strict' const deprCheck = require('./utils/depr-check') +const path = require('path') const log = require('npmlog') const readPackageTree = require('read-package-tree') const rimraf = require('rimraf') @@ -12,12 +13,13 @@ const limit = require('call-limit') const tempFilename = require('./utils/temp-filename') const pacote = require('pacote') const pacoteOpts = require('./config/pacote') +const isWindows = require('./utils/is-windows.js') function andLogAndFinish (spec, tracker, done) { - validate('SF', [spec, done]) + validate('SOF|SZF|OOF|OZF', [spec, tracker, done]) return (er, pkg) => { if (er) { - log.silly('fetchPackageMetaData', 'error for ' + spec, er) + log.silly('fetchPackageMetaData', 'error for ' + String(spec), er) if (tracker) tracker.finish() } return done(er, pkg) @@ -38,12 +40,20 @@ function fetchPackageMetadata (spec, where, opts, done) { opts = {} } var tracker = opts.tracker + const logAndFinish = andLogAndFinish(spec, tracker, done) + if (typeof spec === 'object') { var dep = spec - spec = dep.raw + } else { + dep = npa(spec) } - const logAndFinish = andLogAndFinish(spec, tracker, done) - pacote.manifest(dep || spec, pacoteOpts({ + if (!isWindows && dep.type === 'directory' && /^[a-zA-Z]:/.test(dep.fetchSpec)) { + var err = new Error(`Can't install from windows path on a non-windows system: ${dep.fetchSpec.replace(/[/]/g, '\\')}`) + err.code = 'EWINDOWSPATH' + return logAndFinish(err) + } + + pacote.manifest(dep, pacoteOpts({ annotate: true, fullMetadata: opts.fullMetadata, log: tracker || npmlog, @@ -51,7 +61,22 @@ function fetchPackageMetadata (spec, where, opts, done) { where: where })).then( (pkg) => logAndFinish(null, deprCheck(pkg)), - logAndFinish + (err) => { + if (dep.type !== 'directory') return logAndFinish(err) + if (err.code === 'ENOTDIR') { + var enolocal = new Error(`Could not install "${path.relative(process.cwd(), dep.fetchSpec)}" as it is not a directory and is not a file with a name ending in .tgz, .tar.gz or .tar`) + enolocal.code = 'ENOLOCAL' + if (err.stack) enolocal.stack = err.stack + return logAndFinish(enolocal) + } else if (err.code === 'ENOPACKAGEJSON') { + var enopackage = new Error(`Could not install from "${path.relative(process.cwd(), dep.fetchSpec)}" as it does not contain a package.json file.`) + enopackage.code = 'ENOLOCAL' + if (err.stack) enopackage.stack = err.stack + return logAndFinish(enopackage) + } else { + return logAndFinish(err) + } + } ) } @@ -59,7 +84,16 @@ module.exports.addBundled = addBundled function addBundled (pkg, next) { validate('OF', arguments) if (pkg._bundled !== undefined) return next(null, pkg) - if (!pkg.bundleDependencies) return next(null, pkg) + + if (!pkg.bundleDependencies && pkg._requested.type !== 'directory') return next(null, pkg) + const requested = pkg._requested || npa(pkg._from) + if (requested.type === 'directory') { + pkg._bundled = null + return readPackageTree(pkg._requested.fetchSpec, function (er, tree) { + if (tree) pkg._bundled = tree.children + return next(null, pkg) + }) + } pkg._bundled = null const target = tempFilename('unpack') const opts = pacoteOpts({integrity: pkg._integrity}) diff --git a/lib/install.js b/lib/install.js index fbd82a41b6086..fc6ca3463f9ed 100644 --- a/lib/install.js +++ b/lib/install.js @@ -118,7 +118,7 @@ var saveMetrics = require('./utils/metrics.js').save // install specific libraries var copyTree = require('./install/copy-tree.js') var readShrinkwrap = require('./install/read-shrinkwrap.js') -var recalculateMetadata = require('./install/deps.js').recalculateMetadata +var computeMetadata = require('./install/deps.js').computeMetadata var prefetchDeps = require('./install/deps.js').prefetchDeps var loadDeps = require('./install/deps.js').loadDeps var loadDevDeps = require('./install/deps.js').loadDevDeps @@ -128,7 +128,6 @@ var loadExtraneous = require('./install/deps.js').loadExtraneous var diffTrees = require('./install/diff-trees.js') var checkPermissions = require('./install/check-permissions.js') var decomposeActions = require('./install/decompose-actions.js') -var filterInvalidActions = require('./install/filter-invalid-actions.js') var validateTree = require('./install/validate-tree.js') var validateArgs = require('./install/validate-args.js') var saveRequested = require('./install/save.js').saveRequested @@ -141,6 +140,8 @@ var removeObsoleteDep = require('./install/deps.js').removeObsoleteDep var packageId = require('./utils/package-id.js') var moduleName = require('./utils/module-name.js') var errorMessage = require('./utils/error-message.js') +var removeDeps = require('./install/deps.js').removeDeps +var isExtraneous = require('./install/is-extraneous.js') function unlockCB (lockPath, name, cb) { validate('SSF', arguments) @@ -281,8 +282,12 @@ Installer.prototype.run = function (_cb) { [this, this.runPostinstallTopLevelLifecycles], [this, this.finishTracker, 'runTopLevelLifecycles'] ) - if (getSaveType(this.args)) { - postInstallSteps.push([this, this.saveToDependencies]) + if (getSaveType()) { + postInstallSteps.push( + // this is necessary as we don't fill in `dependencies` and `devDependencies` in deps loaded from shrinkwrap + // until after we extract them + [this, (next) => { computeMetadata(this.idealTree); next() }], + [this, this.saveToDependencies]) } } postInstallSteps.push( @@ -340,11 +345,24 @@ Installer.prototype.loadCurrentTree = function (cb) { } else { todo.push([this, this.readLocalPackageData]) } - todo.push( - [this, this.normalizeTree, log.newGroup('loadCurrentTree:normalizeTree')]) + todo.push([this, this.normalizeCurrentTree]) chain(todo, cb) } +var createNode = require('./install/node.js').create +var flatNameFromTree = require('./install/flatten-tree.js').flatNameFromTree +Installer.prototype.normalizeCurrentTree = function (cb) { + this.currentTree.isTop = true + normalizeTree(this.currentTree) + return cb() + + function normalizeTree (tree) { + createNode(tree) + tree.location = flatNameFromTree(tree) + tree.children.forEach(normalizeTree) + } +} + Installer.prototype.loadIdealTree = function (cb) { validate('F', arguments) log.silly('install', 'loadIdealTree') @@ -361,16 +379,22 @@ Installer.prototype.loadIdealTree = function (cb) { [this.newTracker(this.progress.loadIdealTree, 'loadIdealTree:loadAllDepsIntoIdealTree', 10)], [this, this.loadAllDepsIntoIdealTree], [this, this.finishTracker, 'loadIdealTree:loadAllDepsIntoIdealTree'], - - // TODO: Remove this (should no longer be necessary, instead counter productive) - [this, function (next) { recalculateMetadata(this.idealTree, log, next) }] + [this, function (next) { computeMetadata(this.idealTree); next() }], + [this, this.pruneIdealTree] ], cb) } +Installer.prototype.pruneIdealTree = function (cb) { + var toPrune = this.idealTree.children + .filter((n) => !n.fromShrinkwrap && isExtraneous(n)) + .map((n) => ({name: moduleName(n)})) + return removeDeps(toPrune, this.idealTree, null, log.newGroup('pruneDeps'), cb) +} + Installer.prototype.loadAllDepsIntoIdealTree = function (cb) { validate('F', arguments) log.silly('install', 'loadAllDepsIntoIdealTree') - var saveDeps = getSaveType(this.args) + var saveDeps = getSaveType() var cg = this.progress['loadIdealTree:loadAllDepsIntoIdealTree'] var installNewModules = !!this.args.length @@ -386,8 +410,7 @@ Installer.prototype.loadAllDepsIntoIdealTree = function (cb) { ) if (this.prod || this.dev) { steps.push( - [prefetchDeps, this.idealTree, depsToPreload, cg.newGroup('prefetchDeps')] - ) + [prefetchDeps, this.idealTree, depsToPreload, cg.newGroup('prefetchDeps')]) } if (this.prod) { steps.push( @@ -411,7 +434,6 @@ Installer.prototype.generateActionsToTake = function (cb) { [validateTree, this.idealTree, cg.newGroup('validateTree')], [diffTrees, this.currentTree, this.idealTree, this.differences, cg.newGroup('diffTrees')], [this, this.computeLinked], - [filterInvalidActions, this.where, this.differences], [checkPermissions, this.differences], [decomposeActions, this.differences, this.todo] ], cb) @@ -474,10 +496,11 @@ Installer.prototype.executeActions = function (cb) { [lock, node_modules, '.staging'], [rimraf, staging], [doParallelActions, 'extract', staging, todo, cg.newGroup('extract', 100)], - [doParallelActions, 'preinstall', staging, todo, trackLifecycle.newGroup('preinstall')], [doReverseSerialActions, 'remove', staging, todo, cg.newGroup('remove')], [doSerialActions, 'move', staging, todo, cg.newGroup('move')], [doSerialActions, 'finalize', staging, todo, cg.newGroup('finalize')], + [doParallelActions, 'refresh-package-json', staging, todo, cg.newGroup('refresh-package-json')], + [doParallelActions, 'preinstall', staging, todo, trackLifecycle.newGroup('preinstall')], [doSerialActions, 'build', staging, todo, trackLifecycle.newGroup('build')], [doSerialActions, 'global-link', staging, todo, trackLifecycle.newGroup('global-link')], [doParallelActions, 'update-linked', staging, todo, trackLifecycle.newGroup('update-linked')], @@ -557,6 +580,7 @@ Installer.prototype.runPostinstallTopLevelLifecycles = function (cb) { Installer.prototype.saveToDependencies = function (cb) { validate('F', arguments) if (this.failing) return cb() + if (!this.differences.length) return cb() log.silly('install', 'saveToDependencies') saveRequested(this.args, this.idealTree, cb) } @@ -623,19 +647,6 @@ Installer.prototype.loadShrinkwrap = function (cb) { readShrinkwrap.andInflate(this.idealTree, cb) } -Installer.prototype.normalizeTree = function (log, cb) { - validate('OF', arguments) - log.silly('install', 'normalizeTree') - recalculateMetadata(this.currentTree, log, iferr(cb, function (tree) { - tree.children.forEach(function (child) { - if (child.requiredBy.length === 0) { - child.existing = true - } - }) - cb(null, tree) - })) -} - Installer.prototype.getInstalledModules = function () { return this.differences.filter(function (action) { var mutation = action[0] diff --git a/lib/install/action/extract.js b/lib/install/action/extract.js index ea2d509352cb3..4693f4940bb60 100644 --- a/lib/install/action/extract.js +++ b/lib/install/action/extract.js @@ -15,10 +15,9 @@ const pacote = require('pacote') const pacoteOpts = require('../../config/pacote') const path = require('path') const readJson = BB.promisify(require('read-package-json')) -const updatePackageJson = BB.promisify(require('../update-package-json')) module.exports = extract -function extract (staging, pkg, log, next) { +function extract (staging, pkg, log) { log.silly('extract', packageId(pkg)) const up = npm.config.get('unsafe-perm') const user = up ? null : npm.config.get('user') @@ -49,14 +48,13 @@ function extract (staging, pkg, log, next) { delete metadata.readmeFilename pkg.package = metadata } - return updatePackageJson(pkg, extractTo) }).then(() => { if (pkg.package.bundleDependencies) { return readBundled(pkg, staging, extractTo) } }).then(() => { return gentlyRm(path.join(extractTo, 'node_modules')) - }).then(() => next(), next) + }) } function readBundled (pkg, staging, extractTo) { @@ -101,8 +99,6 @@ function finishModule (bundler, child, stageTo, stageFrom) { if (child.fromBundle === bundler) { return mkdirp(path.dirname(stageTo)).then(() => { return move(stageFrom, stageTo) - }).then(() => { - return updatePackageJson(child, stageTo) }) } else { return fs.statAsync(stageFrom).then(() => { diff --git a/lib/install/action/finalize.js b/lib/install/action/finalize.js index 03a71f4cc03da..283e7f14e1f98 100644 --- a/lib/install/action/finalize.js +++ b/lib/install/action/finalize.js @@ -1,85 +1,83 @@ 'use strict' -var path = require('path') -var rimraf = require('rimraf') -var fs = require('graceful-fs') -var mkdirp = require('mkdirp') -var asyncMap = require('slide').asyncMap -var move = require('../../utils/move.js') -var gentlyRm = require('../../utils/gently-rm') -var moduleStagingPath = require('../module-staging-path.js') +const path = require('path') +const fs = require('graceful-fs') +const Bluebird = require('bluebird') +const rimraf = Bluebird.promisify(require('rimraf')) +const mkdirp = Bluebird.promisify(require('mkdirp')) +const lstat = Bluebird.promisify(fs.lstat) +const readdir = Bluebird.promisify(fs.readdir) +const symlink = Bluebird.promisify(fs.symlink) +const gentlyRm = require('../../utils/gently-rm') +const moduleStagingPath = require('../module-staging-path.js') +const move = require('move-concurrently') +const moveOpts = {fs: fs, Promise: Bluebird, maxConcurrency: 4} +const getRequested = require('../get-requested.js') -module.exports = function (staging, pkg, log, next) { - log.silly('finalize', pkg.path) +module.exports = function (staging, pkg, log) { + log.silly('finalize', pkg.realpath) - var extractedTo = moduleStagingPath(staging, pkg) + const extractedTo = moduleStagingPath(staging, pkg) - var delpath = path.join(path.dirname(pkg.path), '.' + path.basename(pkg.path) + '.DELETE') + const delpath = path.join(path.dirname(pkg.realpath), '.' + path.basename(pkg.realpath) + '.DELETE') + let movedDestAway = false - mkdirp(path.resolve(pkg.path, '..'), whenParentExists) - - function whenParentExists (mkdirEr) { - if (mkdirEr) return next(mkdirEr) - // We stat first, because we can't rely on ENOTEMPTY from Windows. - // Windows, by contrast, gives the generic EPERM of a folder already exists. - fs.lstat(pkg.path, destStatted) - } - - function destStatted (doesNotExist) { - if (doesNotExist) { - move(extractedTo, pkg.path, whenMoved) - } else { - moveAway() - } - } - - function whenMoved (moveEr) { - if (!moveEr) return next() - if (moveEr.code !== 'ENOTEMPTY' && moveEr.code !== 'EEXIST') return next(moveEr) - moveAway() + const requested = pkg.package._requested || getRequested(pkg) + if (requested.type === 'directory') { + return makeParentPath(pkg.path) + .then(() => symlink(pkg.realpath, pkg.path, 'junction')) + } else { + return makeParentPath(pkg.realpath) + .then(moveStagingToDestination) + .then(restoreOldNodeModules) + .catch((err) => { + if (movedDestAway) { + return rimraf(pkg.realpath).then(moveOldDestinationBack).thenReturn(Promise.reject(err)) + } else { + return Promise.reject(err) + } + }) + .then(() => rimraf(delpath)) } - function moveAway () { - move(pkg.path, delpath, whenOldMovedAway) + function makeParentPath (dir) { + return mkdirp(path.dirname(dir)) } - function whenOldMovedAway (moveEr) { - if (moveEr) return next(moveEr) - move(extractedTo, pkg.path, whenConflictMoved) + function moveStagingToDestination () { + return destinationIsClear() + .then(actuallyMoveStaging) + .catch(() => moveOldDestinationAway().then(actuallyMoveStaging)) } - function whenConflictMoved (moveEr) { - // if we got an error we'll try to put back the original module back, - // succeed or fail though we want the original error that caused this - if (moveEr) return move(delpath, pkg.path, function () { next(moveEr) }) - fs.readdir(path.join(delpath, 'node_modules'), makeTarget) + function destinationIsClear () { + return lstat(pkg.realpath).then(() => Bluebird.reject(new Error('destination exists')), () => Bluebird.resolve()) } - function makeTarget (readdirEr, files) { - if (readdirEr) return cleanup() - if (!files.length) return cleanup() - mkdirp(path.join(pkg.path, 'node_modules'), function (mkdirEr) { moveModules(mkdirEr, files) }) + function actuallyMoveStaging () { + return move(extractedTo, pkg.realpath, moveOpts) } - function moveModules (mkdirEr, files) { - if (mkdirEr) return next(mkdirEr) - asyncMap(files, function (file, done) { - var from = path.join(delpath, 'node_modules', file) - var to = path.join(pkg.path, 'node_modules', file) - move(from, to, done) - }, cleanup) + function moveOldDestinationAway () { + return rimraf(delpath).then(() => move(pkg.realpath, delpath, moveOpts)).then(() => { movedDestAway = true }) } - function cleanup (moveEr) { - if (moveEr) return next(moveEr) - rimraf(delpath, afterCleanup) + function moveOldDestinationBack () { + return move(delpath, pkg.realpath, moveOpts).then(() => { movedDestAway = false }) } - function afterCleanup (rimrafEr) { - if (rimrafEr) log.warn('finalize', rimrafEr) - next() + function restoreOldNodeModules () { + if (!movedDestAway) return + return readdir(path.join(delpath, 'node_modules')).catch(() => []).then((modules) => { + if (!modules.length) return + return mkdirp(path.join(pkg.realpath, 'node_modules')).then(() => Bluebird.map(modules, (file) => { + const from = path.join(delpath, 'node_modules', file) + const to = path.join(pkg.realpath, 'node_modules', file) + return move(from, to, moveOpts) + })) + }) } } module.exports.rollback = function (top, staging, pkg, next) { - gentlyRm(pkg.path, false, top, next) + gentlyRm(pkg.realpath, false, top, next) } diff --git a/lib/install/action/refresh-package-json.js b/lib/install/action/refresh-package-json.js new file mode 100644 index 0000000000000..e2ad747e84e68 --- /dev/null +++ b/lib/install/action/refresh-package-json.js @@ -0,0 +1,36 @@ +'use strict' +const path = require('path') +const Bluebird = require('bluebird') +const readJson = Bluebird.promisify(require('read-package-json')) +const updatePackageJson = Bluebird.promisify(require('../update-package-json')) +const getRequested = require('../get-requested.js') + +module.exports = function (staging, pkg, log) { + log.silly('refresh-package-json', pkg.realpath) + + return readJson(path.join(pkg.path, 'package.json'), false).then((metadata) => { + Object.keys(pkg.package).forEach(function (key) { + if (!isEmpty(pkg.package[key])) { + metadata[key] = pkg.package[key] + } + }) + // These two sneak in and it's awful + delete metadata.readme + delete metadata.readmeFilename + + pkg.package = metadata + }).catch(() => 'ignore').then(() => { + const requested = pkg.package._requested || getRequested(pkg) + if (requested.type !== 'directory') { + return updatePackageJson(pkg, pkg.path) + } + }) +} + +function isEmpty (value) { + if (value == null) return true + if (Array.isArray(value)) return !value.length + if (typeof value === 'object') return !Object.keys(value).length + return false +} + diff --git a/lib/install/action/update-linked.js b/lib/install/action/update-linked.js deleted file mode 100644 index 0babe10fdf04b..0000000000000 --- a/lib/install/action/update-linked.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' -var path = require('path') - -function getTop (pkg) { - if (pkg.target && pkg.target.parent) return getTop(pkg.target.parent) - if (pkg.parent) return getTop(pkg.parent) - return pkg.path -} - -module.exports = function (staging, pkg, log, next) { - if (pkg.package.version !== pkg.oldPkg.package.version) { - log.warn('update-linked', path.relative(getTop(pkg), pkg.path), 'needs updating to', pkg.package.version, - 'from', pkg.oldPkg.package.version, "but we can't, as it's a symlink") - } - next() -} diff --git a/lib/install/actions.js b/lib/install/actions.js index acd542bbddd70..23f0c93cb75c3 100644 --- a/lib/install/actions.js +++ b/lib/install/actions.js @@ -24,9 +24,9 @@ actions.prepare = require('./action/prepare.js') actions.finalize = require('./action/finalize.js') actions.remove = require('./action/remove.js') actions.move = require('./action/move.js') -actions['update-linked'] = require('./action/update-linked.js') actions['global-install'] = require('./action/global-install.js') actions['global-link'] = require('./action/global-link.js') +actions['refresh-package-json'] = require('./action/refresh-package-json.js') // FIXME: We wrap actions like three ways to sunday here. // Rewrite this to only work one way. @@ -58,7 +58,9 @@ Object.keys(actions).forEach(function (actionName) { } } function thenRunAction () { - action(staging, pkg, log, andDone(next)) + var done = andDone(next) + var result = action(staging, pkg, log, done) + if (result && result.then) result.then(() => done(), done) } function andDone (cb) { return andFinishTracker(log, andAddParentToErrors(pkg.parent, andHandleOptionalDepErrors(pkg, cb))) @@ -70,7 +72,7 @@ function markAsFailed (pkg) { pkg.failed = true pkg.requires.forEach(function (req) { req.requiredBy = req.requiredBy.filter(function (reqReqBy) { return reqReqBy !== pkg }) - if (req.requiredBy.length === 0 && !req.userRequired && !req.existing) { + if (req.requiredBy.length === 0 && !req.userRequired) { markAsFailed(req) } }) diff --git a/lib/install/copy-tree.js b/lib/install/copy-tree.js index 67a9c687a22d2..394d3ff8059f9 100644 --- a/lib/install/copy-tree.js +++ b/lib/install/copy-tree.js @@ -1,12 +1,12 @@ 'use strict' - +var createNode = require('./node.js').create module.exports = function (tree) { return copyTree(tree, {}) } function copyTree (tree, cache) { if (cache[tree.path]) return cache[tree.path] - var newTree = cache[tree.path] = Object.create(tree) + var newTree = cache[tree.path] = createNode(Object.assign({}, tree)) copyModuleList(newTree, 'children', cache) newTree.children.forEach(function (child) { child.parent = newTree @@ -18,8 +18,10 @@ function copyTree (tree, cache) { function copyModuleList (tree, key, cache) { var newList = [] - tree[key].forEach(function (child) { - newList.push(copyTree(child, cache)) - }) + if (tree[key]) { + tree[key].forEach(function (child) { + newList.push(copyTree(child, cache)) + }) + } tree[key] = newList } diff --git a/lib/install/decompose-actions.js b/lib/install/decompose-actions.js index 401e29c62c8a9..ed582ce4cdaa7 100644 --- a/lib/install/decompose-actions.js +++ b/lib/install/decompose-actions.js @@ -19,7 +19,6 @@ module.exports = function (differences, decomposed, next) { moveSteps(decomposed, pkg, done) break case 'remove': - case 'update-linked': default: defaultSteps(decomposed, cmd, pkg, done) } @@ -27,7 +26,7 @@ module.exports = function (differences, decomposed, next) { } function addSteps (decomposed, pkg, done) { - if (!pkg.fromBundle) { + if (!pkg.fromBundle && !pkg.isLink) { decomposed.push(['fetch', pkg]) decomposed.push(['extract', pkg]) } @@ -38,6 +37,7 @@ function addSteps (decomposed, pkg, done) { decomposed.push(['postinstall', pkg]) } decomposed.push(['finalize', pkg]) + decomposed.push(['refresh-package-json', pkg]) done() } @@ -51,6 +51,7 @@ function moveSteps (decomposed, pkg, done) { decomposed.push(['build', pkg]) decomposed.push(['install', pkg]) decomposed.push(['postinstall', pkg]) + decomposed.push(['refresh-package-json', pkg]) done() } diff --git a/lib/install/deps.js b/lib/install/deps.js index 8b0f18a076b94..fe694f3227096 100644 --- a/lib/install/deps.js +++ b/lib/install/deps.js @@ -2,12 +2,12 @@ const BB = require('bluebird') +var fs = require('fs') var assert = require('assert') var path = require('path') var semver = require('semver') var asyncMap = require('slide').asyncMap var chain = require('slide').chain -var union = require('lodash.union') var iferr = require('iferr') var npa = require('npm-package-arg') var validate = require('aproba') @@ -29,6 +29,7 @@ var moduleName = require('../utils/module-name.js') var isDevDep = require('./is-dev-dep.js') var isProdDep = require('./is-prod-dep.js') var reportOptionalFailure = require('./report-optional-failure.js') +var getSaveType = require('./save.js').getSaveType // The export functions in this module mutate a dependency tree, adding // items to them. @@ -43,6 +44,11 @@ function doesChildVersionMatch (child, requested, requestor) { // prereleases to match * if there are ONLY prereleases if (requested.type === 'range' && requested.fetchSpec === '*') return true + if (requested.type === 'directory') { + if (!child.isLink) return false + return path.relative(child.realpath, requested.fetchSpec) === '' + } + if (!registryTypes[requested.type]) { var childReq = child.package._requested if (!childReq && child.package._from) { @@ -70,85 +76,51 @@ function doesChildVersionMatch (child, requested, requestor) { return semver.satisfies(child.package.version, requested.fetchSpec) } -// TODO: Rename to maybe computeMetadata or computeRelationships -exports.recalculateMetadata = function (tree, log, next) { - recalculateMetadata(tree, log, {}, next) -} - function childDependencySpecifier (tree, name, spec) { return npa.resolve(name, spec, packageRelativePath(tree)) } -function recalculateMetadata (tree, log, seen, next) { - validate('OOOF', arguments) - if (seen[tree.path]) return next() +exports.computeMetadata = computeMetadata +function computeMetadata (tree, seen) { + if (!seen) seen = {} + if (!tree || seen[tree.path]) return seen[tree.path] = true if (tree.parent == null) { resetMetadata(tree) tree.isTop = true } + tree.location = flatNameFromTree(tree) - function markDeps (toMark, done) { - var name = toMark.name - var spec = toMark.spec - var kind = toMark.kind + function findChild (name, spec, kind) { try { var req = childDependencySpecifier(tree, name, spec) } catch (err) { - return done() + return } - if (!req.name) return done() var child = findRequirement(tree, req.name, req) if (child) { resolveWithExistingModule(child, tree) - done(null, child, log) - } else if (kind === 'dep') { - tree.missingDeps[req.name] = req.rawSpec - done() - } else if (kind === 'dev') { - tree.missingDevDeps[req.name] = req.rawSpec - done() - } else { - done() + return true } + return } - function makeMarkable (deps, kind) { - if (!deps) return [] - return Object.keys(deps).map(function (depname) { return { name: depname, spec: deps[depname], kind: kind } }) + const deps = tree.package.dependencies || {} + for (let name of Object.keys(deps)) { + if (findChild(name, deps[name])) continue + tree.missingDeps[name] = deps[name] + } + if (tree.isTop) { + const devDeps = tree.package.devDependencies || {} + for (let name of Object.keys(devDeps)) { + if (findChild(name, devDeps[name])) continue + tree.missingDevDeps[name] = devDeps[name] + } } - // Ensure dependencies and dev dependencies are marked as required - var tomark = makeMarkable(tree.package.dependencies, 'dep') - if (tree.isTop) tomark = union(tomark, makeMarkable(tree.package.devDependencies, 'dev')) - - // Ensure any children ONLY from a shrinkwrap are also included - var childrenOnlyInShrinkwrap = tree.children.filter(function (child) { - return child.fromShrinkwrap && - !tree.package.dependencies[child.package.name] && - !tree.package.devDependencies[child.package.name] - }) - var tomarkOnlyInShrinkwrap = childrenOnlyInShrinkwrap.map(function (child) { - var name = child.package.name - var matched = child.package._spec.match(/^@?[^@]+@(.*)$/) - var spec = matched ? matched[1] : child.package._spec - var kind = tree.package.dependencies[name] ? 'dep' - : tree.package.devDependencies[name] ? 'dev' - : 'dep' - return { name: name, spec: spec, kind: kind } - }) - tomark = union(tomark, tomarkOnlyInShrinkwrap) - - // Don't bother trying to recalc children of failed deps - tree.children = tree.children.filter(function (child) { return !child.failed }) + tree.children.filter((child) => !child.removed && !child.failed).forEach((child) => computeMetadata(child, seen)) - chain([ - [asyncMap, tomark, markDeps], - [asyncMap, tree.children, function (child, done) { recalculateMetadata(child, log, seen, done) }] - ], function () { - tree.location = flatNameFromTree(tree) - next(null, tree) - }) + return tree } function isDep (tree, child) { @@ -203,12 +175,6 @@ function removeObsoleteDep (child, log) { }) } -function matchingDep (tree, name) { - if (tree.package.dependencies && tree.package.dependencies[name]) return tree.package.dependencies[name] - if (tree.package.devDependencies && tree.package.devDependencies[name]) return tree.package.devDependencies[name] - return -} - function packageRelativePath (tree) { if (!tree) return '' var requested = tree.package._requested || {} @@ -216,17 +182,30 @@ function packageRelativePath (tree) { return isLocal ? requested.fetchSpec : tree.path } +function matchingDep (tree, name) { + if (tree.package.dependencies && tree.package.dependencies[name]) return tree.package.dependencies[name] + if (tree.package.devDependencies && tree.package.devDependencies[name]) return tree.package.devDependencies[name] + return +} + exports.getAllMetadata = function (args, tree, where, next) { asyncMap(args, function (arg, done) { - function fetchMetadataWithVersion () { - var version = matchingDep(tree, arg) - var spec = version == null ? arg : arg + '@' + version - return fetchPackageMetadata(spec, where, done) - } - if (tree && arg.lastIndexOf('@') <= 0) { - return fetchMetadataWithVersion() + var spec = npa(arg) + if (spec.type !== 'file' && spec.type !== 'directory' && (spec.name == null || spec.rawSpec === '')) { + return fs.stat(path.join(arg, 'package.json'), (err) => { + if (err) { + var version = matchingDep(tree, spec.name) + if (version) { + return fetchPackageMetadata(npa.resolve(spec.name, version), where, done) + } else { + return fetchPackageMetadata(spec, where, done) + } + } else { + return fetchPackageMetadata(npa('file:' + arg), where, done) + } + }) } else { - return fetchPackageMetadata(arg, where, done) + return fetchPackageMetadata(spec, where, done) } }, next) } @@ -242,11 +221,11 @@ exports.loadRequestedDeps = function (args, tree, saveToDependencies, log, next) child.isGlobal = true } var childName = moduleName(child) - child.saveSpec = computeVersionSpec(child) + child.saveSpec = computeVersionSpec(tree, child) if (saveToDependencies) { - tree.package[saveToDependencies][childName] = child.saveSpec + tree.package[getSaveType(tree, child)][childName] = child.saveSpec } - if (saveToDependencies === 'optionalDependencies') { + if (getSaveType(tree, child) === 'optionalDependencies') { tree.package.dependencies[childName] = child.saveSpec } child.userRequired = true @@ -262,8 +241,8 @@ exports.loadRequestedDeps = function (args, tree, saveToDependencies, log, next) }, andForEachChild(loadDeps, andFinishTracker(log, next))) } -function computeVersionSpec (child) { - validate('O', arguments) +function computeVersionSpec (tree, child) { + validate('OO', arguments) var requested = child.package._requested if (requested.registry) { var version = child.package.version @@ -274,6 +253,8 @@ function computeVersionSpec (child) { rangeDescriptor = npm.config.get('save-prefix') } return rangeDescriptor + version + } else if (requested.type === 'directory' || requested.type === 'file') { + return 'file:' + path.relative(tree.path, requested.fetchSpec) } else { return requested.saveSpec } @@ -295,11 +276,18 @@ exports.removeDeps = function (args, tree, saveToDependencies, log, next) { var pkgName = moduleName(pkg) var toRemove = tree.children.filter(moduleNameMatches(pkgName)) var pkgToRemove = toRemove[0] || createChild({package: {name: pkgName}}) - if (saveToDependencies) { - replaceModuleByPath(tree, 'removed', pkgToRemove) - pkgToRemove.save = saveToDependencies + if (tree.isTop) { + if (saveToDependencies) { + pkgToRemove.save = getSaveType(tree, pkg) + delete tree.package[pkgToRemove.save][pkgName] + if (pkgToRemove.save === 'optionalDependencies') { + delete tree.package.dependencies[pkgName] + } + replaceModuleByPath(tree, 'removed', pkgToRemove) + } + pkgToRemove.requiredBy = pkgToRemove.requiredBy.filter((parent) => parent !== tree) } - removeObsoleteDep(pkgToRemove) + if (pkgToRemove.requiredBy.length === 0) removeObsoleteDep(pkgToRemove) }) log.finish() next() @@ -400,8 +388,8 @@ function prefetchDeps (tree, deps, log, next) { return npa.resolve(dep, deps[dep]) }).filter((dep) => { return dep.registry && - !seen[dep.toString()] && - !findRequirement(tree, dep.name, dep) + !seen[dep.toString()] && + !findRequirement(tree, dep.name, dep) }) if (skipOptional) { var optDeps = pkg.optionalDependencies || {} @@ -561,13 +549,15 @@ function resolveWithNewModule (pkg, tree, log, next) { return isInstallable(pkg, iferr(next, function () { addBundled(pkg, iferr(next, function () { var parent = earliestInstallable(tree, tree, pkg) || tree + var isLink = pkg._requested.type === 'directory' var child = createChild({ package: pkg, parent: parent, - path: path.join(parent.path, 'node_modules', pkg.name), - realpath: path.resolve(parent.realpath, 'node_modules', pkg.name), + path: path.join(parent.isLink ? parent.realpath : parent.path, 'node_modules', pkg.name), + realpath: isLink ? pkg._requested.fetchSpec : path.join(parent.realpath, 'node_modules', pkg.name), children: pkg._bundled || [], - isLink: tree.isLink, + isLink: isLink, + isInLink: parent.isLink, knownInstallable: true }) delete pkg._bundled @@ -621,7 +611,7 @@ var findRequirement = exports.findRequirement = function (tree, name, requested, validate('OSO', [tree, name, requested]) if (!requestor) requestor = tree var nameMatch = function (child) { - return moduleName(child) === name && child.parent && !child.removed + return moduleName(child) === name && child.parent && !child.removed && !child.failed } var versionMatch = function (child) { return doesChildVersionMatch(child, requested, requestor) @@ -689,5 +679,7 @@ var earliestInstallable = exports.earliestInstallable = function (requiredBy, tr if (npm.config.get('global-style') && tree.parent.isTop) return tree if (npm.config.get('legacy-bundling')) return tree + if (!process.env.NODE_PRESERVE_SYMLINKS && /^[.][.][\\/]/.test(path.relative(tree.parent.realpath, tree.realpath))) return tree + return (earliestInstallable(requiredBy, tree.parent, pkg) || tree) } diff --git a/lib/install/diff-trees.js b/lib/install/diff-trees.js index f99efab5cada9..67fe72d04471c 100644 --- a/lib/install/diff-trees.js +++ b/lib/install/diff-trees.js @@ -50,15 +50,6 @@ module.exports = function (oldTree, newTree, differences, log, next) { next() } -function isLink (node) { - return node && node.isLink -} - -function requiredByAllLinked (node) { - if (!node.requiredBy.length) return false - return node.requiredBy.filter(isLink).length === node.requiredBy.length -} - function isNotTopOrExtraneous (node) { return !node.isTop && !node.userRequired && !node.existing } @@ -136,16 +127,9 @@ var diffTrees = module.exports._diffTrees = function (oldTree, newTree) { Object.keys(flatNewTree).forEach(function (path) { var pkg = flatNewTree[path] pkg.oldPkg = flatOldTree[path] - pkg.isInLink = (pkg.oldPkg && isLink(pkg.oldPkg.parent)) || - (pkg.parent && isLink(pkg.parent)) || - requiredByAllLinked(pkg) if (pkg.oldPkg) { if (!pkg.userRequired && pkgAreEquiv(pkg.oldPkg.package, pkg.package)) return - if (!pkg.isInLink && (isLink(pkg.oldPkg) || isLink(pkg))) { - setAction(differences, 'update-linked', pkg) - } else { - setAction(differences, 'update', pkg) - } + setAction(differences, 'update', pkg) } else { var vername = getUniqueId(pkg.package) var removing = toRemoveByUniqueId[vername] && toRemoveByUniqueId[vername].length @@ -155,7 +139,7 @@ var diffTrees = module.exports._diffTrees = function (oldTree, newTree) { pkg.fromPath = toRemove[flatname].path setAction(differences, 'move', pkg) delete toRemove[flatname] - } else { + } else if (!(pkg.isInLink && pkg.fromBundle)) { setAction(differences, 'add', pkg) } } diff --git a/lib/install/filter-invalid-actions.js b/lib/install/filter-invalid-actions.js deleted file mode 100644 index beac30b7b0019..0000000000000 --- a/lib/install/filter-invalid-actions.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' -var path = require('path') -var validate = require('aproba') -var log = require('npmlog') -var packageId = require('../utils/package-id.js') - -module.exports = function (top, differences, next) { - validate('SAF', arguments) - var action - var keep = [] - - differences.forEach(function (action) { - var cmd = action[0] - var pkg = action[1] - if (cmd === 'remove') { - pkg.removing = true - } - }) - - /*eslint no-cond-assign:0*/ - while (action = differences.shift()) { - var cmd = action[0] - var pkg = action[1] - if (pkg.isInLink || (pkg.parent && (pkg.parent.target || pkg.parent.isLink))) { - // we want to skip warning if this is a child of another module that we're removing - if (!pkg.parent.removing) { - log.verbose('skippingAction', 'Module is inside a symlinked module: not running ' + - cmd + ' ' + packageId(pkg) + ' ' + path.relative(top, pkg.path)) - } - } else { - keep.push(action) - } - } - differences.push.apply(differences, keep) - next() -} diff --git a/lib/install/get-requested.js b/lib/install/get-requested.js new file mode 100644 index 0000000000000..2726766687ddf --- /dev/null +++ b/lib/install/get-requested.js @@ -0,0 +1,12 @@ +'use strict' +const npa = require('npm-package-arg') +const moduleName = require('../utils/module-name.js') + +module.exports = function (child) { + if (!child.requiredBy.length) return + const reqBy = child.requiredBy[0] + const deps = reqBy.package.dependencies || {} + const devDeps = reqBy.package.devDependencies || {} + const name = moduleName(child) + return npa.resolve(name, deps[name] || devDeps[name]) +} diff --git a/lib/install/inflate-bundled.js b/lib/install/inflate-bundled.js index bacf6d2e45472..cf541106aed1d 100644 --- a/lib/install/inflate-bundled.js +++ b/lib/install/inflate-bundled.js @@ -8,10 +8,10 @@ module.exports = function inflateBundled (bundler, parent, children) { children.forEach(function (child) { reset(child) child.fromBundle = bundler - child.package._inBundle = true + child.isInLink = bundler.isLink child.parent = parent child.path = childPath(parent.path, child) - child.realpath = childPath(parent.path, child) + child.realpath = childPath(parent.realpath, child) child.isLink = child.isLink || parent.isLink || parent.target inflateBundled(bundler, child, child.children) }) diff --git a/lib/install/inflate-shrinkwrap.js b/lib/install/inflate-shrinkwrap.js index ec67b87d52cc5..23b8115d76650 100644 --- a/lib/install/inflate-shrinkwrap.js +++ b/lib/install/inflate-shrinkwrap.js @@ -59,14 +59,33 @@ function inflateShrinkwrap (topPath, tree, swdeps) { }) } -function inflatableChild (child, name, topPath, tree, sw, requested) { - if (childIsEquivalent(sw, requested, child)) { +function normalizePackageDataNoErrors (pkg) { + try { + normalizePackageData(pkg) + } catch (ex) { + // don't care + } +} + +function inflatableChild (onDiskChild, name, topPath, tree, sw, requested) { + validate('OSSOOO|ZSSOOO', arguments) + if (onDiskChild && childIsEquivalent(sw, requested, onDiskChild)) { // The version on disk matches the shrinkwrap entry. - if (!child.fromShrinkwrap) child.fromShrinkwrap = requested.raw - if (sw.dev) child.shrinkwrapDev = true - tree.children.push(child) - annotateMetadata(child.package, requested, requested.raw, topPath) - return BB.resolve(child) + if (!onDiskChild.fromShrinkwrap) onDiskChild.fromShrinkwrap = true + if (sw.dev) onDiskChild.shrinkwrapDev = true + onDiskChild.package._requested = requested + onDiskChild.package._spec = requested.rawSpec + onDiskChild.package._where = topPath + onDiskChild.fromBundle = sw.bundled ? tree.fromBundle || tree : null + if (!onDiskChild.package._args) onDiskChild.package._args = [] + onDiskChild.package._args.push([String(requested), topPath]) + // non-npm registries can and will return unnormalized data, plus + // even the npm registry may have package data normalized with older + // normalization rules. This ensures we get package data in a consistent, + // stable format. + normalizePackageDataNoErrors(onDiskChild.package) + tree.children.push(onDiskChild) + return BB.resolve(onDiskChild) } else if (sw.version && sw.integrity) { // The shrinkwrap entry has an integrity field. We can fake a pkg to get // the installer to do a content-address fetch from the cache, if possible. @@ -85,10 +104,13 @@ function makeFakeChild (name, topPath, tree, sw, requested) { name: name, version: sw.version, _resolved: sw.resolved, + _requested: requested, _optional: sw.optional, - _inBundle: sw.bundled, _integrity: sw.integrity, - _from: from + _from: from, + _spec: requested.rawSpec, + _where: topPath, + _args: [[requested.toString(), topPath]] } let bundleAdded = BB.resolve() if (Object.keys(sw.dependencies || {}).some((d) => { @@ -103,11 +125,13 @@ function makeFakeChild (name, topPath, tree, sw, requested) { loaded: true, parent: tree, children: pkg._bundled || [], - fromShrinkwrap: pkg._requested, + fromShrinkwrap: true, + fromBundle: sw.bundled ? tree.fromBundle || tree : null, path: childPath(tree.path, pkg), - realpath: childPath(tree.realpath, pkg) + realpath: childPath(tree.realpath, pkg), + location: tree.location + '/' + pkg.name, + isInLink: tree.isLink }) - annotateMetadata(child.package, requested, requested.raw, topPath) tree.children.push(child) if (pkg._bundled) { delete pkg._bundled @@ -125,14 +149,18 @@ function fetchChild (topPath, tree, sw, requested) { pkg._optional = optional return addBundled(pkg).then(() => pkg) }).then((pkg) => { + var isLink = pkg._requested.type === 'directory' const child = createChild({ package: pkg, loaded: true, parent: tree, - fromShrinkwrap: pkg._requested, + fromShrinkwrap: requested, path: childPath(tree.path, pkg), - realpath: childPath(tree.realpath, pkg), - children: pkg._bundled || [] + realpath: isLink ? requested.fetchSpec : childPath(tree.realpath, pkg), + children: pkg._bundled || [], + location: tree.location + '/' + pkg.name, + isLink: isLink, + isInLink: tree.isLink }) tree.children.push(child) if (pkg._bundled) { @@ -150,22 +178,3 @@ function childIsEquivalent (sw, requested, child) { if (!requested.registry && sw.from) return child.package._from === sw.from return child.package.version === sw.version } - -module.exports.annotateMetadata = annotateMetadata -function annotateMetadata (pkg, requested, spec, where) { - validate('OOSS', arguments) - pkg._requested = requested - pkg._spec = spec - pkg._where = where - if (!pkg._args) pkg._args = [] - pkg._args.push([requested, where]) - // non-npm registries can and will return unnormalized data, plus - // even the npm registry may have package data normalized with older - // normalization rules. This ensures we get package data in a consistent, - // stable format. - try { - normalizePackageData(pkg) - } catch (ex) { - // don't care - } -} diff --git a/lib/install/node.js b/lib/install/node.js index 64414659d1c58..305f705ef3bc5 100644 --- a/lib/install/node.js +++ b/lib/install/node.js @@ -18,10 +18,10 @@ var defaultTemplate = { realpath: null, location: null, userRequired: false, - existing: false, save: false, saveSpec: null, - isTop: false + isTop: false, + fromBundle: false } function isLink (node) { @@ -38,8 +38,21 @@ var create = exports.create = function (node, template) { if (node[key] != null) return node[key] = template[key] }) - if (isLink(node.parent)) { - node.isLink = true + if (node.package) { + // isLink is true for the symlink and everything inside it. + // by contrast, isInLink is true for only the things inside a link + if (node.isLink == null && isLink(node.parent)) { + node.isLink = true + node.isInLink = true + } else if (node.isLink == null) { + node.isLink = false + node.isInLink = false + } + if (node.fromBundle == null && node.package) { + node.fromBundle = node.package._inBundle + } else if (node.fromBundle == null) { + node.fromBundle = false + } } return node } diff --git a/lib/install/read-shrinkwrap.js b/lib/install/read-shrinkwrap.js index f881a46ca0c9c..bf941a49c6f7c 100644 --- a/lib/install/read-shrinkwrap.js +++ b/lib/install/read-shrinkwrap.js @@ -17,7 +17,7 @@ function readShrinkwrap (child, next) { BB.join( readLockfile('npm-shrinkwrap.json', child), // Don't read non-root lockfiles - !child.parent && readLockfile('package-lock.json', child), + child.isTop && readLockfile('package-lock.json', child), (shrinkwrap, lockfile) => { if (shrinkwrap && lockfile) { log.warn('read-shrinkwrap', 'Ignoring package-lock.json because there is already an npm-shrinkwrap.json. Please use only one of the two.') diff --git a/lib/install/realize-shrinkwrap-specifier.js b/lib/install/realize-shrinkwrap-specifier.js index 0a2423b0cff7e..91030bfa826fe 100644 --- a/lib/install/realize-shrinkwrap-specifier.js +++ b/lib/install/realize-shrinkwrap-specifier.js @@ -2,13 +2,17 @@ var npa = require('npm-package-arg') module.exports = function (name, sw, where) { - if (sw.resolved) { - return npa.resolve(name, sw.resolved, where) - } else if (sw.from) { - try { + try { + if (sw.version && sw.integrity) { + return npa.resolve(name, sw.version, where) + } + if (sw.resolved) { + return npa.resolve(name, sw.resolved, where) + } + if (sw.from) { var spec = npa(sw.from, where) if (!spec.registry) return spec - } catch (_) { } - } + } + } catch (_) { } return npa.resolve(name, sw.version, where) } diff --git a/lib/install/save.js b/lib/install/save.js index f9d57aa2b2ce2..5d5f4e7f7a920 100644 --- a/lib/install/save.js +++ b/lib/install/save.js @@ -43,7 +43,7 @@ function andWarnErrors (cb) { function saveShrinkwrap (tree, next) { validate('OF', arguments) - createShrinkwrap(tree.path, tree.package, {silent: false}, next) + createShrinkwrap(tree, {silent: false}, next) } function savePackageJson (args, tree, next) { @@ -61,31 +61,31 @@ function savePackageJson (args, tree, next) { fs.readFile(saveTarget, 'utf8', iferr(next, function (packagejson) { const indent = detectIndent(packagejson).indent || ' ' try { - packagejson = parseJSON(packagejson) + tree.package = parseJSON(packagejson) } catch (ex) { return next(ex) } // If we're saving bundled deps, normalize the key before we start if (saveBundle) { - var bundle = packagejson.bundleDependencies || packagejson.bundledDependencies - delete packagejson.bundledDependencies + var bundle = tree.package.bundleDependencies || tree.package.bundledDependencies + delete tree.package.bundledDependencies if (!Array.isArray(bundle)) bundle = [] } var toSave = getThingsToSave(tree) - var toRemove = getThingsToRemove(args, tree) + var toRemove = getThingsToRemove(tree) var savingTo = {} toSave.forEach(function (pkg) { savingTo[pkg.save] = true }) toRemove.forEach(function (pkg) { savingTo[pkg.save] = true }) Object.keys(savingTo).forEach(function (save) { - if (!packagejson[save]) packagejson[save] = {} + if (!tree.package[save]) tree.package[save] = {} }) log.verbose('saving', toSave) toSave.forEach(function (pkg) { - packagejson[pkg.save][pkg.name] = pkg.spec + tree.package[pkg.save][pkg.name] = pkg.spec if (saveBundle) { var ii = bundle.indexOf(pkg.name) if (ii === -1) bundle.push(pkg.name) @@ -93,35 +93,47 @@ function savePackageJson (args, tree, next) { }) toRemove.forEach(function (pkg) { - delete packagejson[pkg.save][pkg.name] + delete tree.package[pkg.save][pkg.name] if (saveBundle) { bundle = without(bundle, pkg.name) } }) Object.keys(savingTo).forEach(function (key) { - packagejson[key] = deepSortObject(packagejson[key]) + tree.package[key] = deepSortObject(tree.package[key]) }) if (saveBundle) { - packagejson.bundledDependencies = deepSortObject(bundle) + tree.package.bundleDependencies = deepSortObject(bundle) } - var json = JSON.stringify(packagejson, null, indent) + '\n' + var json = JSON.stringify(tree.package, null, indent) + '\n' writeFileAtomic(saveTarget, json, next) })) } -var getSaveType = exports.getSaveType = function (args) { - validate('A', arguments) +exports.getSaveType = function (tree, arg) { + if (arguments.length) validate('OO', arguments) var globalInstall = npm.config.get('global') var noSaveFlags = !npm.config.get('save') && !npm.config.get('save-dev') && !npm.config.get('save-optional') if (globalInstall || noSaveFlags) return null - if (npm.config.get('save-optional')) return 'optionalDependencies' - else if (npm.config.get('save-dev')) return 'devDependencies' - else return 'dependencies' + if (npm.config.get('save-optional')) { + return 'optionalDependencies' + } else if (npm.config.get('save-dev')) { + return 'devDependencies' + } else { + if (arg) { + var name = moduleName(arg) + if (tree.package.optionalDependencies[name]) { + return 'optionalDependencies' + } else if (tree.package.devDependencies[name]) { + return 'devDependencies' + } + } + return 'dependencies' + } } function getThingsToSave (tree) { @@ -138,8 +150,8 @@ function getThingsToSave (tree) { return toSave } -function getThingsToRemove (args, tree) { - validate('AO', arguments) +function getThingsToRemove (tree) { + validate('O', arguments) if (!tree.removed) return [] var toRemove = tree.removed.map(function (child) { return { @@ -147,12 +159,5 @@ function getThingsToRemove (args, tree) { save: child.save } }) - var saveType = getSaveType(args) - args.forEach(function (arg) { - toRemove.push({ - name: arg, - save: saveType - }) - }) return toRemove } diff --git a/lib/install/update-package-json.js b/lib/install/update-package-json.js index eee530c3cd803..21b88bead7cc9 100644 --- a/lib/install/update-package-json.js +++ b/lib/install/update-package-json.js @@ -27,13 +27,13 @@ module.exports = function (mod, buildpath, next) { } }) .concat(mod.userRequired ? ['#USER'] : []) - .concat(mod.existing ? ['#EXISTING'] : []) .sort() pkg._location = mod.location pkg._phantomChildren = {} Object.keys(mod.phantomChildren).sort().forEach(function (name) { pkg._phantomChildren[name] = mod.phantomChildren[name].package.version }) + pkg._inBundle = !!mod.fromBundle // sort keys that are known safe to sort to produce more consistent output sortKeys.forEach(function (key) { diff --git a/lib/ls.js b/lib/ls.js index 478b914bbc81c..b993dd6235ebc 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -9,7 +9,6 @@ module.exports = exports = ls var path = require('path') var url = require('url') var readPackageTree = require('read-package-tree') -var log = require('npmlog') var archy = require('archy') var semver = require('semver') var color = require('ansicolors') @@ -19,7 +18,7 @@ var sortedObject = require('sorted-object') var extend = Object.assign || require('util')._extend var npm = require('./npm.js') var mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js') -var recalculateMetadata = require('./install/deps.js').recalculateMetadata +var computeMetadata = require('./install/deps.js').computeMetadata var packageId = require('./utils/package-id.js') var usage = require('./utils/usage') var output = require('./utils/output.js') @@ -37,14 +36,14 @@ function ls (args, silent, cb) { silent = false } var dir = path.resolve(npm.dir, '..') - readPackageTree(dir, andRecalculateMetadata(iferr(cb, function (physicalTree) { + readPackageTree(dir, andComputeMetadata(iferr(cb, function (physicalTree) { lsFromTree(dir, physicalTree, args, silent, cb) }))) } -function andRecalculateMetadata (next) { +function andComputeMetadata (next) { return function (er, tree) { - recalculateMetadata(tree || {}, log, next) + next(null, computeMetadata(tree || {})) } } diff --git a/lib/outdated.js b/lib/outdated.js index 01cf39d103f52..7d5cfba86e7d3 100644 --- a/lib/outdated.js +++ b/lib/outdated.js @@ -23,7 +23,6 @@ outdated.completion = require('./utils/completion/installed-deep.js') var os = require('os') var url = require('url') var path = require('path') -var log = require('npmlog') var readPackageTree = require('read-package-tree') var readJson = require('read-package-json') var asyncMap = require('slide').asyncMap @@ -38,7 +37,7 @@ var npm = require('./npm.js') var long = npm.config.get('long') var mapToRegistry = require('./utils/map-to-registry.js') var isExtraneous = require('./install/is-extraneous.js') -var recalculateMetadata = require('./install/deps.js').recalculateMetadata +var computeMetadata = require('./install/deps.js').computeMetadata var moduleName = require('./utils/module-name.js') var output = require('./utils/output.js') var ansiTrim = require('./utils/ansi-trim') @@ -59,10 +58,10 @@ function uniq (list) { return uniqed } -function andRecalculateMetadata (next) { +function andComputeMetadata (next) { return function (er, tree) { if (er) return next(er) - recalculateMetadata(tree, log, next) + next(null, computeMetadata(tree)) } } @@ -76,7 +75,7 @@ function outdated (args, silent, cb) { // default depth for `outdated` is 0 (cf. `ls`) if (npm.config.get('depth') === Infinity) npm.config.set('depth', 0) - readPackageTree(dir, andRecalculateMetadata(function (er, tree) { + readPackageTree(dir, andComputeMetadata(function (er, tree) { if (!tree) return cb(er) mutateIntoLogicalTree(tree) outdated_(args, '', tree, {}, 0, function (er, list) { diff --git a/lib/prune.js b/lib/prune.js index a590457a18087..39d1c8ffb7571 100644 --- a/lib/prune.js +++ b/lib/prune.js @@ -15,6 +15,7 @@ var isDev = require('./install/is-dev-dep.js') var removeDeps = require('./install/deps.js').removeDeps var loadExtraneous = require('./install/deps.js').loadExtraneous var chain = require('slide').chain +var computeMetadata = require('./install/deps.js').computeMetadata prune.completion = require('./utils/completion/installed-deep.js') @@ -34,6 +35,7 @@ Pruner.prototype.loadAllDepsIntoIdealTree = function (cb) { var cg = this.progress['loadIdealTree:loadAllDepsIntoIdealTree'] var steps = [] + computeMetadata(this.idealTree) var self = this var excludeDev = npm.config.get('production') || /^prod(uction)?$/.test(npm.config.get('only')) function shouldPrune (child) { @@ -54,7 +56,7 @@ Pruner.prototype.loadAllDepsIntoIdealTree = function (cb) { function nameObj (name) { return {name: name} } - var toPrune = this.currentTree.children.filter(shouldPrune).map(getModuleName).filter(matchesArg).map(nameObj) + var toPrune = this.idealTree.children.filter(shouldPrune).map(getModuleName).filter(matchesArg).map(nameObj) steps.push( [removeDeps, toPrune, this.idealTree, null, cg.newGroup('removeDeps')], diff --git a/lib/shrinkwrap.js b/lib/shrinkwrap.js index 5cbba21379036..eeac91f248cd2 100644 --- a/lib/shrinkwrap.js +++ b/lib/shrinkwrap.js @@ -17,16 +17,14 @@ const move = require('move-concurrently') const npm = require('./npm.js') const packageId = require('./utils/package-id.js') const path = require('path') -const readPackageJson = require('read-package-json') const readPackageTree = require('read-package-tree') -const recalculateMetadata = require('./install/deps.js').recalculateMetadata const ssri = require('ssri') const validate = require('aproba') -const validatePeerDeps = require('./install/deps.js').validatePeerDeps +const id = require('./install/deps.js') const writeFileAtomic = require('write-file-atomic') - const SHRINKWRAP = 'npm-shrinkwrap.json' const PKGLOCK = 'package-lock.json' +const getRequested = require('./install/get-requested.js') // emit JSON describing versions of all packages currently installed (for later // use with shrinkwrap install) @@ -43,8 +41,6 @@ function shrinkwrap (args, silent, cb) { log.warn('shrinkwrap', "doesn't take positional args") } - var packagePath = path.join(npm.localPrefix, 'package.json') - move( path.resolve(npm.prefix, PKGLOCK), path.resolve(npm.prefix, SHRINKWRAP), @@ -53,42 +49,40 @@ function shrinkwrap (args, silent, cb) { log.notice('', `${PKGLOCK} has been renamed to ${SHRINKWRAP}. ${SHRINKWRAP} will be used for future installations.`) return fs.readFileAsync(path.resolve(npm.prefix, SHRINKWRAP)).then((d) => { return JSON.parse(d) - }) + }).then(() => cb(), cb) }).catch({code: 'ENOENT'}, () => { - readPackageJson(packagePath, iferr(cb, function (pkg) { - createShrinkwrap(npm.localPrefix, pkg, { + readPackageTree(npm.localPrefix, andComputeMetadata(iferr(cb, function (tree) { + createShrinkwrap(tree, { silent, defaultFile: SHRINKWRAP }, cb) - })) - }).then((data) => cb(null, data), cb) + }))) + }) } module.exports.createShrinkwrap = createShrinkwrap -function createShrinkwrap (dir, pkg, opts, cb) { +function createShrinkwrap (tree, opts, cb) { opts = opts || {} - lifecycle(pkg, 'preshrinkwrap', dir, function () { - readPackageTree(dir, andRecalculateMetadata(iferr(cb, function (tree) { - var pkginfo = treeToShrinkwrap(tree) - - chain([ - [lifecycle, tree.package, 'shrinkwrap', dir], - [shrinkwrap_, dir, pkginfo, opts], - [lifecycle, tree.package, 'postshrinkwrap', dir] - ], iferr(cb, function (data) { - cb(null, data[0]) - })) - }))) + lifecycle(tree.package, 'preshrinkwrap', tree.path, function () { + var pkginfo = treeToShrinkwrap(tree) + + chain([ + [lifecycle, tree.package, 'shrinkwrap', tree.path], + [shrinkwrap_, tree.path, pkginfo, opts], + [lifecycle, tree.package, 'postshrinkwrap', tree.path] + ], iferr(cb, function (data) { + cb(null, data[0]) + })) }) } -function andRecalculateMetadata (next) { +function andComputeMetadata (next) { validate('F', arguments) return function (er, tree) { validate('EO', arguments) if (er) return next(er) - recalculateMetadata(tree, log, next) + next(null, id.computeMetadata(tree)) } } @@ -99,14 +93,14 @@ function treeToShrinkwrap (tree) { if (tree.package.version) pkginfo.version = tree.package.version var problems = [] if (tree.children.length) { - shrinkwrapDeps(problems, pkginfo.dependencies = {}, tree) + shrinkwrapDeps(problems, pkginfo.dependencies = {}, tree, tree) } if (problems.length) pkginfo.problems = problems return pkginfo } -function shrinkwrapDeps (problems, deps, tree, seen) { - validate('AOO', [problems, deps, tree]) +function shrinkwrapDeps (problems, deps, top, tree, seen) { + validate('AOOO', [problems, deps, top, tree]) if (!seen) seen = {} if (seen[tree.path]) return seen[tree.path] = true @@ -123,11 +117,20 @@ function shrinkwrapDeps (problems, deps, tree, seen) { tree.children.sort(function (aa, bb) { return moduleName(aa).localeCompare(moduleName(bb)) }).forEach(function (child) { var childIsOnlyDev = isOnlyDev(child) var pkginfo = deps[moduleName(child)] = {} - pkginfo.version = child.package.version - if (child.package._inBundle) { + var req = child.package._requested || getRequested(child) + if (req.type === 'directory' || req.type === 'file') { + pkginfo.version = 'file:' + path.relative(top.path, child.package._resolved || req.fetchSpec) + } else if (!req.registry) { + pkginfo.version = child.package._resolved + } else { + pkginfo.version = child.package.version + } + if (child.fromBundle || child.isInLink) { pkginfo.bundled = true } else { - pkginfo.resolved = child.package._resolved + if (req.registry) { + pkginfo.resolved = child.package._resolved + } pkginfo.integrity = child.package._integrity if (!pkginfo.integrity && child.package._shasum) { pkginfo.integrity = ssri.fromHex(child.package._shasum, 'sha1') @@ -138,13 +141,13 @@ function shrinkwrapDeps (problems, deps, tree, seen) { if (isExtraneous(child)) { problems.push('extraneous: ' + child.package._id + ' ' + child.path) } - validatePeerDeps(child, function (tree, pkgname, version) { + id.validatePeerDeps(child, function (tree, pkgname, version) { problems.push('peer invalid: ' + pkgname + '@' + version + ', required by ' + child.package._id) }) if (child.children.length) { pkginfo.dependencies = {} - shrinkwrapDeps(problems, pkginfo.dependencies, child, seen) + shrinkwrapDeps(problems, pkginfo.dependencies, top, child, seen) } }) } diff --git a/lib/uninstall.js b/lib/uninstall.js index 802cf7a28408f..9e3d91ac40bc6 100644 --- a/lib/uninstall.js +++ b/lib/uninstall.js @@ -64,11 +64,10 @@ Uninstaller.prototype.loadArgMetadata = function (next) { Uninstaller.prototype.loadAllDepsIntoIdealTree = function (cb) { validate('F', arguments) log.silly('uninstall', 'loadAllDepsIntoIdealTree') - var saveDeps = getSaveType(this.args) + var saveDeps = getSaveType() var cg = this.progress['loadIdealTree:loadAllDepsIntoIdealTree'] var steps = [] - steps.push( [removeDeps, this.args, this.idealTree, saveDeps, cg.newGroup('removeDeps')], [loadExtraneous, this.idealTree, cg.newGroup('loadExtraneous')]) diff --git a/lib/utils/tar.js b/lib/utils/tar.js index d88c8d5320548..7ebc9d6875cdf 100644 --- a/lib/utils/tar.js +++ b/lib/utils/tar.js @@ -66,14 +66,11 @@ function pack (tarball, folder, pkg, cb) { // we require this at runtime due to load-order issues, because recursive // requires fail if you replace the exports object, and we do, not in deps, but // in a dep of it. - var recalculateMetadata = require('../install/deps.js').recalculateMetadata + var computeMetadata = require('../install/deps.js').computeMetadata readPackageTree(folder, pulseTillDone('pack:readTree:' + packageId(pkg), iferr(cb, function (tree) { - var recalcGroup = log.newGroup('pack:recalc:' + packageId(pkg)) - recalculateMetadata(tree, recalcGroup, iferr(cb, function () { - recalcGroup.finish() - pack_(tarball, folder, tree, pkg, pulseTillDone('pack:' + packageId(pkg), cb)) - })) + computeMetadata(tree) + pack_(tarball, folder, tree, pkg, pulseTillDone('pack:' + packageId(pkg), cb)) }))) } }) diff --git a/test/tap/add-remote-git-shrinkwrap.js b/test/tap/add-remote-git-shrinkwrap.js index 8bd53cb5e091f..c7fb2f9b961aa 100644 --- a/test/tap/add-remote-git-shrinkwrap.js +++ b/test/tap/add-remote-git-shrinkwrap.js @@ -45,8 +45,9 @@ test('setup', function (t) { test('install from repo', function (t) { process.chdir(pkg) - npm.commands.install('.', [], function (er) { - t.ifError(er, 'npm installed via git') + common.npm(['install'], {cwd: pkg, stdio: [0, 1, 2]}, function (er, code) { + if (er) throw er + t.is(code, 0, 'npm installed via git') t.end() }) @@ -56,13 +57,12 @@ test('shrinkwrap gets correct _from and _resolved (#7121)', function (t) { common.npm( [ 'shrinkwrap', - '--loglevel', 'silent' + '--loglevel', 'error' ], - { cwd: pkg }, - function (er, code, stdout, stderr) { - t.ifError(er, 'npm shrinkwrapped without errors') + { cwd: pkg, stdio: [0, 'pipe', 2] }, + function (er, code, stdout) { + if (er) throw er t.is(code, 0, '`npm shrinkwrap` exited ok') - t.equal(stderr.trim(), '', 'no error output on successful shrinkwrap') var shrinkwrap = require(resolve(pkg, 'npm-shrinkwrap.json')) git.whichAndExec( @@ -73,9 +73,7 @@ test('shrinkwrap gets correct _from and _resolved (#7121)', function (t) { t.notOk(stderr, 'no error output') var treeish = stdout.trim() - t.equal( - shrinkwrap.dependencies.child.resolved, - 'git://localhost:1234/child.git#' + treeish, + t.like(shrinkwrap, {dependencies: {child: {version: 'git://localhost:1234/child.git#' + treeish}}}, 'npm shrinkwrapped resolved correctly' ) @@ -95,6 +93,7 @@ test('clean', function (t) { }) function bootstrap () { + cleanup() mkdirp.sync(pkg) fs.writeFileSync(resolve(pkg, 'package.json'), pjParent) } diff --git a/test/tap/anon-cli-metrics.js b/test/tap/anon-cli-metrics.js index 16b9c380f28d3..2ece5a1e6ce9b 100644 --- a/test/tap/anon-cli-metrics.js +++ b/test/tap/anon-cli-metrics.js @@ -1,6 +1,7 @@ 'use strict' var path = require('path') var fs = require('graceful-fs') +var rimraf = require('rimraf') var test = require('tap').test var mr = require('npm-registry-mock') var Tacks = require('tacks') @@ -74,6 +75,10 @@ function cleanup () { fixture.remove(basedir) } +function reset () { + rimraf.sync(testdir + '/' + 'node_modules') +} + test('setup', function (t) { setup() mr({port: common.port, throwOnUnmatched: true}, function (err, s) { @@ -91,51 +96,51 @@ test('setup', function (t) { }) test('record success', function (t) { - common.npm(['install', '--no-send-metrics', 'file:success'], conf, function (err, code, stdout, stderr) { + common.npm(['install', '--no-save', '--no-send-metrics', 'file:success'], conf, function (err, code, stdout, stderr) { if (err) throw err t.is(code, 0, 'always succeeding install succeeded') t.comment(stdout.trim()) t.comment(stderr.trim()) var data = JSON.parse(fs.readFileSync(metricsFile)) - t.is(data.metrics.successfulInstalls, 1) - t.is(data.metrics.failedInstalls, 0) + t.is(data.metrics.successfulInstalls, 1, 'successes') + t.is(data.metrics.failedInstalls, 0, 'failures') t.done() }) }) test('record failure', function (t) { + reset() server.put('/-/npm/anon-metrics/v1/:id', { successfulInstalls: 1, failedInstalls: 0 }).reply(500, {ok: false}) - common.npm(['install', '--send-metrics', 'file:failure'], conf, function (err, code, stdout, stderr) { + common.npm(['install', '--no-save', '--send-metrics', 'file:failure'], conf, function (err, code, stdout, stderr) { if (err) throw err t.notEqual(code, 0, 'always failing install fails') t.comment(stdout.trim()) t.comment(stderr.trim()) var data = JSON.parse(fs.readFileSync(metricsFile)) - t.is(data.metrics.successfulInstalls, 1) - t.is(data.metrics.failedInstalls, 1) + t.is(data.metrics.successfulInstalls, 1, 'successes') + t.is(data.metrics.failedInstalls, 1, 'failures') t.done() }) }) test('report', function (t) { - console.log('setup') - + reset() server.put('/-/npm/anon-metrics/v1/:id', { successfulInstalls: 1, failedInstalls: 1 }).reply(200, {ok: true}) - common.npm(['install', '--send-metrics', 'file:slow'], conf, function (err, code, stdout, stderr) { + common.npm(['install', '--no-save', '--send-metrics', 'file:slow'], conf, function (err, code, stdout, stderr) { if (err) throw err t.is(code, 0, 'command ran ok') t.comment(stdout.trim()) t.comment(stderr.trim()) // todo check mock registry for post var data = JSON.parse(fs.readFileSync(metricsFile)) - t.is(data.metrics.successfulInstalls, 1) - t.is(data.metrics.failedInstalls, 0) + t.is(data.metrics.successfulInstalls, 1, 'successes') + t.is(data.metrics.failedInstalls, 0, 'failures') t.done() }) }) diff --git a/test/tap/bundled-dependencies-no-pkgjson.js b/test/tap/bundled-dependencies-no-pkgjson.js index 44eb47a03a200..a7056408a9a5d 100644 --- a/test/tap/bundled-dependencies-no-pkgjson.js +++ b/test/tap/bundled-dependencies-no-pkgjson.js @@ -19,23 +19,31 @@ var pkgJson = JSON.stringify({ 'a-bundled-dep' ] }, null, 2) + '\n' +var packed test('setup', function (t) { + rimraf.sync(dir) mkdirp.sync(path.join(dir, 'node_modules')) mkdirp.sync(dep) fs.writeFileSync(path.resolve(pkg, 'package.json'), pkgJson) fs.writeFileSync(path.resolve(dep, 'index.js'), '') - t.end() + common.npm(['pack', pkg], {cwd: dir}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'packed ok') + packed = stdout.trim() + t.comment(stderr) + t.end() + }) }) test('proper error on bundled dep with no package.json', function (t) { - t.plan(3) - var npmArgs = ['install', './' + path.basename(pkg)] + t.plan(2) + var npmArgs = ['install', packed] common.npm(npmArgs, { cwd: dir }, function (err, code, stdout, stderr) { - t.ifError(err, 'npm ran without issue') - t.notEqual(code, 0) + if (err) throw err + t.notEqual(code, 0, 'npm ended in error') t.like(stderr, /ENOENT/, 'ENOENT should be in stderr') t.end() }) diff --git a/test/tap/unit-deps-earliestInstallable.js b/test/tap/unit-deps-earliestInstallable.js index b975be685824f..538cfe6c09560 100644 --- a/test/tap/unit-deps-earliestInstallable.js +++ b/test/tap/unit-deps-earliestInstallable.js @@ -23,7 +23,9 @@ test('earliestInstallable should consider devDependencies', function (t) { package: { name: 'dep1', dependencies: { dep2: '2.0.0' } - } + }, + path: '/dep1', + realpath: '/dep1' } // a library required by the base package @@ -31,7 +33,9 @@ test('earliestInstallable should consider devDependencies', function (t) { package: { name: 'dep2', version: '1.0.0' - } + }, + path: '/dep2', + realpath: '/dep2' } // an incompatible verson of dep2. required by dep1 @@ -41,17 +45,21 @@ test('earliestInstallable should consider devDependencies', function (t) { version: '2.0.0', _requested: npa('dep2@2.0.0') }, - parent: dep1 + parent: dep1, + path: '/dep1/node_modules/dep2a', + realpath: '/dep1/node_modules/dep2a' } var pkg = { isTop: true, - children: [dep1], + children: [dep1, dep2], package: { name: 'pkg', dependencies: { dep1: '1.0.0' }, devDependencies: { dep2: '1.0.0' } - } + }, + path: '/', + realpath: '/' } dep1.parent = pkg @@ -69,7 +77,9 @@ test('earliestInstallable should reuse shared prod/dev deps when they are identi package: { name: 'dep1', dependencies: { dep2: '1.0.0' } - } + }, + path: '/dep1', + realpath: '/dep1' } var dep2 = { @@ -77,7 +87,9 @@ test('earliestInstallable should reuse shared prod/dev deps when they are identi name: 'dep2', version: '1.0.0', _requested: npa('dep2@^1.0.0') - } + }, + path: '/dep2', + realpath: '/dep2' } var pkg = { @@ -87,7 +99,9 @@ test('earliestInstallable should reuse shared prod/dev deps when they are identi name: 'pkg', dependencies: { dep1: '1.0.0' }, devDependencies: { dep2: '^1.0.0' } - } + }, + path: '/', + realpath: '/' } dep1.parent = pkg