pack.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. 'use strict'
  2. // npm pack <pkg>
  3. // Packs the specified package into a .tgz file, which can then
  4. // be installed.
  5. // Set this early to avoid issues with circular dependencies.
  6. module.exports = pack
  7. const BB = require('bluebird')
  8. const byteSize = require('byte-size')
  9. const cacache = require('cacache')
  10. const columnify = require('columnify')
  11. const cp = require('child_process')
  12. const deprCheck = require('./utils/depr-check')
  13. const fpm = require('./fetch-package-metadata')
  14. const fs = require('graceful-fs')
  15. const install = require('./install')
  16. const lifecycle = BB.promisify(require('./utils/lifecycle'))
  17. const log = require('npmlog')
  18. const move = require('move-concurrently')
  19. const npm = require('./npm')
  20. const npmConfig = require('./config/figgy-config.js')
  21. const output = require('./utils/output')
  22. const pacote = require('pacote')
  23. const path = require('path')
  24. const PassThrough = require('stream').PassThrough
  25. const pathIsInside = require('path-is-inside')
  26. const pipe = BB.promisify(require('mississippi').pipe)
  27. const prepublishWarning = require('./utils/warn-deprecated')('prepublish-on-install')
  28. const pinflight = require('promise-inflight')
  29. const readJson = BB.promisify(require('read-package-json'))
  30. const tar = require('tar')
  31. const packlist = require('npm-packlist')
  32. const ssri = require('ssri')
  33. pack.usage = 'npm pack [[<@scope>/]<pkg>...] [--dry-run]'
  34. // if it can be installed, it can be packed.
  35. pack.completion = install.completion
  36. function pack (args, silent, cb) {
  37. const cwd = process.cwd()
  38. if (typeof cb !== 'function') {
  39. cb = silent
  40. silent = false
  41. }
  42. if (args.length === 0) args = ['.']
  43. BB.all(
  44. args.map((arg) => pack_(arg, cwd))
  45. ).then((tarballs) => {
  46. if (!silent && npm.config.get('json')) {
  47. output(JSON.stringify(tarballs, null, 2))
  48. } else if (!silent) {
  49. tarballs.forEach(logContents)
  50. output(tarballs.map((f) => path.relative(cwd, f.filename)).join('\n'))
  51. }
  52. return tarballs
  53. }).nodeify(cb)
  54. }
  55. function pack_ (pkg, dir) {
  56. return BB.fromNode((cb) => fpm(pkg, dir, cb)).then((mani) => {
  57. let name = mani.name[0] === '@'
  58. // scoped packages get special treatment
  59. ? mani.name.substr(1).replace(/\//g, '-')
  60. : mani.name
  61. const target = `${name}-${mani.version}.tgz`
  62. return pinflight(target, () => {
  63. const dryRun = npm.config.get('dry-run')
  64. if (mani._requested.type === 'directory') {
  65. return prepareDirectory(mani._resolved)
  66. .then(() => {
  67. return packDirectory(mani, mani._resolved, target, target, true, dryRun)
  68. })
  69. } else if (dryRun) {
  70. log.verbose('pack', '--dry-run mode enabled. Skipping write.')
  71. return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'packing'}, (tmp) => {
  72. const tmpTarget = path.join(tmp, path.basename(target))
  73. return packFromPackage(pkg, tmpTarget, target)
  74. })
  75. } else {
  76. return packFromPackage(pkg, target, target)
  77. }
  78. })
  79. })
  80. }
  81. function packFromPackage (arg, target, filename) {
  82. const opts = npmConfig()
  83. return pacote.tarball.toFile(arg, target, opts)
  84. .then(() => cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'unpacking'}, (tmp) => {
  85. const tmpTarget = path.join(tmp, filename)
  86. return pacote.extract(arg, tmpTarget, opts)
  87. .then(() => readJson(path.join(tmpTarget, 'package.json')))
  88. }))
  89. .then((pkg) => getContents(pkg, target, filename))
  90. }
  91. module.exports.prepareDirectory = prepareDirectory
  92. function prepareDirectory (dir) {
  93. return readJson(path.join(dir, 'package.json')).then((pkg) => {
  94. if (!pkg.name) {
  95. throw new Error('package.json requires a "name" field')
  96. }
  97. if (!pkg.version) {
  98. throw new Error('package.json requires a valid "version" field')
  99. }
  100. if (!pathIsInside(dir, npm.tmp)) {
  101. if (pkg.scripts && pkg.scripts.prepublish) {
  102. prepublishWarning([
  103. 'As of npm@5, `prepublish` scripts are deprecated.',
  104. 'Use `prepare` for build steps and `prepublishOnly` for upload-only.',
  105. 'See the deprecation note in `npm help scripts` for more information.'
  106. ])
  107. }
  108. if (npm.config.get('ignore-prepublish')) {
  109. return lifecycle(pkg, 'prepare', dir).then(() => pkg)
  110. } else {
  111. return lifecycle(pkg, 'prepublish', dir).then(() => {
  112. return lifecycle(pkg, 'prepare', dir)
  113. }).then(() => pkg)
  114. }
  115. }
  116. return pkg
  117. })
  118. }
  119. module.exports.packDirectory = packDirectory
  120. function packDirectory (mani, dir, target, filename, logIt, dryRun) {
  121. deprCheck(mani)
  122. return readJson(path.join(dir, 'package.json')).then((pkg) => {
  123. return lifecycle(pkg, 'prepack', dir)
  124. }).then(() => {
  125. return readJson(path.join(dir, 'package.json'))
  126. }).then((pkg) => {
  127. return cacache.tmp.withTmp(npm.tmp, {tmpPrefix: 'packing'}, (tmp) => {
  128. const tmpTarget = path.join(tmp, path.basename(target))
  129. const tarOpt = {
  130. file: tmpTarget,
  131. cwd: dir,
  132. prefix: 'package/',
  133. portable: true,
  134. // Provide a specific date in the 1980s for the benefit of zip,
  135. // which is confounded by files dated at the Unix epoch 0.
  136. mtime: new Date('1985-10-26T08:15:00.000Z'),
  137. gzip: true
  138. }
  139. return BB.resolve(packlist({ path: dir }))
  140. // NOTE: node-tar does some Magic Stuff depending on prefixes for files
  141. // specifically with @ signs, so we just neutralize that one
  142. // and any such future "features" by prepending `./`
  143. .then((files) => tar.create(tarOpt, files.map((f) => `./${f}`)))
  144. .then(() => getContents(pkg, tmpTarget, filename, logIt))
  145. // thread the content info through
  146. .tap(() => {
  147. if (dryRun) {
  148. log.verbose('pack', '--dry-run mode enabled. Skipping write.')
  149. } else {
  150. return move(tmpTarget, target, {Promise: BB, fs})
  151. }
  152. })
  153. .tap(() => lifecycle(pkg, 'postpack', dir))
  154. })
  155. })
  156. }
  157. module.exports.logContents = logContents
  158. function logContents (tarball) {
  159. log.notice('')
  160. log.notice('', `${npm.config.get('unicode') ? '📦 ' : 'package:'} ${tarball.name}@${tarball.version}`)
  161. log.notice('=== Tarball Contents ===')
  162. if (tarball.files.length) {
  163. log.notice('', columnify(tarball.files.map((f) => {
  164. const bytes = byteSize(f.size)
  165. return {path: f.path, size: `${bytes.value}${bytes.unit}`}
  166. }), {
  167. include: ['size', 'path'],
  168. showHeaders: false
  169. }))
  170. }
  171. if (tarball.bundled.length) {
  172. log.notice('=== Bundled Dependencies ===')
  173. tarball.bundled.forEach((name) => log.notice('', name))
  174. }
  175. log.notice('=== Tarball Details ===')
  176. log.notice('', columnify([
  177. {name: 'name:', value: tarball.name},
  178. {name: 'version:', value: tarball.version},
  179. tarball.filename && {name: 'filename:', value: tarball.filename},
  180. {name: 'package size:', value: byteSize(tarball.size)},
  181. {name: 'unpacked size:', value: byteSize(tarball.unpackedSize)},
  182. {name: 'shasum:', value: tarball.shasum},
  183. {
  184. name: 'integrity:',
  185. value: tarball.integrity.toString().substr(0, 20) + '[...]' + tarball.integrity.toString().substr(80)},
  186. tarball.bundled.length && {name: 'bundled deps:', value: tarball.bundled.length},
  187. tarball.bundled.length && {name: 'bundled files:', value: tarball.entryCount - tarball.files.length},
  188. tarball.bundled.length && {name: 'own files:', value: tarball.files.length},
  189. {name: 'total files:', value: tarball.entryCount}
  190. ].filter((x) => x), {
  191. include: ['name', 'value'],
  192. showHeaders: false
  193. }))
  194. log.notice('', '')
  195. }
  196. module.exports.getContents = getContents
  197. function getContents (pkg, target, filename, silent) {
  198. const bundledWanted = new Set(
  199. pkg.bundleDependencies ||
  200. pkg.bundledDependencies ||
  201. []
  202. )
  203. const files = []
  204. const bundled = new Set()
  205. let totalEntries = 0
  206. let totalEntrySize = 0
  207. return tar.t({
  208. file: target,
  209. onentry (entry) {
  210. totalEntries++
  211. totalEntrySize += entry.size
  212. const p = entry.path
  213. if (p.startsWith('package/node_modules/')) {
  214. const name = p.match(/^package\/node_modules\/((?:@[^/]+\/)?[^/]+)/)[1]
  215. if (bundledWanted.has(name)) {
  216. bundled.add(name)
  217. }
  218. } else {
  219. files.push({
  220. path: entry.path.replace(/^package\//, ''),
  221. size: entry.size,
  222. mode: entry.mode
  223. })
  224. }
  225. },
  226. strip: 1
  227. })
  228. .then(() => BB.all([
  229. BB.fromNode((cb) => fs.stat(target, cb)),
  230. ssri.fromStream(fs.createReadStream(target), {
  231. algorithms: ['sha1', 'sha512']
  232. })
  233. ]))
  234. .then(([stat, integrity]) => {
  235. const shasum = integrity['sha1'][0].hexDigest()
  236. return {
  237. id: pkg._id,
  238. name: pkg.name,
  239. version: pkg.version,
  240. from: pkg._from,
  241. size: stat.size,
  242. unpackedSize: totalEntrySize,
  243. shasum,
  244. integrity: ssri.parse(integrity['sha512'][0]),
  245. filename,
  246. files,
  247. entryCount: totalEntries,
  248. bundled: Array.from(bundled)
  249. }
  250. })
  251. }
  252. const PASSTHROUGH_OPTS = [
  253. 'always-auth',
  254. 'auth-type',
  255. 'ca',
  256. 'cafile',
  257. 'cert',
  258. 'git',
  259. 'local-address',
  260. 'maxsockets',
  261. 'offline',
  262. 'prefer-offline',
  263. 'prefer-online',
  264. 'proxy',
  265. 'https-proxy',
  266. 'registry',
  267. 'send-metrics',
  268. 'sso-poll-frequency',
  269. 'sso-type',
  270. 'strict-ssl'
  271. ]
  272. module.exports.packGitDep = packGitDep
  273. function packGitDep (manifest, dir) {
  274. const stream = new PassThrough()
  275. readJson(path.join(dir, 'package.json')).then((pkg) => {
  276. if (pkg.scripts && pkg.scripts.prepare) {
  277. log.verbose('prepareGitDep', `${manifest._spec}: installing devDeps and running prepare script.`)
  278. const cliArgs = PASSTHROUGH_OPTS.reduce((acc, opt) => {
  279. if (npm.config.get(opt, 'cli') != null) {
  280. acc.push(`--${opt}=${npm.config.get(opt)}`)
  281. }
  282. return acc
  283. }, [])
  284. const child = cp.spawn(process.env.NODE || process.execPath, [
  285. require.resolve('../bin/npm-cli.js'),
  286. 'install',
  287. '--dev',
  288. '--prod',
  289. '--ignore-prepublish',
  290. '--no-progress',
  291. '--no-save'
  292. ].concat(cliArgs), {
  293. cwd: dir,
  294. env: process.env
  295. })
  296. let errData = []
  297. let errDataLen = 0
  298. let outData = []
  299. let outDataLen = 0
  300. child.stdout.on('data', (data) => {
  301. outData.push(data)
  302. outDataLen += data.length
  303. log.gauge.pulse('preparing git package')
  304. })
  305. child.stderr.on('data', (data) => {
  306. errData.push(data)
  307. errDataLen += data.length
  308. log.gauge.pulse('preparing git package')
  309. })
  310. return BB.fromNode((cb) => {
  311. child.on('error', cb)
  312. child.on('exit', (code, signal) => {
  313. if (code > 0) {
  314. const err = new Error(`${signal}: npm exited with code ${code} while attempting to build ${manifest._requested}. Clone the repository manually and run 'npm install' in it for more information.`)
  315. err.code = code
  316. err.signal = signal
  317. cb(err)
  318. } else {
  319. cb()
  320. }
  321. })
  322. }).then(() => {
  323. if (outDataLen > 0) log.silly('prepareGitDep', '1>', Buffer.concat(outData, outDataLen).toString())
  324. if (errDataLen > 0) log.silly('prepareGitDep', '2>', Buffer.concat(errData, errDataLen).toString())
  325. }, (err) => {
  326. if (outDataLen > 0) log.error('prepareGitDep', '1>', Buffer.concat(outData, outDataLen).toString())
  327. if (errDataLen > 0) log.error('prepareGitDep', '2>', Buffer.concat(errData, errDataLen).toString())
  328. throw err
  329. })
  330. }
  331. }).then(() => {
  332. return readJson(path.join(dir, 'package.json'))
  333. }).then((pkg) => {
  334. return cacache.tmp.withTmp(npm.tmp, {
  335. tmpPrefix: 'pacote-packing'
  336. }, (tmp) => {
  337. const tmpTar = path.join(tmp, 'package.tgz')
  338. return packDirectory(manifest, dir, tmpTar).then(() => {
  339. return pipe(fs.createReadStream(tmpTar), stream)
  340. })
  341. })
  342. }).catch((err) => stream.emit('error', err))
  343. return stream
  344. }