UNPKG

9.13 kBJavaScriptView Raw
1'use strict'
2
3const BB = require('bluebird')
4
5const chain = require('slide').chain
6const detectIndent = require('detect-indent')
7const detectNewline = require('detect-newline')
8const readFile = BB.promisify(require('graceful-fs').readFile)
9const getRequested = require('./install/get-requested.js')
10const id = require('./install/deps.js')
11const iferr = require('iferr')
12const isOnlyOptional = require('./install/is-only-optional.js')
13const isOnlyDev = require('./install/is-only-dev.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 readPackageTree = BB.promisify(require('read-package-tree'))
21const ssri = require('ssri')
22const stringifyPackage = require('stringify-package')
23const validate = require('aproba')
24const writeFileAtomic = require('write-file-atomic')
25const unixFormatPath = require('./utils/unix-format-path.js')
26const isRegistry = require('./utils/is-registry.js')
27
28const PKGLOCK = 'package-lock.json'
29const SHRINKWRAP = 'npm-shrinkwrap.json'
30const PKGLOCK_VERSION = npm.lockfileVersion
31
32// emit JSON describing versions of all packages currently installed (for later
33// use with shrinkwrap install)
34shrinkwrap.usage = 'npm shrinkwrap'
35
36module.exports = exports = shrinkwrap
37exports.treeToShrinkwrap = treeToShrinkwrap
38
39function shrinkwrap (args, silent, cb) {
40 if (typeof cb !== 'function') {
41 cb = silent
42 silent = false
43 }
44
45 if (args.length) {
46 log.warn('shrinkwrap', "doesn't take positional args")
47 }
48
49 move(
50 path.resolve(npm.prefix, PKGLOCK),
51 path.resolve(npm.prefix, SHRINKWRAP),
52 { Promise: BB }
53 ).then(() => {
54 log.notice('', `${PKGLOCK} has been renamed to ${SHRINKWRAP}. ${SHRINKWRAP} will be used for future installations.`)
55 return readFile(path.resolve(npm.prefix, SHRINKWRAP)).then((d) => {
56 return JSON.parse(d)
57 })
58 }, (err) => {
59 if (err.code !== 'ENOENT') {
60 throw err
61 } else {
62 return readPackageTree(npm.localPrefix).then(
63 id.computeMetadata
64 ).then((tree) => {
65 return BB.fromNode((cb) => {
66 createShrinkwrap(tree, {
67 silent,
68 defaultFile: SHRINKWRAP
69 }, cb)
70 })
71 })
72 }
73 }).then((data) => cb(null, data), cb)
74}
75
76module.exports.createShrinkwrap = createShrinkwrap
77
78function createShrinkwrap (tree, opts, cb) {
79 opts = opts || {}
80 lifecycle(tree.package, 'preshrinkwrap', tree.path, function () {
81 const pkginfo = treeToShrinkwrap(tree)
82 chain([
83 [lifecycle, tree.package, 'shrinkwrap', tree.path],
84 [shrinkwrap_, tree.path, pkginfo, opts],
85 [lifecycle, tree.package, 'postshrinkwrap', tree.path]
86 ], iferr(cb, function (data) {
87 cb(null, pkginfo)
88 }))
89 })
90}
91
92function treeToShrinkwrap (tree) {
93 validate('O', arguments)
94 var pkginfo = {}
95 if (tree.package.name) pkginfo.name = tree.package.name
96 if (tree.package.version) pkginfo.version = tree.package.version
97 if (tree.children.length) {
98 pkginfo.requires = true
99 shrinkwrapDeps(pkginfo.dependencies = {}, tree, tree)
100 }
101 return pkginfo
102}
103
104function shrinkwrapDeps (deps, top, tree, seen) {
105 validate('OOO', [deps, top, tree])
106 if (!seen) seen = new Set()
107 if (seen.has(tree)) return
108 seen.add(tree)
109 sortModules(tree.children).forEach(function (child) {
110 var childIsOnlyDev = isOnlyDev(child)
111 var pkginfo = deps[moduleName(child)] = {}
112 var requested = getRequested(child) || child.package._requested || {}
113 pkginfo.version = childVersion(top, child, requested)
114 if (requested.type === 'git' && child.package._from) {
115 pkginfo.from = child.package._from
116 }
117 if (child.fromBundle || child.isInLink) {
118 pkginfo.bundled = true
119 } else {
120 if (isRegistry(requested)) {
121 pkginfo.resolved = child.package._resolved
122 }
123 // no integrity for git deps as integirty hashes are based on the
124 // tarball and we can't (yet) create consistent tarballs from a stable
125 // source.
126 if (requested.type !== 'git') {
127 pkginfo.integrity = child.package._integrity || undefined
128 if (!pkginfo.integrity && child.package._shasum) {
129 pkginfo.integrity = ssri.fromHex(child.package._shasum, 'sha1')
130 }
131 }
132 }
133 if (childIsOnlyDev) pkginfo.dev = true
134 if (isOnlyOptional(child)) pkginfo.optional = true
135 if (child.requires.length) {
136 pkginfo.requires = {}
137 sortModules(child.requires).forEach((required) => {
138 var requested = getRequested(required, child) || required.package._requested || {}
139 pkginfo.requires[moduleName(required)] = childRequested(top, required, requested)
140 })
141 }
142 if (child.children.length) {
143 pkginfo.dependencies = {}
144 shrinkwrapDeps(pkginfo.dependencies, top, child, seen)
145 }
146 })
147}
148
149function sortModules (modules) {
150 // sort modules with the locale-agnostic Unicode sort
151 var sortedModuleNames = modules.map(moduleName).sort()
152 return modules.sort((a, b) => (
153 sortedModuleNames.indexOf(moduleName(a)) - sortedModuleNames.indexOf(moduleName(b))
154 ))
155}
156
157function childVersion (top, child, req) {
158 if (req.type === 'directory' || req.type === 'file') {
159 return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || req.fetchSpec))
160 } else if (!isRegistry(req) && !child.fromBundle) {
161 return child.package._resolved || req.saveSpec || req.rawSpec
162 } else {
163 return child.package.version
164 }
165}
166
167function childRequested (top, child, requested) {
168 if (requested.type === 'directory' || requested.type === 'file') {
169 return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || requested.fetchSpec))
170 } else if (!isRegistry(requested) && !child.fromBundle) {
171 return child.package._resolved || requested.saveSpec || requested.rawSpec
172 } else if (requested.type === 'tag') {
173 // tags are not ranges we can match against, so we invent a "reasonable"
174 // one based on what we actually installed.
175 return npm.config.get('save-prefix') + child.package.version
176 } else if (requested.saveSpec || requested.rawSpec) {
177 return requested.saveSpec || requested.rawSpec
178 } else if (child.package._from || (child.package._requested && child.package._requested.rawSpec)) {
179 return child.package._from.replace(/^@?[^@]+@/, '') || child.package._requested.rawSpec
180 } else {
181 return child.package.version
182 }
183}
184
185function shrinkwrap_ (dir, pkginfo, opts, cb) {
186 save(dir, pkginfo, opts, cb)
187}
188
189function save (dir, pkginfo, opts, cb) {
190 // copy the keys over in a well defined order
191 // because javascript objects serialize arbitrarily
192 BB.join(
193 checkPackageFile(dir, SHRINKWRAP),
194 checkPackageFile(dir, PKGLOCK),
195 checkPackageFile(dir, 'package.json'),
196 (shrinkwrap, lockfile, pkg) => {
197 const info = (
198 shrinkwrap ||
199 lockfile ||
200 {
201 path: path.resolve(dir, opts.defaultFile || PKGLOCK),
202 data: '{}',
203 indent: pkg && pkg.indent,
204 newline: pkg && pkg.newline
205 }
206 )
207 const updated = updateLockfileMetadata(pkginfo, pkg && JSON.parse(pkg.raw))
208 const swdata = stringifyPackage(updated, info.indent, info.newline)
209 if (swdata === info.raw) {
210 // skip writing if file is identical
211 log.verbose('shrinkwrap', `skipping write for ${path.basename(info.path)} because there were no changes.`)
212 cb(null, pkginfo)
213 } else {
214 writeFileAtomic(info.path, swdata, (err) => {
215 if (err) return cb(err)
216 if (opts.silent) return cb(null, pkginfo)
217 if (!shrinkwrap && !lockfile) {
218 log.notice('', `created a lockfile as ${path.basename(info.path)}. You should commit this file.`)
219 }
220 cb(null, pkginfo)
221 })
222 }
223 }
224 ).then((file) => {
225 }, cb)
226}
227
228function updateLockfileMetadata (pkginfo, pkgJson) {
229 // This is a lot of work just to make sure the extra metadata fields are
230 // between version and dependencies fields, without affecting any other stuff
231 const newPkg = {}
232 let metainfoWritten = false
233 const metainfo = new Set([
234 'lockfileVersion',
235 'preserveSymlinks'
236 ])
237 Object.keys(pkginfo).forEach((k) => {
238 if (k === 'dependencies') {
239 writeMetainfo(newPkg)
240 }
241 if (!metainfo.has(k)) {
242 newPkg[k] = pkginfo[k]
243 }
244 if (k === 'version') {
245 writeMetainfo(newPkg)
246 }
247 })
248 if (!metainfoWritten) {
249 writeMetainfo(newPkg)
250 }
251 function writeMetainfo (pkginfo) {
252 pkginfo.lockfileVersion = PKGLOCK_VERSION
253 if (process.env.NODE_PRESERVE_SYMLINKS) {
254 pkginfo.preserveSymlinks = process.env.NODE_PRESERVE_SYMLINKS
255 }
256 metainfoWritten = true
257 }
258 return newPkg
259}
260
261function checkPackageFile (dir, name) {
262 const file = path.resolve(dir, name)
263 return readFile(
264 file, 'utf8'
265 ).then((data) => {
266 return {
267 path: file,
268 raw: data,
269 indent: detectIndent(data).indent,
270 newline: detectNewline(data)
271 }
272 }).catch({code: 'ENOENT'}, () => {})
273}