• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const BB = require('bluebird')
4
5let addBundled
6const childPath = require('../utils/child-path.js')
7const createChild = require('./node.js').create
8let fetchPackageMetadata
9const inflateBundled = require('./inflate-bundled.js')
10const moduleName = require('../utils/module-name.js')
11const normalizePackageData = require('normalize-package-data')
12const npm = require('../npm.js')
13const realizeShrinkwrapSpecifier = require('./realize-shrinkwrap-specifier.js')
14const validate = require('aproba')
15const path = require('path')
16const isRegistry = require('../utils/is-registry.js')
17const hasModernMeta = require('./has-modern-meta.js')
18const ssri = require('ssri')
19const npa = require('npm-package-arg')
20
21module.exports = function (tree, sw, opts, finishInflating) {
22  if (!fetchPackageMetadata) {
23    fetchPackageMetadata = BB.promisify(require('../fetch-package-metadata.js'))
24    addBundled = BB.promisify(fetchPackageMetadata.addBundled)
25  }
26  if (arguments.length === 3) {
27    finishInflating = opts
28    opts = {}
29  }
30  if (!npm.config.get('shrinkwrap') || !npm.config.get('package-lock')) {
31    return finishInflating()
32  }
33  tree.loaded = false
34  tree.hasRequiresFromLock = sw.requires
35  return inflateShrinkwrap(tree.path, tree, sw.dependencies, opts).then(
36    () => finishInflating(),
37    finishInflating
38  )
39}
40
41function inflateShrinkwrap (topPath, tree, swdeps, opts) {
42  if (!swdeps) return Promise.resolve()
43  if (!opts) opts = {}
44  const onDisk = {}
45  tree.children.forEach((child) => {
46    onDisk[moduleName(child)] = child
47  })
48
49  tree.children = []
50
51  return BB.each(Object.keys(swdeps), (name) => {
52    const sw = swdeps[name]
53    const dependencies = sw.dependencies || {}
54    const requested = realizeShrinkwrapSpecifier(name, sw, topPath)
55
56    if (Object.keys(sw).length === 0) {
57      let message = `Object for dependency "${name}" is empty.\n`
58      message += 'Something went wrong. Regenerate the package-lock.json with "npm install".\n'
59      message += 'If using a shrinkwrap, regenerate with "npm shrinkwrap".'
60      return Promise.reject(new Error(message))
61    }
62
63    return inflatableChild(
64      onDisk[name], name, topPath, tree, sw, requested, opts
65    ).then((child) => {
66      child.hasRequiresFromLock = tree.hasRequiresFromLock
67      return inflateShrinkwrap(topPath, child, dependencies)
68    })
69  })
70}
71
72function normalizePackageDataNoErrors (pkg) {
73  try {
74    normalizePackageData(pkg)
75  } catch (ex) {
76    // don't care
77  }
78}
79
80function quotemeta (str) {
81  return str.replace(/([^A-Za-z_0-9/])/g, '\\$1')
82}
83
84function tarballToVersion (name, tb) {
85  const registry = quotemeta(npm.config.get('registry') || '')
86    .replace(/https?:/, 'https?:')
87    .replace(/([^/])$/, '$1/')
88  let matchRegTarball
89  if (name) {
90    const nameMatch = quotemeta(name)
91    matchRegTarball = new RegExp(`^${registry}${nameMatch}/-/${nameMatch}-(.*)[.]tgz$`)
92  } else {
93    matchRegTarball = new RegExp(`^${registry}(.*)?/-/\\1-(.*)[.]tgz$`)
94  }
95  const match = tb.match(matchRegTarball)
96  if (!match) return
97  return match[2] || match[1]
98}
99
100function relativizeLink (name, spec, topPath, requested) {
101  if (!spec.startsWith('file:')) {
102    return
103  }
104
105  let requestedPath = requested.fetchSpec
106  if (requested.type === 'file') {
107    requestedPath = path.dirname(requestedPath)
108  }
109
110  const relativized = path.relative(requestedPath, path.resolve(topPath, spec.slice(5)))
111  return 'file:' + relativized
112}
113
114function inflatableChild (onDiskChild, name, topPath, tree, sw, requested, opts) {
115  validate('OSSOOOO|ZSSOOOO', arguments)
116  const usesIntegrity = (
117    requested.registry ||
118    requested.type === 'remote' ||
119    requested.type === 'file'
120  )
121  const regTarball = tarballToVersion(name, sw.version)
122  if (regTarball) {
123    sw.resolved = sw.version
124    sw.version = regTarball
125  }
126  if (sw.requires) {
127    Object.keys(sw.requires).forEach(name => {
128      const spec = sw.requires[name]
129      sw.requires[name] = tarballToVersion(name, spec) ||
130        relativizeLink(name, spec, topPath, requested) ||
131        spec
132    })
133  }
134  const modernLink = requested.type === 'directory' && !sw.from
135  if (hasModernMeta(onDiskChild) && childIsEquivalent(sw, requested, onDiskChild)) {
136    // The version on disk matches the shrinkwrap entry.
137    if (!onDiskChild.fromShrinkwrap) onDiskChild.fromShrinkwrap = requested
138    onDiskChild.package._requested = requested
139    onDiskChild.package._spec = requested.rawSpec
140    onDiskChild.package._where = topPath
141    onDiskChild.package._optional = sw.optional
142    onDiskChild.package._development = sw.dev
143    onDiskChild.package._inBundle = sw.bundled
144    onDiskChild.fromBundle = (sw.bundled || onDiskChild.package._inBundle) ? tree.fromBundle || tree : null
145    if (!onDiskChild.package._args) onDiskChild.package._args = []
146    onDiskChild.package._args.push([String(requested), topPath])
147    // non-npm registries can and will return unnormalized data, plus
148    // even the npm registry may have package data normalized with older
149    // normalization rules. This ensures we get package data in a consistent,
150    // stable format.
151    normalizePackageDataNoErrors(onDiskChild.package)
152    onDiskChild.swRequires = sw.requires
153    tree.children.push(onDiskChild)
154    return BB.resolve(onDiskChild)
155  } else if ((sw.version && (sw.integrity || !usesIntegrity) && (requested.type !== 'directory' || modernLink)) || sw.bundled) {
156    // The shrinkwrap entry has an integrity field. We can fake a pkg to get
157    // the installer to do a content-address fetch from the cache, if possible.
158    return BB.resolve(makeFakeChild(name, topPath, tree, sw, requested))
159  } else {
160    // It's not on disk, and we can't just look it up by address -- do a full
161    // fpm/inflate bundle pass. For registry deps, this will go straight to the
162    // tarball URL, as if it were a remote tarball dep.
163    return fetchChild(topPath, tree, sw, requested)
164  }
165}
166
167function isGit (sw) {
168  const version = npa.resolve(sw.name, sw.version)
169  return (version && version.type === 'git')
170}
171
172function makeFakeChild (name, topPath, tree, sw, requested) {
173  const isDirectory = requested.type === 'directory'
174  const from = sw.from || requested.raw
175  const pkg = {
176    name: name,
177    version: sw.version,
178    _id: name + '@' + sw.version,
179    _resolved: sw.resolved || (isGit(sw) && sw.version),
180    _requested: requested,
181    _optional: sw.optional,
182    _development: sw.dev,
183    _inBundle: sw.bundled,
184    _integrity: sw.integrity,
185    _from: from,
186    _spec: requested.rawSpec,
187    _where: topPath,
188    _args: [[requested.toString(), topPath]],
189    dependencies: sw.requires
190  }
191
192  if (!sw.bundled) {
193    const bundleDependencies = Object.keys(sw.dependencies || {}).filter((d) => sw.dependencies[d].bundled)
194    if (bundleDependencies.length === 0) {
195      pkg.bundleDependencies = bundleDependencies
196    }
197  }
198  const child = createChild({
199    package: pkg,
200    loaded: isDirectory,
201    parent: tree,
202    children: [],
203    fromShrinkwrap: requested,
204    fakeChild: sw,
205    fromBundle: sw.bundled ? tree.fromBundle || tree : null,
206    path: childPath(tree.path, pkg),
207    realpath: isDirectory ? requested.fetchSpec : childPath(tree.realpath, pkg),
208    location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name,
209    isLink: isDirectory,
210    isInLink: tree.isLink || tree.isInLink,
211    swRequires: sw.requires
212  })
213  tree.children.push(child)
214  return child
215}
216
217function fetchChild (topPath, tree, sw, requested) {
218  return fetchPackageMetadata(requested, topPath).then((pkg) => {
219    pkg._from = sw.from || requested.raw
220    pkg._optional = sw.optional
221    pkg._development = sw.dev
222    pkg._inBundle = false
223    return addBundled(pkg).then(() => pkg)
224  }).then((pkg) => {
225    var isLink = pkg._requested.type === 'directory'
226    const child = createChild({
227      package: pkg,
228      loaded: false,
229      parent: tree,
230      fromShrinkwrap: requested,
231      path: childPath(tree.path, pkg),
232      realpath: isLink ? requested.fetchSpec : childPath(tree.realpath, pkg),
233      children: pkg._bundled || [],
234      location: (tree.location === '/' ? '' : tree.location + '/') + pkg.name,
235      fromBundle: null,
236      isLink: isLink,
237      isInLink: tree.isLink,
238      swRequires: sw.requires
239    })
240    tree.children.push(child)
241    if (pkg._bundled) {
242      delete pkg._bundled
243      inflateBundled(child, child, child.children)
244    }
245    return child
246  })
247}
248
249function childIsEquivalent (sw, requested, child) {
250  if (!child) return false
251  if (child.fromShrinkwrap) return true
252  if (
253    sw.integrity &&
254    child.package._integrity &&
255    ssri.parse(sw.integrity).match(child.package._integrity)
256  ) return true
257  if (child.isLink && requested.type === 'directory') return path.relative(child.realpath, requested.fetchSpec) === ''
258
259  if (sw.resolved) return child.package._resolved === sw.resolved
260  if (!isRegistry(requested) && sw.from) return child.package._from === sw.from
261  if (!isRegistry(requested) && child.package._resolved) return sw.version === child.package._resolved
262  return child.package.version === sw.version
263}
264