index.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. let childProcess = require('child_process')
  2. let escalade = require('escalade/sync')
  3. let pico = require('picocolors')
  4. let path = require('path')
  5. let fs = require('fs')
  6. function BrowserslistUpdateError(message) {
  7. this.name = 'BrowserslistUpdateError'
  8. this.message = message
  9. this.browserslist = true
  10. if (Error.captureStackTrace) {
  11. Error.captureStackTrace(this, BrowserslistUpdateError)
  12. }
  13. }
  14. BrowserslistUpdateError.prototype = Error.prototype
  15. /* c8 ignore next 3 */
  16. function defaultPrint(str) {
  17. process.stdout.write(str)
  18. }
  19. function detectLockfile() {
  20. let packageDir = escalade('.', (dir, names) => {
  21. return names.indexOf('package.json') !== -1 ? dir : ''
  22. })
  23. if (!packageDir) {
  24. throw new BrowserslistUpdateError(
  25. 'Cannot find package.json. ' +
  26. 'Is this the right directory to run `npx update-browserslist-db` in?'
  27. )
  28. }
  29. let lockfileNpm = path.join(packageDir, 'package-lock.json')
  30. let lockfileShrinkwrap = path.join(packageDir, 'npm-shrinkwrap.json')
  31. let lockfileYarn = path.join(packageDir, 'yarn.lock')
  32. let lockfilePnpm = path.join(packageDir, 'pnpm-lock.yaml')
  33. if (fs.existsSync(lockfilePnpm)) {
  34. return { mode: 'pnpm', file: lockfilePnpm }
  35. } else if (fs.existsSync(lockfileNpm)) {
  36. return { mode: 'npm', file: lockfileNpm }
  37. } else if (fs.existsSync(lockfileYarn)) {
  38. let lock = { mode: 'yarn', file: lockfileYarn }
  39. lock.content = fs.readFileSync(lock.file).toString()
  40. lock.version = /# yarn lockfile v1/.test(lock.content) ? 1 : 2
  41. return lock
  42. } else if (fs.existsSync(lockfileShrinkwrap)) {
  43. return { mode: 'npm', file: lockfileShrinkwrap }
  44. }
  45. throw new BrowserslistUpdateError(
  46. 'No lockfile found. Run "npm install", "yarn install" or "pnpm install"'
  47. )
  48. }
  49. function getLatestInfo(lock) {
  50. if (lock.mode === 'yarn') {
  51. if (lock.version === 1) {
  52. return JSON.parse(
  53. childProcess.execSync('yarn info caniuse-lite --json').toString()
  54. ).data
  55. } else {
  56. return JSON.parse(
  57. childProcess.execSync('yarn npm info caniuse-lite --json').toString()
  58. )
  59. }
  60. }
  61. return JSON.parse(
  62. childProcess.execSync('npm show caniuse-lite --json').toString()
  63. )
  64. }
  65. function getBrowsers() {
  66. let browserslist = require('browserslist')
  67. return browserslist().reduce((result, entry) => {
  68. if (!result[entry[0]]) {
  69. result[entry[0]] = []
  70. }
  71. result[entry[0]].push(entry[1])
  72. return result
  73. }, {})
  74. }
  75. function diffBrowsers(old, current) {
  76. let browsers = Object.keys(old).concat(
  77. Object.keys(current).filter(browser => old[browser] === undefined)
  78. )
  79. return browsers
  80. .map(browser => {
  81. let oldVersions = old[browser] || []
  82. let currentVersions = current[browser] || []
  83. let common = oldVersions.filter(v => currentVersions.includes(v))
  84. let added = currentVersions.filter(v => !common.includes(v))
  85. let removed = oldVersions.filter(v => !common.includes(v))
  86. return removed
  87. .map(v => pico.red('- ' + browser + ' ' + v))
  88. .concat(added.map(v => pico.green('+ ' + browser + ' ' + v)))
  89. })
  90. .reduce((result, array) => result.concat(array), [])
  91. .join('\n')
  92. }
  93. function updateNpmLockfile(lock, latest) {
  94. let metadata = { latest, versions: [] }
  95. let content = deletePackage(JSON.parse(lock.content), metadata)
  96. metadata.content = JSON.stringify(content, null, ' ')
  97. return metadata
  98. }
  99. function deletePackage(node, metadata) {
  100. if (node.dependencies) {
  101. if (node.dependencies['caniuse-lite']) {
  102. let version = node.dependencies['caniuse-lite'].version
  103. metadata.versions[version] = true
  104. delete node.dependencies['caniuse-lite']
  105. }
  106. for (let i in node.dependencies) {
  107. node.dependencies[i] = deletePackage(node.dependencies[i], metadata)
  108. }
  109. }
  110. return node
  111. }
  112. let yarnVersionRe = /version "(.*?)"/
  113. function updateYarnLockfile(lock, latest) {
  114. let blocks = lock.content.split(/(\n{2,})/).map(block => {
  115. return block.split('\n')
  116. })
  117. let versions = {}
  118. blocks.forEach(lines => {
  119. if (lines[0].indexOf('caniuse-lite@') !== -1) {
  120. let match = yarnVersionRe.exec(lines[1])
  121. versions[match[1]] = true
  122. if (match[1] !== latest.version) {
  123. lines[1] = lines[1].replace(
  124. /version "[^"]+"/,
  125. 'version "' + latest.version + '"'
  126. )
  127. lines[2] = lines[2].replace(
  128. /resolved "[^"]+"/,
  129. 'resolved "' + latest.dist.tarball + '"'
  130. )
  131. if (lines.length === 4) {
  132. lines[3] = latest.dist.integrity
  133. ? lines[3].replace(
  134. /integrity .+/,
  135. 'integrity ' + latest.dist.integrity
  136. )
  137. : ''
  138. }
  139. }
  140. }
  141. })
  142. let content = blocks.map(lines => lines.join('\n')).join('')
  143. return { content, versions }
  144. }
  145. function updateLockfile(lock, latest) {
  146. if (!lock.content) lock.content = fs.readFileSync(lock.file).toString()
  147. if (lock.mode === 'yarn') {
  148. return updateYarnLockfile(lock, latest)
  149. } else {
  150. return updateNpmLockfile(lock, latest)
  151. }
  152. }
  153. function updatePackageManually(print, lock, latest) {
  154. let lockfileData = updateLockfile(lock, latest)
  155. let caniuseVersions = Object.keys(lockfileData.versions).sort()
  156. if (caniuseVersions.length === 1 && caniuseVersions[0] === latest.version) {
  157. print(
  158. 'Installed version: ' +
  159. pico.bold(pico.green(latest.version)) +
  160. '\n' +
  161. pico.bold(pico.green('caniuse-lite is up to date')) +
  162. '\n'
  163. )
  164. return
  165. }
  166. if (caniuseVersions.length === 0) {
  167. caniuseVersions[0] = 'none'
  168. }
  169. print(
  170. 'Installed version' +
  171. (caniuseVersions.length === 1 ? ': ' : 's: ') +
  172. pico.bold(pico.red(caniuseVersions.join(', '))) +
  173. '\n' +
  174. 'Removing old caniuse-lite from lock file\n'
  175. )
  176. fs.writeFileSync(lock.file, lockfileData.content)
  177. let install = lock.mode === 'yarn' ? 'yarn add -W' : lock.mode + ' install'
  178. print(
  179. 'Installing new caniuse-lite version\n' +
  180. pico.yellow('$ ' + install + ' caniuse-lite') +
  181. '\n'
  182. )
  183. try {
  184. childProcess.execSync(install + ' caniuse-lite')
  185. } catch (e) /* c8 ignore start */ {
  186. print(
  187. pico.red(
  188. '\n' +
  189. e.stack +
  190. '\n\n' +
  191. 'Problem with `' +
  192. install +
  193. ' caniuse-lite` call. ' +
  194. 'Run it manually.\n'
  195. )
  196. )
  197. process.exit(1)
  198. } /* c8 ignore end */
  199. let del = lock.mode === 'yarn' ? 'yarn remove -W' : lock.mode + ' uninstall'
  200. print(
  201. 'Cleaning package.json dependencies from caniuse-lite\n' +
  202. pico.yellow('$ ' + del + ' caniuse-lite') +
  203. '\n'
  204. )
  205. childProcess.execSync(del + ' caniuse-lite')
  206. }
  207. function updateWith(print, cmd) {
  208. print('Updating caniuse-lite version\n' + pico.yellow('$ ' + cmd) + '\n')
  209. try {
  210. childProcess.execSync(cmd)
  211. } catch (e) /* c8 ignore start */ {
  212. print(pico.red(e.stdout.toString()))
  213. print(
  214. pico.red(
  215. '\n' +
  216. e.stack +
  217. '\n\n' +
  218. 'Problem with `' +
  219. cmd +
  220. '` call. ' +
  221. 'Run it manually.\n'
  222. )
  223. )
  224. process.exit(1)
  225. } /* c8 ignore end */
  226. }
  227. module.exports = function updateDB(print = defaultPrint) {
  228. let lock = detectLockfile()
  229. let latest = getLatestInfo(lock)
  230. let listError
  231. let oldList
  232. try {
  233. oldList = getBrowsers()
  234. } catch (e) {
  235. listError = e
  236. }
  237. print('Latest version: ' + pico.bold(pico.green(latest.version)) + '\n')
  238. if (lock.mode === 'yarn' && lock.version !== 1) {
  239. updateWith(print, 'yarn up -R caniuse-lite')
  240. } else if (lock.mode === 'pnpm') {
  241. updateWith(print, 'pnpm up caniuse-lite')
  242. } else {
  243. updatePackageManually(print, lock, latest)
  244. }
  245. print('caniuse-lite has been successfully updated\n')
  246. let newList
  247. if (!listError) {
  248. try {
  249. newList = getBrowsers()
  250. } catch (e) /* c8 ignore start */ {
  251. listError = e
  252. } /* c8 ignore end */
  253. }
  254. if (listError) {
  255. print(
  256. pico.red(
  257. '\n' +
  258. listError.stack +
  259. '\n\n' +
  260. 'Problem with browser list retrieval.\n' +
  261. 'Target browser changes won’t be shown.\n'
  262. )
  263. )
  264. } else {
  265. let changes = diffBrowsers(oldList, newList)
  266. if (changes) {
  267. print('\nTarget browser changes:\n')
  268. print(changes + '\n')
  269. } else {
  270. print('\n' + pico.green('No target browser changes') + '\n')
  271. }
  272. }
  273. }