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

1. 前言

2. update-notifier

update-notifier是在给定的时间间隔内,检查package.json 中的 name 和 version 和当前 npm registry 上是否有新的更新可用,并在有更新时通知用户。它基于 ConfigStore 来保存和读取配置信息,使用了一些第三方库来获取最新版本的信息,并使用 boxen 来创建一个带有边框的通知框。

3. 源码调试

3.1 第三方库文件说明

  • process:Node.js 的 process 模块,提供了对进程的操作和控制。
  • spawnchild_process 模块的 spawn 方法,用于启动一个子进程并执行命令。
  • fileURLToPathurl 模块的 fileURLToPath 方法,用于将文件的 URL 转换为本地路径。
  • path:Node.js 的 path 模块,提供了处理文件路径的工具函数。
  • formatutil 模块的 format 方法,用于格式化字符串。
  • ConfigStore:一个用于保存配置信息的库。
  • chalk:一个用于在终端中添加颜色和样式的库。
  • semver:用于对语义化版本进行比较和操作的库。
  • semverDiff:用于计算两个版本之间的差异的库。
  • latestVersion:用于获取指定包名的最新版本号的库。
  • isNpmOrYarn:一个用于检测当前项目是使用npm还是yarn的库。
  • isInstalledGlobally:一个用于检测当前包是否全局安装的库。
  • boxen:用于在终端创建带有边框的框的库。
  • xdgConfig:一个用于获取 XDG 配置目录路径的库。
  • isInCi:用于检测当前是否在 CI 环境下运行的库。
  • pupa:用于填充模板字符串的库。

UpdateNotifier 类有一些公共属性和方法,用于检查和通知更新。其中重要的属性和方法包括:

  • config:一个 ConfigStore 实例,用于保存和读取更新通知的配置信息。
  • update:一个保存最新版本信息的对象,包括当前版本、最新版本和版本差异。
  • check():用于检查是否有新的更新可用。
  • fetchInfo():用于获取最新版本的信息。
  • notify():用于通知用户有新的更新可用。

3.2 函数说明

js 复制代码
class UpdateNotifier {
    // 初始化操作
	constructor(options = {}) {}
    // 检查
	check() {}
    // 获取版本信息
	fetchInfo() {}
    // 提示
	notify(options) {}
}

初始化

js 复制代码
	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,
		};
		// 必须传项 pkg.name(需要校验的包), pkg.version(需要校验包的版本)
		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;
		// 默认检查间隔为 1 天 小于这个间隔时不再检查
		this.#updateCheckInterval =
			typeof options.updateCheckInterval === "number"
				? options.updateCheckInterval
				: ONE_DAY;
		// 禁用提示
		// 1. 在环境变量中禁用 NO_UPDATE_NOTIFIER
		// 2. 在环境变量中禁用 NODE_ENV 为 test
		// 3. 在命令行中禁用 node example.js --no-update-notifier
		// 4. 在CI中禁用
		this.#isDisabled =
			"NO_UPDATE_NOTIFIER" in process.env ||
			process.env.NODE_ENV === "test" ||
			process.argv.includes("--no-update-notifier") ||
			isInCi;
		// 是否在npm脚本中通知
		this._shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
		// 如果没有禁用提示 继续执行
		if (!this.#isDisabled) {
			try {
                            // 持久化存储数据 存储一些默认值如 {optOut: false, lastUpdateCheck: Date.now()} 存储位置 ~/.config/update-notifier
                            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" }));
                            });
			}
		}
	}

check

js 复制代码
	check() {
		// 判断是否禁用
		// 1. 是否存在config属性
		// 2. 是否有optOut字段
		// 3. 是否禁用提示
		if (!this.config || this.config.get("optOut") || this.#isDisabled) {
			return;
		}
		// 第一次是undefined
		this.update = this.config.get("update");
		// 如果存在update 更新当前的最新版本current
		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
		// 如果当前检测时间和上一次检测的时间间隔小于 updateCheckInterval, 则不进行校验, 防止校验次数过多, 降低用户体验
		if (
			Date.now() - this.config.get("lastUpdateCheck") <
			this.#updateCheckInterval
		) {
			return;
		}

		// Spawn a detached process, passing the options as an environment property
		// 启动一个独立的子进程中执行 check.js 文件,并将 this.options 对象作为参数传递给 check.js。子进程的标准输入、输出和错误流都会被忽略,子进程与父进程的关联也会被解除。
		// 目的是更新configStore中的update数据
		spawn(
			process.execPath,
			[path.join(__dirname, "check.js"), JSON.stringify(this.#options)],
			{
				detached: true,
				stdio: "ignore",
			},
		).unref();
	}

check.js

js 复制代码
/* eslint-disable unicorn/no-process-exit */
import process from "node:process";
import UpdateNotifier from "./update-notifier.js";
// 传入this.#options的配置
const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2]));

try {
	// Exit process when offline
	// 30s退出
	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

js 复制代码
	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,
		};
	}

notify

js 复制代码
	notify(options) {
		// 如果是npm或yarn且不在npm脚本中通知,则不通知
		const suppressForNpm = !this._shouldNotifyInNpmScript && isNpmOrYarn;
		// 是否提示
		// 1. 判断TTY是否可用
		// 2. 判断是不是在npm脚本中
		// 3. 判断是否有没有update信息
		// 4. 判断是否大于最新版本
		if (
			!process.stdout.isTTY ||
			suppressForNpm ||
			!this.update ||
			!semver.gt(this.update.latest, this.update.current)
		) {
			return this;
		}
		// 是否是全局安装
		options = {
			isGlobal: isInstalledGlobally,
			...options,
		};
		// 根据判断是否是全局安装 设置不同命令
		const installCommand = options.isGlobal
			? `npm i -g ${this._packageName}`
			: `npm i ${this._packageName}`;
		// 提示信息 chalk是一个命令行颜色库
		const defaultTemplate =
			"Update available " +
			chalk.dim("{currentVersion}") +
			chalk.reset(" → ") +
			chalk.green("{latestVersion}") +
			" \nRun " +
			chalk.cyan("{updateCommand}") +
			" to update";
		// 如果有传入message则使用传入的, 否则使用默认
		const template = options.message || defaultTemplate;
		// 如果没有传入boxenOptions则使用默认  这个库来构建各种不同的方框包裹的的提示信息
		options.boxenOptions ??= {
			padding: 1,
			margin: 1,
			textAlignment: "center",
			borderColor: "yellow",
			borderStyle: "round",
		};
		// 渲染提示信息 pupa将数据渲染到模板中的占位符
		const message = boxen(
			pupa(template, {
				packageName: this._packageName,
				currentVersion: this.update.current,
				latestVersion: this.update.latest,
				updateCommand: installCommand,
			}),
			options.boxenOptions,
		);
		// 如果options.defer属性的值为false,则在控制台输出错误信息。否则,注册一个"exit"事件监听器,该监听器在进程退出时输出错误信息;另外,还注册一个"SIGINT"事件监听器,该监听器在接收到SIGINT信号(通常由用户按下Ctrl+C触发)时输出错误信息,并退出进程。
		if (options.defer === false) {
			console.error(message);
		} else {
			process.on("exit", () => {
				console.error(message);
			});

			process.on("SIGINT", () => {
				console.error("");
				process.exit();
			});
		}

		return this;
	}

第一次初始化: 本地存储数据: 执行check: 执行后本地存储: 结果:

4. 总结

总结一下流程:

如有错误,请指正!O^O!

相关推荐
子春一28 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶14 分钟前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn1 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied2 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
羽沢313 小时前
ECharts 学习
前端·学习·echarts