前言
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第6期,链接:第6期 | update-notifier 检测 npm 包是否更新
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;
}
这里就是根据使用的包管理器 npm 或 yarn 来构造通知消息要显示的更新命令,其中还区分了该包是全局安装的还是局部的
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,然后根据结果更新持久化存储