index.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. 'use strict';
  2. const {spawn} = require('child_process');
  3. const path = require('path');
  4. const {format} = require('util');
  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 isYarnGlobal = importLazy('is-yarn-global');
  13. const hasYarn = importLazy('has-yarn');
  14. const boxen = importLazy('boxen');
  15. const xdgBasedir = importLazy('xdg-basedir');
  16. const isCi = importLazy('is-ci');
  17. const pupa = importLazy('pupa');
  18. const ONE_DAY = 1000 * 60 * 60 * 24;
  19. class UpdateNotifier {
  20. constructor(options = {}) {
  21. this.options = options;
  22. options.pkg = options.pkg || {};
  23. options.distTag = options.distTag || 'latest';
  24. // Reduce pkg to the essential keys. with fallback to deprecated options
  25. // TODO: Remove deprecated options at some point far into the future
  26. options.pkg = {
  27. name: options.pkg.name || options.packageName,
  28. version: options.pkg.version || options.packageVersion
  29. };
  30. if (!options.pkg.name || !options.pkg.version) {
  31. throw new Error('pkg.name and pkg.version required');
  32. }
  33. this.packageName = options.pkg.name;
  34. this.packageVersion = options.pkg.version;
  35. this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
  36. this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
  37. process.env.NODE_ENV === 'test' ||
  38. process.argv.includes('--no-update-notifier') ||
  39. isCi();
  40. this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
  41. if (!this.disabled) {
  42. try {
  43. const ConfigStore = configstore();
  44. this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
  45. optOut: false,
  46. // Init with the current time so the first check is only
  47. // after the set interval, so not to bother users right away
  48. lastUpdateCheck: Date.now()
  49. });
  50. } catch (_) {
  51. // Expecting error code EACCES or EPERM
  52. const message =
  53. chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
  54. format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
  55. '\n to the local update config store via \n' +
  56. chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
  57. process.on('exit', () => {
  58. console.error(boxen()(message, {align: 'center'}));
  59. });
  60. }
  61. }
  62. }
  63. check() {
  64. if (
  65. !this.config ||
  66. this.config.get('optOut') ||
  67. this.disabled
  68. ) {
  69. return;
  70. }
  71. this.update = this.config.get('update');
  72. if (this.update) {
  73. // Use the real latest version instead of the cached one
  74. this.update.current = this.packageVersion;
  75. // Clear cached information
  76. this.config.delete('update');
  77. }
  78. // Only check for updates on a set interval
  79. if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
  80. return;
  81. }
  82. // Spawn a detached process, passing the options as an environment property
  83. spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
  84. detached: true,
  85. stdio: 'ignore'
  86. }).unref();
  87. }
  88. async fetchInfo() {
  89. const {distTag} = this.options;
  90. const latest = await latestVersion()(this.packageName, {version: distTag});
  91. return {
  92. latest,
  93. current: this.packageVersion,
  94. type: semverDiff()(this.packageVersion, latest) || distTag,
  95. name: this.packageName
  96. };
  97. }
  98. notify(options) {
  99. const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
  100. if (!process.stdout.isTTY || suppressForNpm || !this.update || this.update.current === this.update.latest) {
  101. return this;
  102. }
  103. options = Object.assign({
  104. isGlobal: isInstalledGlobally(),
  105. isYarnGlobal: isYarnGlobal()()
  106. }, options);
  107. let installCommand;
  108. if (options.isYarnGlobal) {
  109. installCommand = `yarn global add ${this.packageName}`;
  110. } else if (options.isGlobal) {
  111. installCommand = `npm i -g ${this.packageName}`;
  112. } else if (hasYarn()()) {
  113. installCommand = `yarn add ${this.packageName}`;
  114. } else {
  115. installCommand = `npm i ${this.packageName}`;
  116. }
  117. const defaultTemplate = 'Update available ' +
  118. chalk().dim('{currentVersion}') +
  119. chalk().reset(' → ') +
  120. chalk().green('{latestVersion}') +
  121. ' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';
  122. const template = options.message || defaultTemplate;
  123. options.boxenOptions = options.boxenOptions || {
  124. padding: 1,
  125. margin: 1,
  126. align: 'center',
  127. borderColor: 'yellow',
  128. borderStyle: 'round'
  129. };
  130. const message = boxen()(
  131. pupa()(template, {
  132. packageName: this.packageName,
  133. currentVersion: this.update.current,
  134. latestVersion: this.update.latest,
  135. updateCommand: installCommand
  136. }),
  137. options.boxenOptions
  138. );
  139. if (options.defer === false) {
  140. console.error(message);
  141. } else {
  142. process.on('exit', () => {
  143. console.error(message);
  144. });
  145. process.on('SIGINT', () => {
  146. console.error('');
  147. process.exit();
  148. });
  149. }
  150. return this;
  151. }
  152. }
  153. module.exports = options => {
  154. const updateNotifier = new UpdateNotifier(options);
  155. updateNotifier.check();
  156. return updateNotifier;
  157. };
  158. module.exports.UpdateNotifier = UpdateNotifier;