给你的npm包增加更新检测 - update-notifier源码学习

本文参加了由 公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是源码共读的第6期 | update-notifier 检测 npm 包是否更新

前言

我们在开发一个轮子或者CLI工具时,大部分情况都不会去考虑后续用户使用时的版本检测操作,update-notifier非常精简的实现了版本的自动检测和更新提示,接下来会细细道来其中的实现原理~

使用

使用npm init -y 创建一个库,接着将库的名字改为public-ip用于测试。 然后将以下代码执行两次就可以看到回显

js 复制代码
import updateNotifier from "update-notifier";
//import packageJson from './package.json' assert {type: 'json'};
//手动读取 packageJson
import fs from "node:fs";

const pkgFile = fs.readFileSync("./package.json", { encoding: "utf-8" });
const pkgJson = JSON.parse(pkgFile.toString());
new updateNotifier({ pkg: pkgJson, updateCheckInterval: 0 }).notify();

入口

老规矩,在package.json中 我们可以明确其执行路径为: "exports": "./index.js"

js 复制代码
import UpdateNotifier from "./update-notifier.js";

export default function updateNotifier(options) {
	const updateNotifier = new UpdateNotifier(options);
	updateNotifier.check();
	return updateNotifier;
}

初始化

js 复制代码
constructor(options = {}) {
    //省略若干初始化参数代码
    
    this.#updateCheckInterval =
    typeof options.updateCheckInterval === "number"
            ? options.updateCheckInterval
            : ONE_DAY;
if (!this.#isDisabled) {
        try {
                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 ", xdgConfig)
                        );

                process.on("exit", () => {
                        console.error(boxen(message, { textAlignment: "center" }));
                });
        }
}
}

这里可以看到熟悉的库ConfigStore,上一期也讲过这个库,其作用就是数据本地文件持久化。 这一段代码会将按照我们传递要检测的包作为文件名,然后将执行时的时间作为最后一次检测时间存储进去。

  1. 如果是第一次使用这个库执行检测更新,那么不会有任何结果,仅仅是创建了这个文件并存储
  2. 另外如果你没有指定updateCheckInterval参数,那么它默认只有在一天之后再会去做比较执行
  3. 所以你可以通过改变本地时间来达到提前检测的目的(嘿嘿)

接着回到入口处它会接着执行updateNotifier.check();

check

js 复制代码
check() {
    if (!this.config || this.config.get("optOut") || this.#isDisabled) {
            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 子进程执行命令 process.execPath 得到node执行路径 即node命令 然后执行文件是 当前目录下的 check.js 执行参数
    // 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(); //unref 父级的事件循环不将子级包括在其引用计数中
}
js 复制代码
//取出被转为Json字符串的  对象参数 并转回对象作为参数使用
const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2]));

try {
	// 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);
}

跟着会调用fetchInfo方法,会去获取最新的一个版本号作比较并返回构造的对象信息

semverDiff、semver 库都是基于semver版本号规范的轮子,用于版本号的比较等

js 复制代码
async fetchInfo() {
    const { distTag } = this.#options;
    const latest = await latestVersion(this._packageName, { version: distTag });

    return {
            latest,
            current: this.#packageVersion,
            //更新的版本类型 是 major、patch 还是什么默认的latest
            type: semverDiff(this.#packageVersion, latest) || distTag,
            name: this._packageName,
    };
}

当得到返回信息后,会将检测时间进行更新,接着根据返回的版本类型(latest即最新的)是不是最新的 而决定更新update这个字段

notify

而notify方法则非常简单,就是调用控制台输出的库,根据当前使用的包管理工具决定输出的信息,最终只看这个判断的执行然后就可以看到控制台的输出了

js 复制代码
if (
    !process.stdout.isTTY ||
    suppressForNpm ||
    !this.update ||
    !semver.gt(this.update.latest, this.update.current)
) {
    return this;
}

check函数中已经更新了 this.update.latest 的值,而在初始化中更新了this.update.current的值,两者通过比较,以及其他条件的判断进行输出

一些其他补充

is-ci 默认的导出依赖了 ci-info这个包, 其原理就是通过预先定义好的各种CI 环境信息去做process.env的匹配,当前是否处于 CI服务器环境下

process.stdout.isTTY 用于判断命令执行是否在终端环境 suppressForNpm 是否为Npm Yarn process.execPath 执行node 的环境变量

devdocs.io/node~16_lts...

child_process.spawn(command[, args][, options]) 执行的命令 传递参数 命令参数

总结

梳理下最终可以得到这样一个流程:

  1. 先执行初始化,创建本地文件更新第一次存储的时间
  2. 每次check函数中 会先比较一次 本地时间和持久化文件中的时间,条件符合则更新一次存储时间,并请求最新的版本返回用于比较
  3. 根据前面得到的版本信息进行比较,再根据当前环境决定提示信息的拼装,最后控制台输出更新提示信息
相关推荐
代码搬运媛6 小时前
Jest 测试框架详解与实现指南
前端
counterxing6 小时前
Agent 跑起来之后,难的是复用、观测和评测
node.js·agent·ai编程
counterxing6 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq7 小时前
windows下nginx的安装
linux·服务器·前端
之歆7 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜7 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108087 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong7 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
kyriewen9 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm10 小时前
元框架的工作原理详解
前端·前端框架