1、作用
以非入侵的方式通知cli应用程序更新包。
2、原因
-
美化终端输出信息,描绘边框。
-
我们可以学会怎么去开启子进程
-
给一个node方法作为传参的几种方式
-
通过configstore这个库我们能够持久化存储一些信息
-
通过latest-version获取对应pkg包的最新版本信息
3、源码
'use strict';
const updateNotifier = require('.');
// Run: $ node example
// 第一次必须运行此文件两次
// 因为它不会在第一次运行时报告更新
//如果你想使用,必须确保你使用的是老版本
updateNotifier({
pkg: {
name: 'public-ip',
version: '0.9.2'
},
updateCheckInterval: 0
}).notify();
"use strict";
//spawn方法创建一个子进程来执行特定命令,用法与execFile方法类似,
//但是没有回调函数,只能通过监听事件,来获取运行结果。
//它属于异步执行,适用于子进程长时间运行的情况。
const { spawn } = require("child_process");
const path = require("path");
const { format } = require("util");
const importLazy = require("import-lazy")(require);
const configstore = importLazy("configstore");
const chalk = importLazy("chalk");
//语义化版本
const semver = importLazy("semver");
//semver-diff:获取两个semver版本的diff类型
const semverDiff = importLazy("semver-diff");
const latestVersion = importLazy("latest-version");
const isNpm = importLazy("is-npm");
//检查您的软件包是否已全局安装
const isInstalledGlobally = importLazy("is-installed-globally");
//检查您的yarn包是否已全局安装
const isYarnGlobal = importLazy("is-yarn-global");
const hasYarn = importLazy("has-yarn");
//boxen:在终端中创建框
const boxen = importLazy("boxen");
const xdgBasedir = importLazy("xdg-basedir");
const isCi = importLazy("is-ci");
const pupa = importLazy("pupa");
const ONE_DAY = 1000 * 60 * 60 * 24;
class UpdateNotifier {
//实例化对象
constructor(options = {}) {
this.options = options;
options.pkg = options.pkg || {};
options.distTag = options.distTag || "latest";
//减少pkg到基本的关键:退回到已弃用选项
// TODO:将来的时间点移除已弃用的选项
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;
// 检查周期 单位是ms
this.updateCheckInterval =
typeof options.updateCheckInterval === "number"
? options.updateCheckInterval
: ONE_DAY;
this.disabled =
//获取process检查是否禁用更新
"NO_UPDATE_NOTIFIER" in process.env ||
process.env.NODE_ENV === "test" ||
process.argv.includes("--no-update-notifier") ||
isCi(); //isCi的作用是如果当前环境是持续集成服务器,则返回true
//判断是否在Npm脚本中通知
this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
if (!this.disabled) {
try {
//ConfigStore的作用是轻松加载和保留配置,而无需考虑在哪里以及如何
const ConfigStore = configstore();
this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
optOut: false,
// 初始化当前时间,因此第一次仅检查
//在设置间隔之后,所以不要马上打扰用户
lastUpdateCheck: Date.now(),
});
} catch {
//期望出现错误代码 EACCES 或 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" }));
});
}
}
}
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(options) {
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (
//判断它是否在终端(terminal)终端环境中执行
!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;
}
}
module.exports = (options) => {
//检查更新
updateNotifier.check();
return updateNotifier;
};
module.exports.UpdateNotifier = UpdateNotifier;
1、实例化对象
//实例化对象 包名通过ConfigStore进行持久化存储,顺便把最后一次检查的时间更新成现在
const updateNotifier = new UpdateNotifier(options);
constructor(options = {}) {
this.options = options;
options.pkg = options.pkg || {};
options.distTag = options.distTag || "latest";
//减少pkg到基本的关键:退回到已弃用选项
// TODO:将来的时间点移除已弃用的选项
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;
// 检查周期 单位是ms
this.updateCheckInterval =
typeof options.updateCheckInterval === "number"
? options.updateCheckInterval
: ONE_DAY;
this.disabled =
//获取process检查是否禁用更新
"NO_UPDATE_NOTIFIER" in process.env ||
process.env.NODE_ENV === "test" ||
process.argv.includes("--no-update-notifier") ||
isCi(); //isCi的作用是如果当前环境是持续集成服务器,则返回true
this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
if (!this.disabled) {
try {
//ConfigStore的作用是轻松加载和保留配置,而无需考虑在哪里以及如何
const ConfigStore = configstore();
this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
optOut: false,
// 初始化当前时间,因此第一次仅检查
//在设置间隔之后,所以不要马上打扰用户
lastUpdateCheck: Date.now(),
});
} catch {
//期望出现错误代码 EACCES 或 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" }));
});
}
}
}
实例化结果:
2、第一次检查更新
check() {
//没有本地存储或者禁用更新的情况下,那么就不执行了
if (!this.config || this.config.get("optOut") || this.disabled) {
return;
}
//将之前存进去的update对象来更新,第一次获得的update是空
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开启一个子进程,运行check.js,然后把options作为调用check.js的命令行参数
spawn(
process.execPath,
[path.join(__dirname, "check.js"), JSON.stringify(this.options)],
{
detached: true,
stdio: "ignore",
}
).unref();
}
3、通知
notify(options) {
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (
//判断它是否在终端(terminal)终端环境中执行
!process.stdout.isTTY ||
suppressForNpm ||
//第一次因为update为空,直接返回了
!this.update ||
//语义化对比
!semver().gt(this.update.latest, this.update.current)
) {
return this;
}
.....
return this;
}
}
4、第二次执行,进入check.js文件
updateNotifier = new updateNotifier.UpdateNotifier(options);
(async () => {
// 30s后退出程序
setTimeout(process.exit, 1000 * 30);
//获得更新的内容
const update = await updateNotifier.fetchInfo();
// 设置最后更新时间
updateNotifier.config.set("lastUpdateCheck", Date.now());
//版本差别 设置更新
if (update.type && update.type !== "latest") {
updateNotifier.config.set("update", update);
}
//终止子进程
// 否则,根据 Node.js 文档,子进程将永远运行
process.exit();
})().catch((error) => {
console.error(error);
process.exit(1);
});
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,
};
}
5、第二次通知
notify(options) {
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (
//判断它是否在终端(terminal)终端环境中执行
!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}`;
}
//设置cmd 提示内容
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;
}
6、最后输出
"use strict";
//spawn方法创建一个子进程来执行特定命令,用法与execFile方法类似,
//但是没有回调函数,只能通过监听事件,来获取运行结果。
//它属于异步执行,适用于子进程长时间运行的情况。
const { spawn } = require("child_process");
const path = require("path");
const { format } = require("util");
const importLazy = require("import-lazy")(require);
const configstore = importLazy("configstore");
const chalk = importLazy("chalk");
//语义化版本
const semver = importLazy("semver");
//semver-diff:获取两个semver版本的diff类型
const semverDiff = importLazy("semver-diff");
//获取最新版本的 npm 包
const latestVersion = importLazy("latest-version");
const isNpm = importLazy("is-npm");
//检查您的软件包是否已全局安装
const isInstalledGlobally = importLazy("is-installed-globally");
//检查您的yarn包是否已全局安装
const isYarnGlobal = importLazy("is-yarn-global");
const hasYarn = importLazy("has-yarn");
//boxen:在终端中创建框
const boxen = importLazy("boxen");
const xdgBasedir = importLazy("xdg-basedir");
const isCi = importLazy("is-ci");
const pupa = importLazy("pupa");
const ONE_DAY = 1000 * 60 * 60 * 24;
class UpdateNotifier {
constructor(options = {}) {
this.options = options;
options.pkg = options.pkg || {};
options.distTag = options.distTag || "latest";
//减少pkg到基本的关键:退回到已弃用选项
// TODO:将来的时间点移除已弃用的选项
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;
// 检查周期 单位是ms
this.updateCheckInterval =
typeof options.updateCheckInterval === "number"
? options.updateCheckInterval
: ONE_DAY;
this.disabled =
//获取process检查是否禁用更新
"NO_UPDATE_NOTIFIER" in process.env ||
process.env.NODE_ENV === "test" ||
process.argv.includes("--no-update-notifier") ||
isCi(); //isCi的作用是如果当前环境是持续集成服务器,则返回true
this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
if (!this.disabled) {
try {
//ConfigStore的作用是轻松加载和保留配置,而无需考虑在哪里以及如何
const ConfigStore = configstore();
this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
optOut: false,
// 初始化当前时间,因此第一次仅检查
//在设置间隔之后,所以不要马上打扰用户
lastUpdateCheck: Date.now(),
});
} catch {
//期望出现错误代码 EACCES 或 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() {
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(
process.execPath,
[path.join(__dirname, "check.js"), JSON.stringify(this.options)],
{
detached: true,
stdio: "ignore",
}
).unref();
}
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(options) {
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (
//判断它是否在终端(terminal)终端环境中执行
!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;
}
}
module.exports = (options) => {
const updateNotifier = new UpdateNotifier(options);
updateNotifier.check();
return updateNotifier;
};
module.exports.UpdateNotifier = UpdateNotifier;