update-notifier 源码学习

131 阅读3分钟

前言

update-notifier 代码结构

class UpdateNotifier {
    check() {}

    async fetchInfo(){}
    
    notify(options){}
}
module.exports = (options) => {
	const updateNotifier = new UpdateNotifier(options);
	updateNotifier.check();
	return updateNotifier;
};

module.exports.UpdateNotifier = UpdateNotifier;

源码地址:update-notifier

初始化

'use strict';
const {spawn} = require('child_process');
const path = require('path');
const {format} = require('util');
// 懒加载
const importLazy = require('import-lazy')(require);
// 持久化存储
const configstore = importLazy('configstore');
// 彩色log
const chalk = importLazy('chalk');
// 是一个专门分析Semantic Version(语义化版本)的工具
// 功能包括:
//   比较两个版本号的大小
//   验证某个版本号是否合法
//   提取版本号,例如从“=v1.2.1”体取出"1.2.1"
//   分析版本号是否属于某个范围或符合一系列条件
const semver = importLazy('semver');
// 版本比较
const semverDiff = importLazy('semver-diff');
// 直接从注册表获取版本,而不是像最新的模块那样依赖大型npm模块
const latestVersion = importLazy('latest-version');
// 检查您的代码是否作为npm或yarn脚本运行
const isNpm = importLazy('is-npm');
// 检查您的软件包是否已全局安装
const isInstalledGlobally = importLazy('is-installed-globally');
// 检查是否由 yarn 全局安装而无需任何`fs`调用
const isYarnGlobal = importLazy('is-yarn-global');
// 检查项目是否正在使用Yarn
const hasYarn = importLazy('has-yarn');
// 终端边框log
const boxen = importLazy('boxen');
// 获取XDG路径
const xdgBasedir = importLazy('xdg-basedir');
// 当前环境是否为持续集成服务器
const isCi = importLazy('is-ci');
// 简易micro模板
const pupa = importLazy('pupa');
// 默认检测间隔 1天
const ONE_DAY = 1000 * 60 * 60 * 24;

构造函数

constructor(options = {}) {
  this.options = options;
  options.pkg = options.pkg || {};
  options.distTag = options.distTag || "latest";
  
  // Reduce pkg to the essential keys. with fallback to deprecated options
  // TODO: Remove deprecated options at some point far into the future
  options.pkg = {
    name: options.pkg.name || options.packageName,
    version: options.pkg.version || options.packageVersion,
  };
  // 关键的包名和版本不能缺失
  if (!options.pkg.name || !options.pkg.version) {
    throw new Error("pkg.name and pkg.version required");
  }
  
  this.packageName = options.pkg.name;
  this.packageVersion = options.pkg.version;
  // 设置间隔时间
  this.updateCheckInterval =
    typeof options.updateCheckInterval === "number"
    ? options.updateCheckInterval
  : ONE_DAY;
  // 测试环境,ci环境,命令参数设置了禁止更新的参数则禁用功能
  this.disabled =
    "NO_UPDATE_NOTIFIER" in process.env ||
    process.env.NODE_ENV === "test" ||
    process.argv.includes("--no-update-notifier") ||
    isCi();
  this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
  
  if (!this.disabled) {
    try {
      // 持久化
      const ConfigStore = configstore();
      this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
        optOut: false,
        // Init with the current time so the first check is only
        // after the set interval, so not to bother users right away
        lastUpdateCheck: Date.now(),
      });
    } catch {
      // Expecting error code EACCES or EPERM
      const message =
            chalk().yellow(format(" %s update check failed ", options.pkg.name)) +
            format(
              "\n Try running with %s or get access ",
              chalk().cyan("sudo")
            ) +
            "\n to the local update config store via \n" +
            chalk().cyan(
              format(
                " sudo chown -R $USER:$(id -gn $USER) %s ",
                xdgBasedir().config
              )
            );
      
      process.on("exit", () => {
        console.error(boxen()(message, { align: "center" }));
      });
    }
  }
}

check 检测包版本任务

check() {
  if (!this.config || this.config.get("optOut") || this.disabled) {
    return;
  }
  // 从持久化存储里获取数据
  this.update = this.config.get("update");
  // 更新为当前的
  if (this.update) {
    // Use the real latest version instead of the cached one
    this.update.current = this.packageVersion;
    
    // Clear cached information
    this.config.delete("update");
  }
  
  // Only check for updates on a set interval
  // 间隔内才执行
  if (
    Date.now() - this.config.get("lastUpdateCheck") <
    this.updateCheckInterval
  ) {
    return;
  }
  
  // Spawn a detached process, passing the options as an environment property
  // 用一个新的进程去执行
  spawn(
    process.execPath,
    [path.join(__dirname, "check.js"), JSON.stringify(this.options)],
    {
      detached: true,
      stdio: "ignore",
    }
  ).unref();
}

检测缓存,没过期不处理,如果过期了则用一个子进程去执行检测任务

fetchInfo 获取包版本等信息

async fetchInfo() {
  const { distTag } = this.options;
  // 从注册表获取最终版本
  const latest = await latestVersion()(this.packageName, {
    version: distTag,
  });
  // 构造信息返回
  return {
    latest,
    current: this.packageVersion,
    type: semverDiff()(this.packageVersion, latest) || distTag,
    name: this.packageName,
  };
}

使用 latest-version 模块读取包的最终版本

notify 通知包有更新

notify(options) {
  const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
  if (
    !process.stdout.isTTY ||
    suppressForNpm ||
    !this.update ||
    !semver().gt(this.update.latest, this.update.current)
  ) {
    return this;
  }
  
  options = {
    isGlobal: isInstalledGlobally(),
    isYarnGlobal: isYarnGlobal()(),
    ...options,
  };
  
  let installCommand;
  if (options.isYarnGlobal) {
    installCommand = `yarn global add ${this.packageName}`;
  } else if (options.isGlobal) {
    installCommand = `npm i -g ${this.packageName}`;
  } else if (hasYarn()()) {
    installCommand = `yarn add ${this.packageName}`;
  } else {
    installCommand = `npm i ${this.packageName}`;
  }
  
  const defaultTemplate =
        "Update available " +
        chalk().dim("{currentVersion}") +
        chalk().reset(" → ") +
        chalk().green("{latestVersion}") +
        " \nRun " +
        chalk().cyan("{updateCommand}") +
        " to update";
  
  const template = options.message || defaultTemplate;
  
  options.boxenOptions = options.boxenOptions || {
    padding: 1,
    margin: 1,
    align: "center",
    borderColor: "yellow",
    borderStyle: "round",
  };
  
  const message = boxen()(
    pupa()(template, {
      packageName: this.packageName,
      currentVersion: this.update.current,
      latestVersion: this.update.latest,
      updateCommand: installCommand,
    }),
    options.boxenOptions
  );
  
  if (options.defer === false) {
    console.error(message);
  } else {
    process.on("exit", () => {
      console.error(message);
    });
    
    process.on("SIGINT", () => {
      console.error("");
      process.exit();
    });
  }
  
  return this;
}

这里就是根据使用的包管理器 npmyarn 来构造通知消息要显示的更新命令,其中还区分了该包是全局安装的还是局部的

check.js

文件地址: update-notifier/check

/* eslint-disable unicorn/no-process-exit */
'use strict';
let updateNotifier = require('.');

const options = JSON.parse(process.argv[2]);

updateNotifier = new updateNotifier.UpdateNotifier(options);

(async () => {
  // Exit process when offline
  setTimeout(process.exit, 1000 * 30);
  
  const update = await updateNotifier.fetchInfo();
  
  // Only update the last update check time on success
  updateNotifier.config.set('lastUpdateCheck', Date.now());
  
  if (update.type && update.type !== 'latest') {
    updateNotifier.config.set('update', update);
  }
  
  // Call process exit explicitly to terminate the child process,
  // otherwise the child process will run forever, according to the Node.js docs
  process.exit();
})().catch(error => {
  console.error(error);
  process.exit(1);
});

这里就是子进程执行检测包是否更新的任务代码,就是一个setTimeout 然后调用fetchInfo,然后根据结果更新持久化存储