index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. 'use strict';
  2. const spawn = require('child_process').spawn;
  3. const path = require('path');
  4. const format = require('util').format;
  5. const importLazy = require('import-lazy')(require);
  6. const configstore = importLazy('configstore');
  7. const chalk = importLazy('chalk');
  8. const semverDiff = importLazy('semver-diff');
  9. const latestVersion = importLazy('latest-version');
  10. const isNpm = importLazy('is-npm');
  11. const isInstalledGlobally = importLazy('is-installed-globally');
  12. const boxen = importLazy('boxen');
  13. const xdgBasedir = importLazy('xdg-basedir');
  14. const isCi = importLazy('is-ci');
  15. const ONE_DAY = 1000 * 60 * 60 * 24;
  16. class UpdateNotifier {
  17. constructor(options) {
  18. options = options || {};
  19. this.options = options;
  20. options.pkg = options.pkg || {};
  21. // Reduce pkg to the essential keys. with fallback to deprecated options
  22. // TODO: Remove deprecated options at some point far into the future
  23. options.pkg = {
  24. name: options.pkg.name || options.packageName,
  25. version: options.pkg.version || options.packageVersion
  26. };
  27. if (!options.pkg.name || !options.pkg.version) {
  28. throw new Error('pkg.name and pkg.version required');
  29. }
  30. this.packageName = options.pkg.name;
  31. this.packageVersion = options.pkg.version;
  32. this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
  33. this.hasCallback = typeof options.callback === 'function';
  34. this.callback = options.callback || (() => {});
  35. this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
  36. process.argv.indexOf('--no-update-notifier') !== -1 ||
  37. isCi();
  38. this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
  39. if (!this.disabled && !this.hasCallback) {
  40. try {
  41. const ConfigStore = configstore();
  42. this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
  43. optOut: false,
  44. // Init with the current time so the first check is only
  45. // after the set interval, so not to bother users right away
  46. lastUpdateCheck: Date.now()
  47. });
  48. } catch (err) {
  49. // Expecting error code EACCES or EPERM
  50. const msg =
  51. chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
  52. format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
  53. '\n to the local update config store via \n' +
  54. chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
  55. process.on('exit', () => {
  56. console.error('\n' + boxen()(msg, {align: 'center'}));
  57. });
  58. }
  59. }
  60. }
  61. check() {
  62. if (this.hasCallback) {
  63. this.checkNpm()
  64. .then(update => this.callback(null, update))
  65. .catch(err => this.callback(err));
  66. return;
  67. }
  68. if (
  69. !this.config ||
  70. this.config.get('optOut') ||
  71. this.disabled
  72. ) {
  73. return;
  74. }
  75. this.update = this.config.get('update');
  76. if (this.update) {
  77. this.config.delete('update');
  78. }
  79. // Only check for updates on a set interval
  80. if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
  81. return;
  82. }
  83. // Spawn a detached process, passing the options as an environment property
  84. spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
  85. detached: true,
  86. stdio: 'ignore'
  87. }).unref();
  88. }
  89. checkNpm() {
  90. return latestVersion()(this.packageName).then(latestVersion => {
  91. return {
  92. latest: latestVersion,
  93. current: this.packageVersion,
  94. type: semverDiff()(this.packageVersion, latestVersion) || 'latest',
  95. name: this.packageName
  96. };
  97. });
  98. }
  99. notify(opts) {
  100. const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm();
  101. if (!process.stdout.isTTY || suppressForNpm || !this.update) {
  102. return this;
  103. }
  104. opts = Object.assign({isGlobal: isInstalledGlobally()}, opts);
  105. opts.message = opts.message || 'Update available ' + chalk().dim(this.update.current) + chalk().reset(' → ') +
  106. chalk().green(this.update.latest) + ' \nRun ' + chalk().cyan('npm i ' + (opts.isGlobal ? '-g ' : '') + this.packageName) + ' to update';
  107. opts.boxenOpts = opts.boxenOpts || {
  108. padding: 1,
  109. margin: 1,
  110. align: 'center',
  111. borderColor: 'yellow',
  112. borderStyle: 'round'
  113. };
  114. const message = '\n' + boxen()(opts.message, opts.boxenOpts);
  115. if (opts.defer === false) {
  116. console.error(message);
  117. } else {
  118. process.on('exit', () => {
  119. console.error(message);
  120. });
  121. process.on('SIGINT', () => {
  122. console.error('');
  123. process.exit();
  124. });
  125. }
  126. return this;
  127. }
  128. }
  129. module.exports = options => {
  130. const updateNotifier = new UpdateNotifier(options);
  131. updateNotifier.check();
  132. return updateNotifier;
  133. };
  134. module.exports.UpdateNotifier = UpdateNotifier;