Skip to content

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

未完待续。。。

如有转载或 CV 的请标注本站原文地址