index.js 5.2 KB

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