Skip to content
On this page

update-notifier

update-notifier 通过pkg的name、currentVersion 和执行 tag 的最新版本,在终端输出包需要更新的提示信息。

第一次运行是不会有输出的,第一次运行会把包信息持久化缓存。下一次不在确认检查的间隔时间范围内才会输出检查信息。

  • 初始化配置信息(如:包名、包版本、tag、检查更新间隔时间)
  • check 检查更新间隔时间是否已过
  • fetchInfo latestVersion 获取到包的最新信息,semverDiff 对比版本,ConfigStore 缓存最新信息和更新缓存的时间
  • notify pupa 拼装提示 message 信息,boxen 美化终端方框样式输出包更新提示信息。

主入口

js
// index.js
import UpdateNotifier from './update-notifier.js';

export default function updateNotifier(options) {
  // 创建更新通知类
	const updateNotifier = new UpdateNotifier(options);
	updateNotifier.check();
  // 返回更新通知实例对象
	return updateNotifier;
}

依赖三方包说明

js
// update-notifier.js
import process from 'node:process'; // 操作和查看当前 Node.js 进程的信息
import {spawn} from 'node:child_process'; // 创建子进程执行命令
import {fileURLToPath} from 'node:url'; // 提供用于解析 URL 的方法
import path from 'node:path'; // 提供用于处理文件和目录的方法
import {format} from 'node:util'; // 提供 Node.js 内部的实用功能函数
import ConfigStore from 'configstore'; // 持久化存储
import chalk from 'chalk'; // 终端多色彩输出
import semver from 'semver'; // npm 语义化版本
import semverDiff from 'semver-diff'; // 获取两个版本的差异补丁
import latestVersion from 'latest-version'; // 获取 npm 包的最新版本
import {isNpmOrYarn} from 'is-npm'; // 检查您的代码是否作为npm或yarn脚本运行
import isInstalledGlobally from 'is-installed-globally'; // 检查软件包是否已全局安装
import isYarnGlobal from 'is-yarn-global'; // 检查软件包是否通过 yarn 全局安装
import hasYarn from 'has-yarn'; // 检查项目是否使用Yarn
import boxen from 'boxen'; // 在终端中创建方框
import {xdgConfig} from 'xdg-basedir'; // 获取XDG基本目录路径
import isCi from 'is-ci'; // 是否为持续集成服务器(CI)
import pupa from 'pupa'; // 占位符模板

通知更新类构造函数

js
// update-notifier.js

// 当前模块的目录名
const __dirname = path.dirname(fileURLToPath(import.meta.url));

// 一天的时间戳
const ONE_DAY = 1000 * 60 * 60 * 24;

export default class UpdateNotifier {
	// 公有属性
	config;
	update;

	// protected 保护属性
	_packageName;
	_shouldNotifyInNpmScript;

	// 私有属性
	#options;
	#packageVersion;
	#updateCheckInterval;
	#isDisabled;

	constructor(options = {}) {
		this.#options = options;
		options.pkg = options.pkg || {}; // 传入的包 package.json 文件内容
		options.distTag = options.distTag || 'latest'; // 使用哪个dist标签查找最新版本

		// 先取 package.json 的名称和版本,否则取 options 传入的名称和版本
		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 环境或者环境变量为 NO_UPDATE_NOTIFIER 时,禁用
		this.#isDisabled = 'NO_UPDATE_NOTIFIER' in process.env
			|| process.env.NODE_ENV === 'test'
			|| process.argv.includes('--no-update-notifier')
			|| isCi;

		// 是否允许在作为npm脚本运行时显示通知
		this._shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;

		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'}));
				});
			}
		}
	}
}

check

js
// update-notifier.js
export default class UpdateNotifier {
	check() {
		// 如果已经输出或者禁用了,则退出
		if (
			!this.config
			|| this.config.get('optOut')
			|| this.#isDisabled
		) {
			return;
		}

		// 是否有更新
		this.update = this.config.get('update');

		if (this.update) {
			// 使用最新的版本
			this.update.current = this.#packageVersion;

			// 清除缓存信息
			this.config.delete('update');
		}

		// 是否还在检查更新的时间范围内,是则退出
		if (Date.now() - this.config.get('lastUpdateCheck') < this.#updateCheckInterval) {
			return;
		}

		// 使用子进程执行 check.js 文件,传递options参数
		spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.#options)], {
			detached: true,
			stdio: 'ignore',
		}).unref();
	}
}
js
// check.js

import process from 'node:process';
import UpdateNotifier from './update-notifier.js';

// 根据更新通知类中 check spawn 执行时,传递过来的 options 参数构造更新通知实例
const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2]));

try {
	// 30秒后退出进程
	setTimeout(process.exit, 1000 * 30);

	// 实现核心,或者包的最新信息
	const update = await updateNotifier.fetchInfo();


	// 更新最后一次检查的更新时间
	updateNotifier.config.set('lastUpdateCheck', Date.now());

	// 如果当前pkg的type不是latest,则更新包信息
	if (update.type && update.type !== 'latest') {
		updateNotifier.config.set('update', update);
	}

	// 退出进程
	process.exit();
} catch (error) {
	console.error(error);
	process.exit(1);
}

fetchInfo

js
// update-notifier.js
export default class UpdateNotifier {
	async fetchInfo() {
		const {distTag} = this.#options;
		// 通过包名以及 dist tag 获取最新的包信息 
		const latest = await latestVersion(this._packageName, {version: distTag});

		return {
			latest,
			current: this.#packageVersion,
			type: semverDiff(this.#packageVersion, latest) || distTag,
			name: this._packageName,
		};
	}
}

notify

js
// update-notifier.js
export default class UpdateNotifier {
	notify(options) {
		// 当前包管理工具是否是npm或者yarn,且 _shouldNotifyInNpmScript 为 false
		const suppressForNpm = !this._shouldNotifyInNpmScript && isNpmOrYarn;

		// isTTY 用户判断是否为标准输出的终端
		// 通过四种条件判断能否去构建 notify 的 message 信息
		if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver.gt(this.update.latest, this.update.current)) {
			return this;
		}

		options = {
			isGlobal: isInstalledGlobally,
			isYarnGlobal: isYarnGlobal(),
			...options,
		};

		// 根据当前环境和 pkg 信息构建 installCommand
		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}`;
		}

		// message 的默认内容
		const defaultTemplate = 'Update available '
			+ chalk.dim('{currentVersion}')
			+ chalk.reset('')
			+ chalk.green('{latestVersion}')
			+ ' \nRun ' + chalk.cyan('{updateCommand}') + ' to update';

		// 取不到 options 使用默认内容
		const template = options.message || defaultTemplate;

		// 通过 boxen 这个库来构建各种不同的方框包裹的的提示信息
		options.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,
		);

		if (options.defer === false) {
			console.error(message);
		} else {
			process.on('exit', () => {
				console.error(message);
			});

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

		return this;
	}	
}