inflate-shrinkwrap.js 8.9 KB

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