UNPKG

8.51 kBJavaScriptView Raw
1'use strict'
2
3const BB = require('bluebird')
4
5const chain = require('slide').chain
6const detectIndent = require('detect-indent')
7const fs = BB.promisifyAll(require('graceful-fs'))
8const getRequested = require('./install/get-requested.js')
9const id = require('./install/deps.js')
10const iferr = require('iferr')
11const isDevDep = require('./install/is-dev-dep.js')
12const isOptDep = require('./install/is-opt-dep.js')
13const isProdDep = require('./install/is-prod-dep.js')
14const lifecycle = require('./utils/lifecycle.js')
15const log = require('npmlog')
16const moduleName = require('./utils/module-name.js')
17const move = require('move-concurrently')
18const npm = require('./npm.js')
19const path = require('path')
20const pkgSri = require('./utils/package-integrity.js')
21const readPackageTree = BB.promisify(require('read-package-tree'))
22const ssri = require('ssri')
23const validate = require('aproba')
24const writeFileAtomic = require('write-file-atomic')
25
26const PKGLOCK = 'package-lock.json'
27const SHRINKWRAP = 'npm-shrinkwrap.json'
28const PKGLOCK_VERSION = npm.lockfileVersion
29
30// emit JSON describing versions of all packages currently installed (for later
31// use with shrinkwrap install)
32shrinkwrap.usage = 'npm shrinkwrap'
33
34module.exports = exports = shrinkwrap
35function shrinkwrap (args, silent, cb) {
36 if (typeof cb !== 'function') {
37 cb = silent
38 silent = false
39 }
40
41 if (args.length) {
42 log.warn('shrinkwrap', "doesn't take positional args")
43 }
44
45 move(
46 path.resolve(npm.prefix, PKGLOCK),
47 path.resolve(npm.prefix, SHRINKWRAP),
48 { Promise: BB }
49 ).then(() => {
50 log.notice('', `${PKGLOCK} has been renamed to ${SHRINKWRAP}. ${SHRINKWRAP} will be used for future installations.`)
51 return fs.readFileAsync(path.resolve(npm.prefix, SHRINKWRAP)).then((d) => {
52 return JSON.parse(d)
53 })
54 }, (err) => {
55 if (err.code !== 'ENOENT') {
56 throw err
57 } else {
58 return readPackageTree(npm.localPrefix).then(
59 id.computeMetadata
60 ).then((tree) => {
61 return BB.fromNode((cb) => {
62 createShrinkwrap(tree, {
63 silent,
64 defaultFile: SHRINKWRAP
65 }, cb)
66 })
67 })
68 }
69 }).then((data) => cb(null, data), cb)
70}
71
72module.exports.createShrinkwrap = createShrinkwrap
73
74function createShrinkwrap (tree, opts, cb) {
75 opts = opts || {}
76 lifecycle(tree.package, 'preshrinkwrap', tree.path, function () {
77 const pkginfo = treeToShrinkwrap(tree)
78 chain([
79 [lifecycle, tree.package, 'shrinkwrap', tree.path],
80 [shrinkwrap_, tree.path, pkginfo, opts],
81 [lifecycle, tree.package, 'postshrinkwrap', tree.path]
82 ], iferr(cb, function (data) {
83 cb(null, pkginfo)
84 }))
85 })
86}
87
88function treeToShrinkwrap (tree) {
89 validate('O', arguments)
90 var pkginfo = {}
91 if (tree.package.name) pkginfo.name = tree.package.name
92 if (tree.package.version) pkginfo.version = tree.package.version
93 if (tree.children.length) {
94 shrinkwrapDeps(pkginfo.dependencies = {}, tree, tree)
95 }
96 return pkginfo
97}
98
99function shrinkwrapDeps (deps, top, tree, seen) {
100 validate('OOO', [deps, top, tree])
101 if (!seen) seen = {}
102 if (seen[tree.path]) return
103 seen[tree.path] = true
104 tree.children.sort(function (aa, bb) { return moduleName(aa).localeCompare(moduleName(bb)) }).forEach(function (child) {
105 var childIsOnlyDev = isOnlyDev(child)
106 if (child.fakeChild) {
107 deps[moduleName(child)] = child.fakeChild
108 return
109 }
110 var pkginfo = deps[moduleName(child)] = {}
111 var req = child.package._requested || getRequested(child)
112 if (req.type === 'directory' || req.type === 'file') {
113 pkginfo.version = 'file:' + path.relative(top.path, child.package._resolved || req.fetchSpec)
114 } else if (!req.registry && !child.fromBundle) {
115 pkginfo.version = child.package._resolved || req.saveSpec || req.rawSpec
116 } else {
117 pkginfo.version = child.package.version
118 }
119 if (child.fromBundle || child.isInLink) {
120 pkginfo.bundled = true
121 } else {
122 if (req.registry) {
123 pkginfo.resolved = child.package._resolved
124 }
125 // no integrity for git deps as integirty hashes are based on the
126 // tarball and we can't (yet) create consistent tarballs from a stable
127 // source.
128 if (req.type !== 'git') {
129 pkginfo.integrity = child.package._integrity
130 if (!pkginfo.integrity && child.package._shasum) {
131 pkginfo.integrity = ssri.fromHex(child.package._shasum, 'sha1')
132 }
133 }
134 }
135 if (childIsOnlyDev) pkginfo.dev = true
136 if (isOptional(child)) pkginfo.optional = true
137 if (child.children.length) {
138 pkginfo.dependencies = {}
139 shrinkwrapDeps(pkginfo.dependencies, top, child, seen)
140 }
141 })
142}
143
144function shrinkwrap_ (dir, pkginfo, opts, cb) {
145 save(dir, pkginfo, opts, cb)
146}
147
148function save (dir, pkginfo, opts, cb) {
149 // copy the keys over in a well defined order
150 // because javascript objects serialize arbitrarily
151 BB.join(
152 checkPackageFile(dir, SHRINKWRAP),
153 checkPackageFile(dir, PKGLOCK),
154 checkPackageFile(dir, 'package.json'),
155 (shrinkwrap, lockfile, pkg) => {
156 const info = (
157 shrinkwrap ||
158 lockfile ||
159 {
160 path: path.resolve(dir, opts.defaultFile || PKGLOCK),
161 data: '{}',
162 indent: (pkg && pkg.indent) || 2
163 }
164 )
165 const updated = updateLockfileMetadata(pkginfo, pkg && pkg.data)
166 const swdata = JSON.stringify(updated, null, info.indent) + '\n'
167 writeFileAtomic(info.path, swdata, (err) => {
168 if (err) return cb(err)
169 if (opts.silent) return cb(null, pkginfo)
170 if (!shrinkwrap && !lockfile) {
171 log.notice('', `created a lockfile as ${path.basename(info.path)}. You should commit this file.`)
172 }
173 cb(null, pkginfo)
174 })
175 }
176 ).then((file) => {
177 }, cb)
178}
179
180function updateLockfileMetadata (pkginfo, pkgJson) {
181 // This is a lot of work just to make sure the extra metadata fields are
182 // between version and dependencies fields, without affecting any other stuff
183 const newPkg = {}
184 let metainfoWritten = false
185 const metainfo = new Set([
186 'lockfileVersion',
187 'packageIntegrity',
188 'preserveSymlinks'
189 ])
190 Object.keys(pkginfo).forEach((k) => {
191 if (k === 'dependencies') {
192 writeMetainfo(newPkg)
193 }
194 if (!metainfo.has(k)) {
195 newPkg[k] = pkginfo[k]
196 }
197 if (k === 'version') {
198 writeMetainfo(newPkg)
199 }
200 })
201 if (!metainfoWritten) {
202 writeMetainfo(newPkg)
203 }
204 function writeMetainfo (pkginfo) {
205 pkginfo.lockfileVersion = PKGLOCK_VERSION
206 pkginfo.packageIntegrity = pkgJson && pkgSri.hash(pkgJson)
207 if (process.env.NODE_PRESERVE_SYMLINKS) {
208 pkginfo.preserveSymlinks = process.env.NODE_PRESERVE_SYMLINKS
209 }
210 metainfoWritten = true
211 }
212 return newPkg
213}
214
215function checkPackageFile (dir, name) {
216 const file = path.resolve(dir, name)
217 return fs.readFileAsync(
218 file, 'utf8'
219 ).then((data) => {
220 return {
221 path: file,
222 data: JSON.parse(data),
223 indent: detectIndent(data).indent || 2
224 }
225 }).catch({code: 'ENOENT'}, () => {})
226}
227
228// Returns true if the module `node` is only required direcctly as a dev
229// dependency of the top level or transitively _from_ top level dev
230// dependencies.
231// Dual mode modules (that are both dev AND prod) should return false.
232function isOnlyDev (node, seen) {
233 if (!seen) seen = {}
234 return node.requiredBy.length && node.requiredBy.every(andIsOnlyDev(moduleName(node), seen))
235}
236
237// There is a known limitation with this implementation: If a dependency is
238// ONLY required by cycles that are detached from the top level then it will
239// ultimately return true.
240//
241// This is ok though: We don't allow shrinkwraps with extraneous deps and
242// these situation is caught by the extraneous checker before we get here.
243function andIsOnlyDev (name, seen) {
244 return function (req) {
245 var isDev = isDevDep(req, name)
246 var isProd = isProdDep(req, name)
247 if (req.isTop) {
248 return isDev && !isProd
249 } else {
250 if (seen[req.path]) return true
251 seen[req.path] = true
252 return isOnlyDev(req, seen)
253 }
254 }
255}
256
257function isOptional (node, seen) {
258 if (!seen) seen = {}
259 // If a node is not required by anything, then we've reached
260 // the top level package.
261 if (seen[node.path] || node.requiredBy.length === 0) {
262 return false
263 }
264 seen[node.path] = true
265
266 return node.requiredBy.every(function (req) {
267 return isOptDep(req, node.package.name) || isOptional(req, seen)
268 })
269}