diff options
Diffstat (limited to 'deps/npm/lib/install')
-rw-r--r-- | deps/npm/lib/install/action/extract-worker.js | 4 | ||||
-rw-r--r-- | deps/npm/lib/install/action/extract.js | 32 | ||||
-rw-r--r-- | deps/npm/lib/install/action/fetch.js | 2 | ||||
-rw-r--r-- | deps/npm/lib/install/actions.js | 2 | ||||
-rw-r--r-- | deps/npm/lib/install/audit.js | 272 | ||||
-rw-r--r-- | deps/npm/lib/install/copy-tree.js | 17 | ||||
-rw-r--r-- | deps/npm/lib/install/decompose-actions.js | 79 | ||||
-rw-r--r-- | deps/npm/lib/install/deps.js | 130 | ||||
-rw-r--r-- | deps/npm/lib/install/diff-trees.js | 42 | ||||
-rw-r--r-- | deps/npm/lib/install/get-requested.js | 4 | ||||
-rw-r--r-- | deps/npm/lib/install/has-modern-meta.js | 20 | ||||
-rw-r--r-- | deps/npm/lib/install/inflate-shrinkwrap.js | 81 | ||||
-rw-r--r-- | deps/npm/lib/install/is-only-optional.js | 3 | ||||
-rw-r--r-- | deps/npm/lib/install/read-shrinkwrap.js | 68 | ||||
-rw-r--r-- | deps/npm/lib/install/save.js | 12 |
15 files changed, 606 insertions, 162 deletions
diff --git a/deps/npm/lib/install/action/extract-worker.js b/deps/npm/lib/install/action/extract-worker.js index 24508c7804..2b082b4a57 100644 --- a/deps/npm/lib/install/action/extract-worker.js +++ b/deps/npm/lib/install/action/extract-worker.js @@ -10,9 +10,9 @@ module.exports = (args, cb) => { const spec = parsed[0] const extractTo = parsed[1] const opts = parsed[2] - if (!opts.log && opts.loglevel) { + if (!opts.log) { opts.log = npmlog - opts.log.level = opts.loglevel } + opts.log.level = opts.loglevel || opts.log.level BB.resolve(extract(spec, extractTo, opts)).nodeify(cb) } diff --git a/deps/npm/lib/install/action/extract.js b/deps/npm/lib/install/action/extract.js index 6b827f36ea..e8d7a6c4f6 100644 --- a/deps/npm/lib/install/action/extract.js +++ b/deps/npm/lib/install/action/extract.js @@ -4,9 +4,7 @@ const BB = require('bluebird') const stat = BB.promisify(require('graceful-fs').stat) const gentlyRm = BB.promisify(require('../../utils/gently-rm.js')) -const log = require('npmlog') const mkdirp = BB.promisify(require('mkdirp')) -const moduleName = require('../../utils/module-name.js') const moduleStagingPath = require('../module-staging-path.js') const move = require('../../utils/move.js') const npa = require('npm-package-arg') @@ -59,12 +57,11 @@ function extract (staging, pkg, log) { pacoteOpts = require('../../config/pacote') } const opts = pacoteOpts({ - integrity: pkg.package._integrity + integrity: pkg.package._integrity, + resolved: pkg.package._resolved }) const args = [ - pkg.package._resolved - ? npa.resolve(pkg.package.name, pkg.package._resolved) - : pkg.package._requested, + pkg.package._requested, extractTo, opts ] @@ -112,18 +109,6 @@ function readBundled (pkg, staging, extractTo) { }, {concurrency: 10}) } -function getTree (pkg) { - while (pkg.parent) pkg = pkg.parent - return pkg -} - -function warn (pkg, code, msg) { - const tree = getTree(pkg) - const err = new Error(msg) - err.code = code - tree.warnings.push(err) -} - function stageBundledModule (bundler, child, staging, parentPath) { const stageFrom = path.join(parentPath, 'node_modules', child.package.name) const stageTo = moduleStagingPath(staging, child) @@ -146,15 +131,6 @@ function finishModule (bundler, child, stageTo, stageFrom) { return move(stageFrom, stageTo) }) } else { - return stat(stageFrom).then(() => { - const bundlerId = packageId(bundler) - if (!getTree(bundler).warnings.some((w) => { - return w.code === 'EBUNDLEOVERRIDE' - })) { - warn(bundler, 'EBUNDLEOVERRIDE', `${bundlerId} had bundled packages that do not match the required version(s). They have been replaced with non-bundled versions.`) - } - log.verbose('bundle', `EBUNDLEOVERRIDE: Replacing ${bundlerId}'s bundled version of ${moduleName(child)} with ${packageId(child)}.`) - return gentlyRm(stageFrom) - }, () => {}) + return stat(stageFrom).then(() => gentlyRm(stageFrom), () => {}) } } diff --git a/deps/npm/lib/install/action/fetch.js b/deps/npm/lib/install/action/fetch.js index a4d760fe82..5ad34e29dd 100644 --- a/deps/npm/lib/install/action/fetch.js +++ b/deps/npm/lib/install/action/fetch.js @@ -12,5 +12,5 @@ function fetch (staging, pkg, log, next) { log.silly('fetch', packageId(pkg)) const opts = pacoteOpts({integrity: pkg.package._integrity}) return finished(pacote.tarball.stream(pkg.package._requested, opts)) - .then(() => next(), next) + .then(() => next(), next) } diff --git a/deps/npm/lib/install/actions.js b/deps/npm/lib/install/actions.js index 9608a943a5..a34d03ffe2 100644 --- a/deps/npm/lib/install/actions.js +++ b/deps/npm/lib/install/actions.js @@ -118,7 +118,7 @@ function doParallel (type, staging, actionsToRun, log, next) { } return acc }, []) - log.silly('doParallel', type + ' ' + actionsToRun.length) + log.silly('doParallel', type + ' ' + acts.length) time(log) if (!acts.length) { return next() } return withInit(actions[type], () => { diff --git a/deps/npm/lib/install/audit.js b/deps/npm/lib/install/audit.js new file mode 100644 index 0000000000..4be59ca7c5 --- /dev/null +++ b/deps/npm/lib/install/audit.js @@ -0,0 +1,272 @@ +'use strict' +exports.generate = generate +exports.generateFromInstall = generateFromInstall +exports.submitForInstallReport = submitForInstallReport +exports.submitForFullReport = submitForFullReport +exports.printInstallReport = printInstallReport +exports.printFullReport = printFullReport + +const Bluebird = require('bluebird') +const auditReport = require('npm-audit-report') +const treeToShrinkwrap = require('../shrinkwrap.js').treeToShrinkwrap +const packageId = require('../utils/package-id.js') +const output = require('../utils/output.js') +const npm = require('../npm.js') +const qw = require('qw') +const registryFetch = require('npm-registry-fetch') +const zlib = require('zlib') +const gzip = Bluebird.promisify(zlib.gzip) +const log = require('npmlog') +const perf = require('../utils/perf.js') +const url = require('url') +const npa = require('npm-package-arg') +const uuid = require('uuid') +const ssri = require('ssri') +const cloneDeep = require('lodash.clonedeep') +const pacoteOpts = require('../config/pacote.js') + +// used when scrubbing module names/specifiers +const runId = uuid.v4() + +function submitForInstallReport (auditData) { + const cfg = npm.config // avoid the no-dynamic-lookups test + const scopedRegistries = cfg.keys.filter(_ => /:registry$/.test(_)).map(_ => cfg.get(_)) + perf.emit('time', 'audit compress') + // TODO: registryFetch will be adding native support for `Content-Encoding: gzip` at which point + // we'll pass in something like `gzip: true` and not need to JSON stringify, gzip or headers. + return gzip(JSON.stringify(auditData)).then(body => { + perf.emit('timeEnd', 'audit compress') + log.info('audit', 'Submitting payload of ' + body.length + 'bytes') + scopedRegistries.forEach(reg => { + // we don't care about the response so destroy the stream if we can, or leave it flowing + // so it can eventually finish and clean up after itself + fetchAudit(url.resolve(reg, '/-/npm/v1/security/audits/quick')) + .then(_ => { + _.body.on('error', () => {}) + if (_.body.destroy) { + _.body.destroy() + } else { + _.body.resume() + } + }, _ => {}) + }) + perf.emit('time', 'audit submit') + return fetchAudit('/-/npm/v1/security/audits/quick', body).then(response => { + perf.emit('timeEnd', 'audit submit') + perf.emit('time', 'audit body') + return response.json() + }).then(result => { + perf.emit('timeEnd', 'audit body') + return result + }) + }) +} + +function submitForFullReport (auditData) { + perf.emit('time', 'audit compress') + // TODO: registryFetch will be adding native support for `Content-Encoding: gzip` at which point + // we'll pass in something like `gzip: true` and not need to JSON stringify, gzip or headers. + return gzip(JSON.stringify(auditData)).then(body => { + perf.emit('timeEnd', 'audit compress') + log.info('audit', 'Submitting payload of ' + body.length + ' bytes') + perf.emit('time', 'audit submit') + return fetchAudit('/-/npm/v1/security/audits', body).then(response => { + perf.emit('timeEnd', 'audit submit') + perf.emit('time', 'audit body') + return response.json() + }).then(result => { + perf.emit('timeEnd', 'audit body') + result.runId = runId + return result + }) + }) +} + +function fetchAudit (href, body) { + const opts = pacoteOpts() + return registryFetch(href, { + method: 'POST', + headers: { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' }, + config: npm.config, + npmSession: opts.npmSession, + projectScope: npm.projectScope, + log: log, + body: body + }) +} + +function printInstallReport (auditResult) { + return auditReport(auditResult, { + reporter: 'install', + withColor: npm.color, + withUnicode: npm.config.get('unicode') + }).then(result => output(result.report)) +} + +function printFullReport (auditResult) { + return auditReport(auditResult, { + log: output, + reporter: npm.config.get('json') ? 'json' : 'detail', + withColor: npm.color, + withUnicode: npm.config.get('unicode') + }).then(result => output(result.report)) +} + +function generate (shrinkwrap, requires, diffs, install, remove) { + const sw = cloneDeep(shrinkwrap) + delete sw.lockfileVersion + sw.requires = scrubRequires(requires) + scrubDeps(sw.dependencies) + + // sw.diffs = diffs || {} + sw.install = (install || []).map(scrubArg) + sw.remove = (remove || []).map(scrubArg) + return generateMetadata().then((md) => { + sw.metadata = md + return sw + }) +} + +const scrubKeys = qw`version` +const deleteKeys = qw`from resolved` + +function scrubDeps (deps) { + if (!deps) return + Object.keys(deps).forEach(name => { + if (!shouldScrubName(name) && !shouldScrubSpec(name, deps[name].version)) return + const value = deps[name] + delete deps[name] + deps[scrub(name)] = value + }) + Object.keys(deps).forEach(name => { + for (let toScrub of scrubKeys) { + if (!deps[name][toScrub]) continue + deps[name][toScrub] = scrubSpec(name, deps[name][toScrub]) + } + for (let toDelete of deleteKeys) delete deps[name][toDelete] + + scrubRequires(deps[name].requires) + scrubDeps(deps[name].dependencies) + }) +} + +function scrubRequires (reqs) { + if (!reqs) return reqs + Object.keys(reqs).forEach(name => { + const spec = reqs[name] + if (shouldScrubName(name) || shouldScrubSpec(name, spec)) { + delete reqs[name] + reqs[scrub(name)] = scrubSpec(name, spec) + } else { + reqs[name] = scrubSpec(name, spec) + } + }) + return reqs +} + +function getScope (name) { + if (name[0] === '@') return name.slice(0, name.indexOf('/')) +} + +function shouldScrubName (name) { + const scope = getScope(name) + const cfg = npm.config // avoid the no-dynamic-lookups test + return Boolean(scope && cfg.get(scope + ':registry')) +} +function shouldScrubSpec (name, spec) { + const req = npa.resolve(name, spec) + return !req.registry +} + +function scrubArg (arg) { + const req = npa(arg) + let name = req.name + if (shouldScrubName(name) || shouldScrubSpec(name, req.rawSpec)) { + name = scrubName(name) + } + const spec = scrubSpec(req.name, req.rawSpec) + return name + '@' + spec +} + +function scrubName (name) { + return shouldScrubName(name) ? scrub(name) : name +} + +function scrubSpec (name, spec) { + const req = npa.resolve(name, spec) + if (req.registry) return spec + if (req.type === 'git') { + return 'git+ssh://' + scrub(spec) + } else if (req.type === 'remote') { + return 'https://' + scrub(spec) + } else if (req.type === 'directory') { + return 'file:' + scrub(spec) + } else if (req.type === 'file') { + return 'file:' + scrub(spec) + '.tar' + } else { + return scrub(spec) + } +} + +module.exports.scrub = scrub +function scrub (value, rid) { + return ssri.fromData((rid || runId) + ' ' + value, {algorithms: ['sha256']}).hexDigest() +} + +function generateMetadata () { + const meta = {} + meta.npm_version = npm.version + meta.node_version = process.version + meta.platform = process.platform + meta.node_env = process.env.NODE_ENV + + return Promise.resolve(meta) +} +/* + const head = path.resolve(npm.prefix, '.git/HEAD') + return readFile(head, 'utf8').then((head) => { + if (!head.match(/^ref: /)) { + meta.commit_hash = head.trim() + return + } + const headFile = head.replace(/^ref: /, '').trim() + meta.branch = headFile.replace(/^refs[/]heads[/]/, '') + return readFile(path.resolve(npm.prefix, '.git', headFile), 'utf8') + }).then((commitHash) => { + meta.commit_hash = commitHash.trim() + const proc = spawn('git', qw`diff --quiet --exit-code package.json package-lock.json`, {cwd: npm.prefix, stdio: 'ignore'}) + return new Promise((resolve, reject) => { + proc.once('error', reject) + proc.on('exit', (code, signal) => { + if (signal == null) meta.state = code === 0 ? 'clean' : 'dirty' + resolve() + }) + }) + }).then(() => meta, () => meta) +*/ + +function generateFromInstall (tree, diffs, install, remove) { + const requires = {} + tree.requires.forEach((pkg) => { + requires[pkg.package.name] = tree.package.dependencies[pkg.package.name] || tree.package.devDependencies[pkg.package.name] || pkg.package.version + }) + + const auditInstall = (install || []).filter((a) => a.name).map(packageId) + const auditRemove = (remove || []).filter((a) => a.name).map(packageId) + const auditDiffs = {} + diffs.forEach((action) => { + const mutation = action[0] + const child = action[1] + if (mutation !== 'add' && mutation !== 'update' && mutation !== 'remove') return + if (!auditDiffs[mutation]) auditDiffs[mutation] = [] + if (mutation === 'add') { + auditDiffs[mutation].push({location: child.location}) + } else if (mutation === 'update') { + auditDiffs[mutation].push({location: child.location, previous: packageId(child.oldPkg)}) + } else if (mutation === 'remove') { + auditDiffs[mutation].push({previous: packageId(child)}) + } + }) + + return generate(treeToShrinkwrap(tree), requires, auditDiffs, auditInstall, auditRemove) +} diff --git a/deps/npm/lib/install/copy-tree.js b/deps/npm/lib/install/copy-tree.js index a5b558cf59..2bf7064f33 100644 --- a/deps/npm/lib/install/copy-tree.js +++ b/deps/npm/lib/install/copy-tree.js @@ -1,27 +1,26 @@ 'use strict' var createNode = require('./node.js').create -module.exports = function (tree, filter) { - return copyTree(tree, {}, filter) +module.exports = function (tree) { + return copyTree(tree, {}) } -function copyTree (tree, cache, filter) { - if (filter && !filter(tree)) { return null } +function copyTree (tree, cache) { if (cache[tree.path]) { return cache[tree.path] } var newTree = cache[tree.path] = createNode(Object.assign({}, tree)) - copyModuleList(newTree, 'children', cache, filter) + copyModuleList(newTree, 'children', cache) newTree.children.forEach(function (child) { child.parent = newTree }) - copyModuleList(newTree, 'requires', cache, filter) - copyModuleList(newTree, 'requiredBy', cache, filter) + copyModuleList(newTree, 'requires', cache) + copyModuleList(newTree, 'requiredBy', cache) return newTree } -function copyModuleList (tree, key, cache, filter) { +function copyModuleList (tree, key, cache) { var newList = [] if (tree[key]) { tree[key].forEach(function (child) { - const copy = copyTree(child, cache, filter) + const copy = copyTree(child, cache) if (copy) { newList.push(copy) } diff --git a/deps/npm/lib/install/decompose-actions.js b/deps/npm/lib/install/decompose-actions.js index 57dc7cd687..ba08e6e768 100644 --- a/deps/npm/lib/install/decompose-actions.js +++ b/deps/npm/lib/install/decompose-actions.js @@ -1,72 +1,79 @@ 'use strict' var validate = require('aproba') -var asyncMap = require('slide').asyncMap var npm = require('../npm.js') module.exports = function (differences, decomposed, next) { validate('AAF', arguments) - asyncMap(differences, function (action, done) { + differences.forEach((action) => { var cmd = action[0] var pkg = action[1] switch (cmd) { case 'add': - addSteps(decomposed, pkg, done) + addSteps(decomposed, pkg) break case 'update': - updateSteps(decomposed, pkg, done) + updateSteps(decomposed, pkg) break case 'move': - moveSteps(decomposed, pkg, done) + moveSteps(decomposed, pkg) break case 'remove': - removeSteps(decomposed, pkg, done) + removeSteps(decomposed, pkg) break default: - defaultSteps(decomposed, cmd, pkg, done) + defaultSteps(decomposed, cmd, pkg) } - }, next) + }) + next() +} + +function addAction (decomposed, action, pkg) { + if (decomposed.some((_) => _[0] === action && _[1] === pkg)) return + decomposed.push([action, pkg]) } -function addSteps (decomposed, pkg, done) { +function addSteps (decomposed, pkg) { + if (pkg.fromBundle) { + // make sure our source module exists to extract ourselves from + // if we're installing our source module anyway, the duplication + // of these steps will be elided by `addAction` automatically + addAction(decomposed, 'fetch', pkg.fromBundle) + addAction(decomposed, 'extract', pkg.fromBundle) + } if (!pkg.fromBundle && !pkg.isLink) { - decomposed.push(['fetch', pkg]) - decomposed.push(['extract', pkg]) + addAction(decomposed, 'fetch', pkg) + addAction(decomposed, 'extract', pkg) } if (!pkg.fromBundle || npm.config.get('rebuild-bundle')) { - decomposed.push(['preinstall', pkg]) - decomposed.push(['build', pkg]) - decomposed.push(['install', pkg]) - decomposed.push(['postinstall', pkg]) + addAction(decomposed, 'preinstall', pkg) + addAction(decomposed, 'build', pkg) + addAction(decomposed, 'install', pkg) + addAction(decomposed, 'postinstall', pkg) } if (!pkg.fromBundle || !pkg.isLink) { - decomposed.push(['finalize', pkg]) + addAction(decomposed, 'finalize', pkg) } - decomposed.push(['refresh-package-json', pkg]) - done() + addAction(decomposed, 'refresh-package-json', pkg) } -function updateSteps (decomposed, pkg, done) { - removeSteps(decomposed, pkg.oldPkg, () => { - addSteps(decomposed, pkg, done) - }) +function updateSteps (decomposed, pkg) { + removeSteps(decomposed, pkg.oldPkg) + addSteps(decomposed, pkg) } -function removeSteps (decomposed, pkg, done) { - decomposed.push(['unbuild', pkg]) - decomposed.push(['remove', pkg]) - done() +function removeSteps (decomposed, pkg) { + addAction(decomposed, 'unbuild', pkg) + addAction(decomposed, 'remove', pkg) } -function moveSteps (decomposed, pkg, done) { - decomposed.push(['move', pkg]) - decomposed.push(['build', pkg]) - decomposed.push(['install', pkg]) - decomposed.push(['postinstall', pkg]) - decomposed.push(['refresh-package-json', pkg]) - done() +function moveSteps (decomposed, pkg) { + addAction(decomposed, 'move', pkg) + addAction(decomposed, 'build', pkg) + addAction(decomposed, 'install', pkg) + addAction(decomposed, 'postinstall', pkg) + addAction(decomposed, 'refresh-package-json', pkg) } -function defaultSteps (decomposed, cmd, pkg, done) { - decomposed.push([cmd, pkg]) - done() +function defaultSteps (decomposed, cmd, pkg) { + addAction(decomposed, cmd, pkg) } diff --git a/deps/npm/lib/install/deps.js b/deps/npm/lib/install/deps.js index 93c4adffd7..c36265093b 100644 --- a/deps/npm/lib/install/deps.js +++ b/deps/npm/lib/install/deps.js @@ -33,6 +33,7 @@ var getSaveType = require('./save.js').getSaveType var unixFormatPath = require('../utils/unix-format-path.js') var isExtraneous = require('./is-extraneous.js') var isRegistry = require('../utils/is-registry.js') +var hasModernMeta = require('./has-modern-meta.js') // The export functions in this module mutate a dependency tree, adding // items to them. @@ -50,6 +51,12 @@ function doesChildVersionMatch (child, requested, requestor) { return path.relative(child.realpath, requested.fetchSpec) === '' } + if (requested.type === 'git' && child.fromShrinkwrap) { + const fromSw = child.package._from ? npa(child.package._from) : child.fromShrinkwrap + fromSw.name = requested.name // we're only checking specifiers here + if (fromSw.toString() === requested.toString()) return true + } + if (!registryTypes[requested.type]) { var childReq = child.package._requested if (childReq) { @@ -65,7 +72,7 @@ function doesChildVersionMatch (child, requested, requestor) { // You'll see this scenario happen with at least tags and git dependencies. // Some buggy clients will write spaces into the module name part of a _from. if (child.package._from) { - var fromReq = npa.resolve(moduleName(child), child.package._from.replace(new RegExp('^\s*' + moduleName(child) + '\s*@'), '')) + var fromReq = npa.resolve(moduleName(child), child.package._from.replace(new RegExp('^\\s*' + moduleName(child) + '\\s*@'), '')) if (fromReq.rawSpec === requested.rawSpec) return true if (fromReq.type === requested.type && fromReq.saveSpec && fromReq.saveSpec === requested.saveSpec) return true } @@ -78,8 +85,8 @@ function doesChildVersionMatch (child, requested, requestor) { } } -function childDependencySpecifier (tree, name, spec) { - return npa.resolve(name, spec, packageRelativePath(tree)) +function childDependencySpecifier (tree, name, spec, where) { + return npa.resolve(name, spec, where || packageRelativePath(tree)) } exports.computeMetadata = computeMetadata @@ -104,14 +111,13 @@ function computeMetadata (tree, seen) { resolveWithExistingModule(child, tree) return true } - return } const deps = tree.package.dependencies || {} const reqs = tree.swRequires || {} for (let name of Object.keys(deps)) { if (findChild(name, deps[name])) continue - if (findChild(name, reqs[name])) continue + if (name in reqs && findChild(name, reqs[name])) continue tree.missingDeps[name] = deps[name] } if (tree.isTop) { @@ -186,15 +192,14 @@ function packageRelativePath (tree) { var requested = tree.package._requested || {} var isLocal = requested.type === 'directory' || requested.type === 'file' return isLocal ? requested.fetchSpec - : (tree.isLink || tree.isInLink) && !preserveSymlinks() ? tree.realpath - : tree.path + : (tree.isLink || tree.isInLink) && !preserveSymlinks() ? tree.realpath + : tree.path } function matchingDep (tree, name) { if (!tree || !tree.package) return 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) { @@ -261,6 +266,7 @@ exports.loadRequestedDeps = function (args, tree, saveToDependencies, log, next) delete tree.package[saveType][childName] } } + if (child.save === 'optionalDependencies') tree.package.dependencies[childName] = child.saveSpec } // For things the user asked to install, that aren't a dependency (or @@ -282,10 +288,12 @@ function computeVersionSpec (tree, child) { validate('OO', arguments) var requested var childReq = child.package._requested - if (childReq && (isNotEmpty(childReq.saveSpec) || (isNotEmpty(childReq.rawSpec) && isNotEmpty(childReq.fetchSpec)))) { + if (child.isLink) { + requested = npa.resolve(child.package.name, 'file:' + child.realpath, getTop(tree).path) + } else if (childReq && (isNotEmpty(childReq.saveSpec) || (isNotEmpty(childReq.rawSpec) && isNotEmpty(childReq.fetchSpec)))) { requested = child.package._requested } else if (child.package._from) { - requested = npa(child.package._from) + requested = npa(child.package._from, tree.path) } else { requested = npa.resolve(child.package.name, child.package.version) } @@ -299,7 +307,7 @@ function computeVersionSpec (tree, child) { } return rangeDescriptor + version } else if (requested.type === 'directory' || requested.type === 'file') { - return 'file:' + unixFormatPath(path.relative(tree.path, requested.fetchSpec)) + return 'file:' + unixFormatPath(path.relative(getTop(tree).path, requested.fetchSpec)) } else { return requested.saveSpec || requested.rawSpec } @@ -332,9 +340,21 @@ exports.removeDeps = function (args, tree, saveToDependencies, next) { parent.requires = parent.requires.filter((child) => child !== pkgToRemove) } pkgToRemove.requiredBy = pkgToRemove.requiredBy.filter((parent) => parent !== tree) + flagAsRemoving(pkgToRemove) } next() } + +function flagAsRemoving (toRemove, seen) { + if (!seen) seen = new Set() + if (seen.has(toRemove)) return + seen.add(toRemove) + toRemove.removing = true + toRemove.requires.forEach((required) => { + flagAsRemoving(required, seen) + }) +} + exports.removeExtraneous = function (args, tree, next) { for (let pkg of args) { var pkgName = moduleName(pkg) @@ -369,8 +389,22 @@ function andForEachChild (load, next) { function isDepOptional (tree, name, pkg) { if (pkg.package && pkg.package._optional) return true - if (!tree.package.optionalDependencies) return false - if (tree.package.optionalDependencies[name] != null) return true + const optDeps = tree.package.optionalDependencies + if (optDeps && optDeps[name] != null) return true + + const devDeps = tree.package.devDependencies + if (devDeps && devDeps[name] != null) { + const includeDev = npm.config.get('dev') || + (!/^prod(uction)?$/.test(npm.config.get('only')) && !npm.config.get('production')) || + /^dev(elopment)?$/.test(npm.config.get('only')) || + /^dev(elopment)?$/.test(npm.config.get('also')) + return !includeDev + } + const prodDeps = tree.package.dependencies + if (prodDeps && prodDeps[name] != null) { + const includeProd = !/^dev(elopment)?$/.test(npm.config.get('only')) + return !includeProd + } return false } @@ -461,12 +495,6 @@ function loadDeps (tree, log, next) { if (!tree.package.dependencies) tree.package.dependencies = {} asyncMap(Object.keys(tree.package.dependencies), function (dep, done) { var version = tree.package.dependencies[dep] - if (tree.package.optionalDependencies && - tree.package.optionalDependencies[dep] && - !npm.config.get('optional')) { - return done() - } - addDependency(dep, version, tree, log.newGroup('loadDep:' + dep), andHandleOptionalErrors(log, tree, dep, done)) }, andForEachChild(loadDeps, andFinishTracker(log, next))) } @@ -481,7 +509,7 @@ exports.loadDevDeps = function (tree, log, next) { if (tree.package.dependencies[dep]) return done() var logGroup = log.newGroup('loadDevDep:' + dep) - addDependency(dep, tree.package.devDependencies[dep], tree, logGroup, done) + addDependency(dep, tree.package.devDependencies[dep], tree, logGroup, andHandleOptionalErrors(log, tree, dep, done)) }, andForEachChild(loadDeps, andFinishTracker(log, next))) } @@ -519,14 +547,14 @@ function addDependency (name, versionSpec, tree, log, done) { try { var req = childDependencySpecifier(tree, name, versionSpec) if (tree.swRequires && tree.swRequires[name]) { - var swReq = childDependencySpecifier(tree, name, tree.swRequires[name]) + var swReq = childDependencySpecifier(tree, name, tree.swRequires[name], tree.package._where) } } catch (err) { return done(err) } var child = findRequirement(tree, name, req) if (!child && swReq) child = findRequirement(tree, name, swReq) - if (child) { + if (hasModernMeta(child)) { resolveWithExistingModule(child, tree) if (child.package._shrinkwrap === undefined) { readShrinkwrap.andInflate(child, function (er) { next(er, child, log) }) @@ -534,12 +562,42 @@ function addDependency (name, versionSpec, tree, log, done) { next(null, child, log) } } else { + if (child) { + if (req.registry) { + req = childDependencySpecifier(tree, name, child.package.version) + } + if (child.fromBundle) reportBundleOverride(child, log) + removeObsoleteDep(child, log) + } fetchPackageMetadata(req, packageRelativePath(tree), {tracker: log.newItem('fetchMetadata')}, iferr(next, function (pkg) { resolveWithNewModule(pkg, tree, log, next) })) } } +function getTop (pkg) { + const seen = new Set() + while (pkg.parent && !seen.has(pkg.parent)) { + pkg = pkg.parent + seen.add(pkg) + } + return pkg +} + +function reportBundleOverride (child, log) { + const code = 'EBUNDLEOVERRIDE' + const top = getTop(child.fromBundle) + const bundlerId = packageId(child.fromBundle) + if (!top.warnings.some((w) => { + return w.code === code + })) { + const err = new Error(`${bundlerId} had bundled packages that do not match the required version(s). They have been replaced with non-bundled versions.`) + err.code = code + top.warnings.push(err) + } + if (log) log.verbose('bundle', `${code}: Replacing ${bundlerId}'s bundled version of ${moduleName(child)} with ${packageId(child)}.`) +} + function resolveWithExistingModule (child, tree) { validate('OO', arguments) addRequiredDep(tree, child) @@ -592,7 +650,7 @@ function resolveWithNewModule (pkg, tree, log, next) { return isInstallable(pkg, (err) => { let installable = !err addBundled(pkg, (bundleErr) => { - var parent = earliestInstallable(tree, tree, pkg) || tree + var parent = earliestInstallable(tree, tree, pkg, log) || tree var isLink = pkg._requested.type === 'directory' var child = createChild({ package: pkg, @@ -609,7 +667,10 @@ function resolveWithNewModule (pkg, tree, log, next) { var hasBundled = child.children.length var replaced = replaceModuleByName(parent, 'children', child) - if (replaced) removeObsoleteDep(replaced) + if (replaced) { + if (replaced.fromBundle) reportBundleOverride(replaced, log) + removeObsoleteDep(replaced) + } addRequiredDep(tree, child) child.location = flatNameFromTree(child) @@ -694,12 +755,25 @@ function preserveSymlinks () { // Find the highest level in the tree that we can install this module in. // If the module isn't installed above us yet, that'd be the very top. // If it is, then it's the level below where its installed. -var earliestInstallable = exports.earliestInstallable = function (requiredBy, tree, pkg) { - validate('OOO', arguments) +var earliestInstallable = exports.earliestInstallable = function (requiredBy, tree, pkg, log) { + validate('OOOO', arguments) + function undeletedModuleMatches (child) { return !child.removed && moduleName(child) === pkg.name } - if (tree.children.some(undeletedModuleMatches)) return null + const undeletedMatches = tree.children.filter(undeletedModuleMatches) + if (undeletedMatches.length) { + // if there's a conflict with another child AT THE SAME level then we're replacing it, so + // mark it as removed and continue with resolution normally. + if (tree === requiredBy) { + undeletedMatches.forEach((pkg) => { + if (pkg.fromBundle) reportBundleOverride(pkg, log) + removeObsoleteDep(pkg, log) + }) + } else { + return null + } + } // If any of the children of this tree have conflicting // binaries then we need to decline to install this package here. @@ -738,5 +812,5 @@ var earliestInstallable = exports.earliestInstallable = function (requiredBy, tr if (!preserveSymlinks() && /^[.][.][\\/]/.test(path.relative(tree.parent.realpath, tree.realpath))) return tree - return (earliestInstallable(requiredBy, tree.parent, pkg) || tree) + return (earliestInstallable(requiredBy, tree.parent, pkg, log) || tree) } diff --git a/deps/npm/lib/install/diff-trees.js b/deps/npm/lib/install/diff-trees.js index 4316f351cc..346846fdc0 100644 --- a/deps/npm/lib/install/diff-trees.js +++ b/deps/npm/lib/install/diff-trees.js @@ -8,6 +8,7 @@ var log = require('npmlog') var path = require('path') var ssri = require('ssri') var moduleName = require('../utils/module-name.js') +var isOnlyOptional = require('./is-only-optional.js') // we don't use get-requested because we're operating on files on disk, and // we don't want to extropolate from what _should_ be there. @@ -50,7 +51,7 @@ function pkgIntegrity (pkg) { if (Object.keys(integrity).length === 0) return return integrity } catch (ex) { - return + } } @@ -70,6 +71,9 @@ function sriMatch (aa, bb) { function pkgAreEquiv (aa, bb) { // coming in we know they share a path… + // if one is inside a link and the other is not, then they are not equivalent + // this happens when we're replacing a linked dep with a non-linked version + if (aa.isInLink !== bb.isInLink) return false // if they share package metadata _identity_, they're the same thing if (aa.package === bb.package) return true // if they share integrity information, they're the same thing @@ -162,6 +166,11 @@ var sortActions = module.exports.sortActions = function (differences) { sorted.unshift(action) } + // safety net, anything excluded above gets tacked on the end + differences.forEach((_) => { + if (sorted.indexOf(_) === -1) sorted.push(_) + }) + return sorted } @@ -213,9 +222,8 @@ var diffTrees = module.exports._diffTrees = function (oldTree, newTree) { pkg.fromPath = toMv.pkg.path setAction(differences, 'move', pkg) delete toRemove[toMv.flatname] - // we don't generate add actions for things found in links (which already exist on disk) or - // for bundled modules (which will be installed when we install their parent) - } else if (!(pkg.isInLink && pkg.fromBundle)) { + // we don't generate add actions for things found in links (which already exist on disk) + } else if (!pkg.isInLink || !(pkg.fromBundle && pkg.fromBundle.isLink)) { setAction(differences, 'add', pkg) } } @@ -227,18 +235,26 @@ var diffTrees = module.exports._diffTrees = function (oldTree, newTree) { .map((flatname) => toRemove[flatname]) .forEach((pkg) => setAction(differences, 'remove', pkg)) + return filterActions(differences) +} + +function filterActions (differences) { + const includeOpt = npm.config.get('optional') const includeDev = npm.config.get('dev') || (!/^prod(uction)?$/.test(npm.config.get('only')) && !npm.config.get('production')) || /^dev(elopment)?$/.test(npm.config.get('only')) || /^dev(elopment)?$/.test(npm.config.get('also')) const includeProd = !/^dev(elopment)?$/.test(npm.config.get('only')) - if (!includeProd || !includeDev) { - log.silly('diff-trees', 'filtering actions:', 'includeDev', includeDev, 'includeProd', includeProd) - differences = differences.filter((diff) => { - const pkg = diff[1] - const pkgIsOnlyDev = isOnlyDev(pkg) - return (!includeProd && pkgIsOnlyDev) || (includeDev && pkgIsOnlyDev) || (includeProd && !pkgIsOnlyDev) - }) - } - return differences + if (includeProd && includeDev && includeOpt) return differences + + log.silly('diff-trees', 'filtering actions:', 'includeDev', includeDev, 'includeProd', includeProd, 'includeOpt', includeOpt) + return differences.filter((diff) => { + const pkg = diff[1] + const pkgIsOnlyDev = isOnlyDev(pkg) + const pkgIsOnlyOpt = isOnlyOptional(pkg) + if (!includeProd && pkgIsOnlyDev) return true + if (includeDev && pkgIsOnlyDev) return true + if (includeProd && !pkgIsOnlyDev && (includeOpt || !pkgIsOnlyOpt)) return true + return false + }) } diff --git a/deps/npm/lib/install/get-requested.js b/deps/npm/lib/install/get-requested.js index f6c44d1463..ab410ffc9b 100644 --- a/deps/npm/lib/install/get-requested.js +++ b/deps/npm/lib/install/get-requested.js @@ -2,9 +2,9 @@ const npa = require('npm-package-arg') const moduleName = require('../utils/module-name.js') -module.exports = function (child) { +module.exports = function (child, reqBy) { if (!child.requiredBy.length) return - const reqBy = child.requiredBy[0] + if (!reqBy) reqBy = child.requiredBy[0] const deps = reqBy.package.dependencies || {} const devDeps = reqBy.package.devDependencies || {} const name = moduleName(child) diff --git a/deps/npm/lib/install/has-modern-meta.js b/deps/npm/lib/install/has-modern-meta.js new file mode 100644 index 0000000000..bf801d0d31 --- /dev/null +++ b/deps/npm/lib/install/has-modern-meta.js @@ -0,0 +1,20 @@ +'use strict' +module.exports = hasModernMeta + +const npa = require('npm-package-arg') +const moduleName = require('../utils/module-name.js') + +function isLink (child) { + return child.isLink || (child.parent && isLink(child.parent)) +} + +function hasModernMeta (child) { + if (!child) return false + const resolved = child.package._resolved && npa.resolve(moduleName(child), child.package._resolved) + const version = npa.resolve(moduleName(child), child.package.version) + return child.isTop || + isLink(child) || + child.fromBundle || child.package._inBundle || + child.package._integrity || child.package._shasum || + (resolved && resolved.type === 'git') || (version && version.type === 'git') +} diff --git a/deps/npm/lib/install/inflate-shrinkwrap.js b/deps/npm/lib/install/inflate-shrinkwrap.js index 43ac9136f0..bf1ab70657 100644 --- a/deps/npm/lib/install/inflate-shrinkwrap.js +++ b/deps/npm/lib/install/inflate-shrinkwrap.js @@ -14,6 +14,9 @@ const realizeShrinkwrapSpecifier = require('./realize-shrinkwrap-specifier.js') const validate = require('aproba') const path = require('path') const isRegistry = require('../utils/is-registry.js') +const hasModernMeta = require('./has-modern-meta.js') +const ssri = require('ssri') +const npa = require('npm-package-arg') module.exports = function (tree, sw, opts, finishInflating) { if (!fetchPackageMetadata) { @@ -66,11 +69,43 @@ function normalizePackageDataNoErrors (pkg) { } } +function quotemeta (str) { + return str.replace(/([^A-Za-z_0-9/])/g, '\\$1') +} + +function tarballToVersion (name, tb) { + const registry = quotemeta(npm.config.get('registry')) + .replace(/https?:/, 'https?:') + .replace(/([^/])$/, '$1/') + let matchRegTarball + if (name) { + const nameMatch = quotemeta(name) + matchRegTarball = new RegExp(`^${registry}${nameMatch}/-/${nameMatch}-(.*)[.]tgz$`) + } else { + matchRegTarball = new RegExp(`^${registry}(.*)?/-/\\1-(.*)[.]tgz$`) + } + const match = tb.match(matchRegTarball) + if (!match) return + return match[2] || match[1] +} + function inflatableChild (onDiskChild, name, topPath, tree, sw, requested, opts) { validate('OSSOOOO|ZSSOOOO', arguments) - if (onDiskChild && childIsEquivalent(sw, requested, onDiskChild)) { + const usesIntegrity = ( + requested.registry || + requested.type === 'remote' || + requested.type === 'file' + ) + const regTarball = tarballToVersion(name, sw.version) + if (regTarball) { + sw.resolved = sw.version + sw.version = regTarball + } + if (sw.requires) Object.keys(sw.requires).map(_ => { sw.requires[_] = tarballToVersion(_, sw.requires[_]) || sw.requires[_] }) + const modernLink = requested.type === 'directory' && !sw.from + if (hasModernMeta(onDiskChild) && childIsEquivalent(sw, requested, onDiskChild)) { // The version on disk matches the shrinkwrap entry. - if (!onDiskChild.fromShrinkwrap) onDiskChild.fromShrinkwrap = true + if (!onDiskChild.fromShrinkwrap) onDiskChild.fromShrinkwrap = requested onDiskChild.package._requested = requested onDiskChild.package._spec = requested.rawSpec onDiskChild.package._where = topPath @@ -88,7 +123,7 @@ function inflatableChild (onDiskChild, name, topPath, tree, sw, requested, opts) onDiskChild.swRequires = sw.requires tree.children.push(onDiskChild) return BB.resolve(onDiskChild) - } else if ((sw.version && sw.integrity) || sw.bundled) { + } else if ((sw.version && (sw.integrity || !usesIntegrity) && (requested.type !== 'directory' || modernLink)) || sw.bundled) { // 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. return BB.resolve(makeFakeChild(name, topPath, tree, sw, requested)) @@ -100,13 +135,18 @@ function inflatableChild (onDiskChild, name, topPath, tree, sw, requested, opts) } } +function isGit (sw) { + const version = npa.resolve(sw.name, sw.version) + return (version && version.type === 'git') +} + function makeFakeChild (name, topPath, tree, sw, requested) { const from = sw.from || requested.raw const pkg = { name: name, version: sw.version, _id: name + '@' + sw.version, - _resolved: adaptResolved(requested, sw.resolved), + _resolved: sw.resolved || (isGit(sw) && sw.version), _requested: requested, _optional: sw.optional, _development: sw.dev, @@ -127,15 +167,15 @@ function makeFakeChild (name, topPath, tree, sw, requested) { } const child = createChild({ package: pkg, - loaded: true, + loaded: false, parent: tree, children: [], - fromShrinkwrap: true, + fromShrinkwrap: requested, fakeChild: sw, fromBundle: sw.bundled ? tree.fromBundle || tree : null, path: childPath(tree.path, pkg), - realpath: childPath(tree.realpath, pkg), - location: tree.location + '/' + pkg.name, + realpath: requested.type === 'directory' ? requested.fetchSpec : childPath(tree.realpath, pkg), + location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name, isLink: requested.type === 'directory', isInLink: tree.isLink, swRequires: sw.requires @@ -144,23 +184,6 @@ function makeFakeChild (name, topPath, tree, sw, requested) { return child } -function adaptResolved (requested, resolved) { - const registry = requested.scope - ? npm.config.get(`${requested.scope}:registry`) || npm.config.get('registry') - : npm.config.get('registry') - if (!isRegistry(requested) || (resolved && resolved.indexOf(registry) === 0)) { - // Nothing to worry about here. Pass it through. - return resolved - } else { - // We could fast-path for registry.npmjs.org here, but if we do, it - // would end up getting written back to the `resolved` field. By always - // returning `null` for other registries, `pacote.extract()` will take - // care of any required metadata fetches internally, without altering - // the tree we're going to write out to shrinkwrap/lockfile. - return null - } -} - function fetchChild (topPath, tree, sw, requested) { return fetchPackageMetadata(requested, topPath).then((pkg) => { pkg._from = sw.from || requested.raw @@ -178,7 +201,7 @@ function fetchChild (topPath, tree, sw, requested) { path: childPath(tree.path, pkg), realpath: isLink ? requested.fetchSpec : childPath(tree.realpath, pkg), children: pkg._bundled || [], - location: tree.location + '/' + pkg.name, + location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name, fromBundle: null, isLink: isLink, isInLink: tree.isLink, @@ -196,7 +219,11 @@ function fetchChild (topPath, tree, sw, requested) { function childIsEquivalent (sw, requested, child) { if (!child) return false if (child.fromShrinkwrap) return true - if (sw.integrity && child.package._integrity === sw.integrity) return true + if ( + sw.integrity && + child.package._integrity && + ssri.parse(sw.integrity).match(child.package._integrity) + ) return true if (child.isLink && requested.type === 'directory') return path.relative(child.realpath, requested.fetchSpec) === '' if (sw.resolved) return child.package._resolved === sw.resolved diff --git a/deps/npm/lib/install/is-only-optional.js b/deps/npm/lib/install/is-only-optional.js index 7366e9abe1..72d6f065e6 100644 --- a/deps/npm/lib/install/is-only-optional.js +++ b/deps/npm/lib/install/is-only-optional.js @@ -11,8 +11,9 @@ function isOptional (node, seen) { return false } seen.add(node) - + const swOptional = node.fromShrinkwrap && node.package._optional return node.requiredBy.every(function (req) { + if (req.fakeChild && swOptional) return true return isOptDep(req, node.package.name) || isOptional(req, seen) }) } diff --git a/deps/npm/lib/install/read-shrinkwrap.js b/deps/npm/lib/install/read-shrinkwrap.js index 45e883caa2..7074678011 100644 --- a/deps/npm/lib/install/read-shrinkwrap.js +++ b/deps/npm/lib/install/read-shrinkwrap.js @@ -25,14 +25,7 @@ function readShrinkwrap (child, next) { log.warn('read-shrinkwrap', 'Ignoring package-lock.json because there is already an npm-shrinkwrap.json. Please use only one of the two.') } const name = shrinkwrap ? 'npm-shrinkwrap.json' : 'package-lock.json' - let parsed = null - if (shrinkwrap || lockfile) { - try { - parsed = parseJSON(shrinkwrap || lockfile) - } catch (ex) { - throw ex - } - } + const parsed = parsePkgLock(shrinkwrap || lockfile, name) if (parsed && parsed.lockfileVersion !== PKGLOCK_VERSION) { log.warn('read-shrinkwrap', `This version of npm is compatible with lockfileVersion@${PKGLOCK_VERSION}, but ${name} was generated for lockfileVersion@${parsed.lockfileVersion || 0}. I'll try to do my best with it!`) } @@ -43,7 +36,8 @@ function readShrinkwrap (child, next) { function maybeReadFile (name, child) { return readFileAsync( - path.join(child.path, name) + path.join(child.path, name), + 'utf8' ).catch({code: 'ENOENT'}, () => null) } @@ -56,3 +50,59 @@ module.exports.andInflate = function (child, next) { } })) } + +const PARENT_RE = /\|{7,}/g +const OURS_RE = /<{7,}/g +const THEIRS_RE = /={7,}/g +const END_RE = />{7,}/g + +module.exports._isDiff = isDiff +function isDiff (str) { + return str.match(OURS_RE) && str.match(THEIRS_RE) && str.match(END_RE) +} + +module.exports._parsePkgLock = parsePkgLock +function parsePkgLock (str, filename) { + if (!str) { return null } + try { + return parseJSON(str) + } catch (e) { + if (isDiff(str)) { + log.warn('conflict', `A git conflict was detected in ${filename}. Attempting to auto-resolve.`) + log.warn('conflict', 'To make this happen automatically on git rebase/merge, consider using the npm-merge-driver:') + log.warn('conflict', '$ npx npm-merge-driver install -g') + const pieces = str.split(/[\n\r]+/g).reduce((acc, line) => { + if (line.match(PARENT_RE)) acc.state = 'parent' + else if (line.match(OURS_RE)) acc.state = 'ours' + else if (line.match(THEIRS_RE)) acc.state = 'theirs' + else if (line.match(END_RE)) acc.state = 'top' + else { + if (acc.state === 'top' || acc.state === 'ours') acc.ours += line + if (acc.state === 'top' || acc.state === 'theirs') acc.theirs += line + if (acc.state === 'top' || acc.state === 'parent') acc.parent += line + } + return acc + }, { + state: 'top', + ours: '', + theirs: '', + parent: '' + }) + try { + const ours = parseJSON(pieces.ours) + const theirs = parseJSON(pieces.theirs) + return reconcileLockfiles(ours, theirs) + } catch (_e) { + log.error('conflict', `Automatic conflict resolution failed. Please manually resolve conflicts in ${filename} and try again.`) + log.silly('conflict', `Error during resolution: ${_e}`) + throw e + } + } else { + throw e + } + } +} + +function reconcileLockfiles (parent, ours, theirs) { + return Object.assign({}, ours, theirs) +} diff --git a/deps/npm/lib/install/save.js b/deps/npm/lib/install/save.js index f0c61f555d..8bafcbfc6b 100644 --- a/deps/npm/lib/install/save.js +++ b/deps/npm/lib/install/save.js @@ -1,8 +1,8 @@ 'use strict' -const createShrinkwrap = require('../shrinkwrap.js').createShrinkwrap const deepSortObject = require('../utils/deep-sort-object.js') const detectIndent = require('detect-indent') +const detectNewline = require('detect-newline') const fs = require('graceful-fs') const iferr = require('iferr') const log = require('npmlog') @@ -10,6 +10,7 @@ const moduleName = require('../utils/module-name.js') const npm = require('../npm.js') const parseJSON = require('../utils/parse-json.js') const path = require('path') +const stringifyPackage = require('../utils/stringify-package') const validate = require('aproba') const without = require('lodash.without') const writeFileAtomic = require('write-file-atomic') @@ -44,9 +45,9 @@ exports.saveShrinkwrap = saveShrinkwrap function saveShrinkwrap (tree, next) { validate('OF', arguments) if (!npm.config.get('shrinkwrap') || !npm.config.get('package-lock')) { - next() + return next() } - createShrinkwrap(tree, {silent: false}, next) + require('../shrinkwrap.js').createShrinkwrap(tree, {silent: false}, next) } function savePackageJson (tree, next) { @@ -60,7 +61,8 @@ function savePackageJson (tree, next) { // don't use readJson, because we don't want to do all the other // tricky npm-specific stuff that's in there. fs.readFile(saveTarget, 'utf8', iferr(next, function (packagejson) { - const indent = detectIndent(packagejson).indent || ' ' + const indent = detectIndent(packagejson).indent + const newline = detectNewline(packagejson) try { tree.package = parseJSON(packagejson) } catch (ex) { @@ -122,7 +124,7 @@ function savePackageJson (tree, next) { tree.package.bundleDependencies = deepSortObject(bundle) } - var json = JSON.stringify(tree.package, null, indent) + '\n' + var json = stringifyPackage(tree.package, indent, newline) if (json === packagejson) { log.verbose('shrinkwrap', 'skipping write for package.json because there were no changes.') next() |