1const tar = require('tar') 2const ssri = require('ssri') 3const log = require('./log-shim') 4const formatBytes = require('./format-bytes.js') 5const columnify = require('columnify') 6const localeCompare = require('@isaacs/string-locale-compare')('en', { 7 sensitivity: 'case', 8 numeric: true, 9}) 10 11const logTar = (tarball, opts = {}) => { 12 const { unicode = false } = opts 13 log.notice('') 14 log.notice('', `${unicode ? ' ' : 'package:'} ${tarball.name}@${tarball.version}`) 15 log.notice('=== Tarball Contents ===') 16 if (tarball.files.length) { 17 log.notice( 18 '', 19 columnify( 20 tarball.files 21 .map(f => { 22 const bytes = formatBytes(f.size, false) 23 return /^node_modules\//.test(f.path) ? null : { path: f.path, size: `${bytes}` } 24 }) 25 .filter(f => f), 26 { 27 include: ['size', 'path'], 28 showHeaders: false, 29 } 30 ) 31 ) 32 } 33 if (tarball.bundled.length) { 34 log.notice('=== Bundled Dependencies ===') 35 tarball.bundled.forEach(name => log.notice('', name)) 36 } 37 log.notice('=== Tarball Details ===') 38 log.notice( 39 '', 40 columnify( 41 [ 42 { name: 'name:', value: tarball.name }, 43 { name: 'version:', value: tarball.version }, 44 tarball.filename && { name: 'filename:', value: tarball.filename }, 45 { name: 'package size:', value: formatBytes(tarball.size) }, 46 { name: 'unpacked size:', value: formatBytes(tarball.unpackedSize) }, 47 { name: 'shasum:', value: tarball.shasum }, 48 { 49 name: 'integrity:', 50 value: 51 tarball.integrity.toString().slice(0, 20) + 52 '[...]' + 53 tarball.integrity.toString().slice(80), 54 }, 55 tarball.bundled.length && { name: 'bundled deps:', value: tarball.bundled.length }, 56 tarball.bundled.length && { 57 name: 'bundled files:', 58 value: tarball.entryCount - tarball.files.length, 59 }, 60 tarball.bundled.length && { name: 'own files:', value: tarball.files.length }, 61 { name: 'total files:', value: tarball.entryCount }, 62 ].filter(x => x), 63 { 64 include: ['name', 'value'], 65 showHeaders: false, 66 } 67 ) 68 ) 69 log.notice('', '') 70} 71 72const getContents = async (manifest, tarball) => { 73 const files = [] 74 const bundled = new Set() 75 let totalEntries = 0 76 let totalEntrySize = 0 77 78 // reads contents of tarball 79 const stream = tar.t({ 80 onentry (entry) { 81 totalEntries++ 82 totalEntrySize += entry.size 83 const p = entry.path 84 if (p.startsWith('package/node_modules/')) { 85 const name = p.match(/^package\/node_modules\/((?:@[^/]+\/)?[^/]+)/)[1] 86 bundled.add(name) 87 } 88 files.push({ 89 path: entry.path.replace(/^package\//, ''), 90 size: entry.size, 91 mode: entry.mode, 92 }) 93 }, 94 }) 95 stream.end(tarball) 96 97 const integrity = await ssri.fromData(tarball, { 98 algorithms: ['sha1', 'sha512'], 99 }) 100 101 const comparator = ({ path: a }, { path: b }) => localeCompare(a, b) 102 103 const isUpper = str => { 104 const ch = str.charAt(0) 105 return ch === ch.toUpperCase() 106 } 107 108 const uppers = files.filter(file => isUpper(file.path)) 109 const others = files.filter(file => !isUpper(file.path)) 110 111 uppers.sort(comparator) 112 others.sort(comparator) 113 114 const shasum = integrity.sha1[0].hexDigest() 115 return { 116 id: manifest._id || `${manifest.name}@${manifest.version}`, 117 name: manifest.name, 118 version: manifest.version, 119 size: tarball.length, 120 unpackedSize: totalEntrySize, 121 shasum, 122 integrity: ssri.parse(integrity.sha512[0]), 123 // @scope/packagename.tgz => scope-packagename.tgz 124 // we can safely use these global replace rules due to npm package naming rules 125 filename: `${manifest.name.replace('@', '').replace('/', '-')}-${manifest.version}.tgz`, 126 files: uppers.concat(others), 127 entryCount: totalEntries, 128 bundled: Array.from(bundled), 129 } 130} 131 132module.exports = { logTar, getContents } 133