update-notifier 源码解析
初步使用
看之前先了解下这个库的用法
javascript
'use strict'
const updateNotifier = require('.')
// Run: $ node example
// You have to run this file two times the first time
// This is because it never reports updates on the first run
// If you want to test your own usage, ensure you set an older version
updateNotifier({
pkg: {
name: 'public-ip',
version: '0.9.2'
},
updateCheckInterval: 0
}).notify()
执行这个 js 之后第二次就会显示最新包,显示出让你更新的提示,看起来用法还是很简单的,updateCheckInterval 是用来检查是否有新包的间隔时间。总的看来就是传哥参数调用下方法
一探究竟
先看下 package.json 文件,对项目整体和依赖的包有个初步的了解
开发依赖
可以看出生产依赖的包还是很多的,一会在逐个介绍包的作用吧。还有几个开发依赖包,简单去了解下
ava: 用来测试 node 的,你看这里的单元测试用的就是它
clear-module:清除本地的包缓存
其实 npm 包第一次加载后,就会缓存在本地。
javascript
// foo.js
let i = 0
module.exports = () => ++i
const clearModule = require('clear-module')
require('./foo')()
//=> 1
require('./foo')()
//=> 2
clearModule('./foo')
require('./foo')()
//=> 1
fixture-stdout:看名字像是控制台输出的包,看描述是用来测试用的
javascript
var StdOutFixture = require('fixture-stdout')
var fixture = new StdOutFixture()
// Keep track of writes so we can check them later..
var _writes = []
// Capture a write to stdout
fixture.capture(function onWrite(string, encoding, fd) {
_writes.push({
string: string,
encoding: encoding,
fd: fd
})
// If you return `false`, you'll prevent the write to the original stream (useful for preventing log output during tests.)
return false
})
// Uses intercepted version of stdout
console.log('a')
console.log('b')
fixture.release()
// Now we're back to the original version of stdout
console.log('c')
console.log('d')
// Voila!
// Only the first two logs ("a" and "b") are in our `_writes` array
mock-require:模拟引入某个包
javascript
var mock = require('mock-require')
mock('http', {
request: function () {
console.log('http.request called')
}
})
var http = require('http')
http.request() // 'http.request called'
strip-ansi:字符串中取出 ansi 码
javascript
import stripAnsi from 'strip-ansi'
stripAnsi('\u001B[4mUnicorn\u001B[0m')
//=> 'Unicorn'
stripAnsi('\u001B]8;;https://github.com\u0007Click\u001B]8;;\u0007')
//=> 'Click'
xo:可以简单理解封装的 eslint 工具
javascript
$ xo --help
Usage
$ xo [<file|glob> ...]
Options
--fix Automagically fix issues
--reporter Reporter to use
--env Environment preset [Can be set multiple times]
--global Global variable [Can be set multiple times]
--ignore Additional paths to ignore [Can be set multiple times]
--space Use space indent instead of tabs [Default: 2]
--no-semicolon Prevent use of semicolons
--prettier Conform to Prettier code style
--node-version Range of Node.js version to support
--plugin Include third-party plugins [Can be set multiple times]
--extend Extend defaults with a custom config [Can be set multiple times]
--open Open files with issues in your editor
--quiet Show only errors and no warnings
--extension Additional extension to lint [Can be set multiple times]
--cwd=<dir> Working directory for files
--stdin Validate/fix code from stdin
--stdin-filename Specify a filename for the --stdin option
--print-config Print the ESLint configuration for the given file
Examples
$ xo
$ xo index.js
$ xo *.js !foo.js
$ xo --space
$ xo --env=node --env=mocha
$ xo --plugin=react
$ xo --plugin=html --extension=html
$ echo 'const x=true' | xo --stdin --fix
$ xo --print-config=index.js
Tips
- Add XO to your project with `npm init xo`.
- Put options in package.json instead of using flags so other tools can read it.
正式依赖
boxen
创建一个盒子在命令行
javascript
const boxen = require('boxen')
console.log(boxen('unicorn', { padding: 1 }))
/*
┌─────────────┐
│ │
│ unicorn │
│ │
└─────────────┘
*/
console.log(boxen('unicorn', { padding: 1, margin: 1, borderStyle: 'double' }))
/*
╔═════════════╗
║ ║
║ unicorn ║
║ ║
╚═════════════╝
*/
console.log(boxen('unicorns love rainbows', { title: 'magical', titleAlignment: 'center' }))
/*
┌────── magical ───────┐
│unicorns love rainbows│
└──────────────────────┘
*/
chalk
这个就很熟悉了,写过 cli 的都懂,美化输出日志的
configstore
我的理解就是更方便拿到 package.json 的配置或者设置
javascript
import Configstore from 'configstore'
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
// Create a Configstore instance.
const config = new Configstore(packageJson.name, { foo: 'bar' })
console.log(config.get('foo'))
//=> 'bar'
has-yarn
判断项目是否使用 yarn
import-lazy
懒加载模块,在用的时候才会真正引入该包,有个注意是不能使用解构,那样懒加载失效
is-ci
判断是否是 ci 环境
is-installed-globally
检查包是不是已经全局安装了
is-npm
判读是如何启动的脚本
javascript
import {isNpmOrYarn, isNpm, isYarn} from 'is-npm';
console.table({isNpmOrYarn, isNpm, isYarn});
$ node foo.js
# ┌─────────────┬────────┐
# │ (index) │ Values │
# ├─────────────┼────────┤
# │ isNpmOrYarn │ false │
# │ isNpm │ false │
# │ isYarn │ false │
# └─────────────┴────────┘
$ npm run foo
# ┌─────────────┬────────┐
# │ (index) │ Values │
# ├─────────────┼────────┤
# │ isNpmOrYarn │ true │
# │ isNpm │ true │
# │ isYarn │ false │
# └─────────────┴────────┘
$ yarn run foo
# ┌─────────────┬────────┐
# │ (index) │ Values │
# ├─────────────┼────────┤
# │ isNpmOrYarn │ true │
# │ isNpm │ false │
# │ isYarn │ true │
# └─────────────┴────────┘
is-yarn-global
检测包是否是 yarn 全局安装的
latest-version
获取包的最新版本
javascript
import latestVersion from 'latest-version'
console.log(await latestVersion('ava'))
//=> '0.18.0'
console.log(await latestVersion('@sindresorhus/df'))
//=> '1.0.1'
// Also works with semver ranges and dist-tags
console.log(await latestVersion('npm', { version: 'latest-5' }))
//=> '5.5.1'
pupa
用来定义模版的
semver
参加过前几期的都知道,整版本号的
semver-diff
对比版本号的不同
javascript
import semverDiff from 'semver-diff'
semverDiff('1.1.1', '1.1.2')
//=> 'patch'
semverDiff('1.1.1-foo', '1.1.2')
//=> 'prepatch'
semverDiff('0.0.1', '1.0.0')
//=> 'major'
semverDiff('0.0.1-foo', '1.0.0')
//=> 'premajor'
semverDiff('0.0.1', '0.1.0')
//=> 'minor'
semverDiff('0.0.1-foo', '0.1.0')
//=> 'preminor'
semverDiff('0.0.1-foo', '0.0.1-foo.bar')
//=> 'prerelease'
semverDiff('0.1.0', '0.1.0+foo')
//=> 'build'
semverDiff('0.0.1', '0.0.1')
//=> undefined
semverDiff('0.0.2', '0.0.1')
//=> undefined
xdg-basedir
用在 linux 下获取 xdg 基本路径
核心代码具体流程解析
先上代码
javascript
'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')
const chalk = importLazy('chalk')
const semver = importLazy('semver')
const semverDiff = importLazy('semver-diff')
const latestVersion = importLazy('latest-version')
const isNpm = importLazy('is-npm')
const isInstalledGlobally = importLazy('is-installed-globally')
const isYarnGlobal = importLazy('is-yarn-global')
const hasYarn = importLazy('has-yarn')
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'
// 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
// 根据环境变量禁用检查
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() {
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()
}
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 (
!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
未完待续。。。