vue-cli源码分析之upgrade命令

1,882 阅读3分钟

我们先来看下upgrade命令的配置

packages/@vue/cli/bin/vue.js

program
    .command('upgrade [plugin-name]')
    .description('(experimental) upgrade vue cli service / plugins')
    .option('-t, --to <version>', 'Upgrade <package-name> to a version that is not latest')
    .option('-f, --from <version>', 'Skip probing installed plugin, assuming it is upgraded from the designated version')
    .option('-r, --registry <url>', 'Use specified npm registry when installing dependencies')
    .option('--all', 'Upgrade all plugins')
    .option('--next', 'Also check for alpha / beta / rc versions when upgrading')
    .action((packageName, options) => {
    require('../lib/upgrade')(packageName, options)
    })

upgrade命令是用来升级@vue/cli-servicevue-cli插件的,它有以下选项:

  • -t, --to <version>:升级 到指定的版本
  • -f, --from <version>:跳过本地版本检测,默认插件是从此处指定的版本升级上来
  • -r, --registry <url>:使用指定的 registry 地址安装依赖
  • --all:升级所有的插件
  • --next:检查插件新版本时,包括 alpha/beta/rc 版本在内

命令处理函数中在../lib/upgrade文件中,我们打开该文件可以看到导出了upgrade函数。

packages/@vue/cli/lib/upgrade.js

module.exports = (...args) => {
  return upgrade(...args).catch(err => {
    error(err)
    if (!process.env.VUE_CLI_TEST) {
      process.exit(1)
    }
  })
}

接下来我们来分析下upgrade函数

async function upgrade (packageName, options, context = process.cwd()) {
  // 判断是否有修改了未提交的文件
  if (!(await confirmIfGitDirty(context))) {
    return
  }
  // 新建一个Upgrader实例
  const upgrader = new Upgrader(context)

  // 没有指定具体的npm包名称
  if (!packageName) {
    // 目标版本
    if (options.to) {
      error(`Must specify a package name to upgrade to ${options.to}`)
      process.exit(1)
    }
    // 是否全部进行升级
    if (options.all) {
      return upgrader.upgradeAll(options.next)
    }

    // 查找可以进行升级的npm包
    const upgradable = await upgrader.checkForUpdates(options.next)
    if (upgradable) {
      // 询问是否升级这些包
      const { ok } = await inquirer.prompt([
        {
          name: 'ok',
          type: 'confirm',
          message: 'Continue to upgrade these plugins?',
          default: true
        }
      ])
      // 允许升级的话则升级这些可以升级的包
      if (ok) {
        return upgrader.upgradeAll(options.next)
      }
    }

    return
  }
  // 升级指定的包
  return upgrader.upgrade(packageName, options)
}

这里主要是对命令选项的一些判断,如果没有初始化git或者有待提交的文件是不允许进行升级的,如果有修改了的文件则询问用户是否进行升级。没有指定具体要升级的插件名的话,指定了目标版本则会报错,如果是升级全部插件的话,则执行全部升级,不是全部升级的话,则遍历插件看哪些可以进行升级,遍历完成后询问用户是否进行这些插件的升级,用户允许的话则执行这些插件的升级。

接着我们来看下Upgrader实例

packages/@vue/cli/lib/Upgrader.js

先来看下构造函数里定义的属性

constructor (context = process.cwd()) {
  // 获取当前工作目录
  this.context = context
  // 保存package.json的内容
  this.pkg = getPkg(this.context)
  // 创建一个包管理对象(获取包地址,安装、升级、删除包等)
  this.pm = new PackageManager({ context })
}

然后该实例定义了四个函数

批量升级npm包

async upgradeAll (includeNext) {
  // TODO: should confirm for major version upgrades
  // for patch & minor versions, upgrade directly
  // for major versions, prompt before upgrading
  // 判断哪些npm包可以进行升级(包括alpha/beta/rc版本在内)
  const upgradable = await this.getUpgradable(includeNext)

  // 所有的npm包都是最新的,没有可以升级的
  if (!upgradable.length) {
    done('Seems all plugins are up to date. Good work!')
    return
  }

  // 循环可以升级的包
  for (const p of upgradable) {
    // reread to avoid accidentally writing outdated package.json back
    // 这里再一次读取了package.json的内容,避免包不是最新的
    this.pkg = getPkg(this.context)
    // 依次进行升级
    await this.upgrade(p.name, { to: p.latest })
  }

  done('All plugins are up to date!')
}

npm包单个进行升级

  async upgrade (pluginId, options) {
    // 解析出包名
    const packageName = resolvePluginId(pluginId)

    let depEntry, required
    for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
      if (this.pkg[depType] && this.pkg[depType][packageName]) {
        depEntry = depType
        // 解析出需要升级的包版本
        required = this.pkg[depType][packageName]
        break
      }
    }
    // 若没有解析出来,则说明不存在
    if (!required) {
      throw new Error(`Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('package.json')}`)
    }

    // 判断插件是从哪个版本升级
    const installed = options.from || this.pm.getInstalledVersion(packageName)

    // 不存在则抛出错误
    if (!installed) {
      throw new Error(
        `Can't find ${chalk.yellow(packageName)} in ${chalk.yellow('node_modules')}. Please install the dependencies first.\n` +
        `Or to force upgrade, you can specify your current plugin version with the ${chalk.cyan('--from')} option`
      )
    }

    // 要升级到的指定版本,没有指定则升级到最新版本
    let targetVersion = options.to || 'latest'
    // if the targetVersion is not an exact version
    // 判断版本号是否符合格式
    if (!/\d+\.\d+\.\d+/.test(targetVersion)) {
      if (targetVersion === 'latest') {
        logWithSpinner(`Getting latest version of ${packageName}`)
      } else {
        logWithSpinner(`Getting max satisfying version of ${packageName}@${options.to}`)
      }

      // 获取该npm包的版本
      targetVersion = await this.pm.getRemoteVersion(packageName, targetVersion)
      // 如果没有指定版本并且版本检查包括 alpha/beta/rc 版本在内
      if (!options.to && options.next) {
        const next = await this.pm.getRemoteVersion(packageName, 'next')
        if (next) {
          // targetVersion >= next的话取targetVersion,反之取next
          targetVersion = semver.gte(targetVersion, next) ? targetVersion : next
        }
      }
      stopSpinner()
    }

    // 如果目标版本是已经安装的版本
    if (targetVersion === installed) {
      log(`Already installed ${packageName}@${targetVersion}`)
      // 取较大的版本号
      const newRange = tryGetNewerRange(`~${targetVersion}`, required)
      if (newRange !== required) {
        this.pkg[depEntry][packageName] = newRange
        fs.writeFileSync(path.resolve(this.context, 'package.json'), JSON.stringify(this.pkg, null, 2))
        log(`${chalk.green('✔')}  Updated version range in ${chalk.yellow('package.json')}`)
      }
      return
    }

    log(`Upgrading ${packageName} from ${installed} to ${targetVersion}`)
    // 执行升级
    await this.pm.upgrade(`${packageName}@~${targetVersion}`)

    // The cached `pkg` field won't automatically update after running `this.pm.upgrade`.
    // Also, `npm install pkg@~version` won't replace the original `"pkg": "^version"` field.
    // So we have to manually update `this.pkg` and write to the file system in `runMigrator`

    this.pkg[depEntry][packageName] = `~${targetVersion}`

    const resolvedPluginMigrator =
      resolveModule(`${packageName}/migrator`, this.context)

    if (resolvedPluginMigrator) {
      // for unit tests, need to run migrator in the same process for mocks to work
      // TODO: fix the tests and remove this special case
      if (process.env.VUE_CLI_TEST) {
        clearRequireCache()
        await require('./migrate').runMigrator(
          this.context,
          {
            id: packageName,
            apply: loadModule(`${packageName}/migrator`, this.context),
            baseVersion: installed
          },
          this.pkg
        )
        return
      }

      const cliBin = path.resolve(__dirname, '../bin/vue.js')
      // Run migrator in a separate process to avoid all kinds of require cache issues
      await execa('node', [cliBin, 'migrate', packageName, '--from', installed], {
        cwd: this.context,
        stdio: 'inherit'
      })
    }
  }

先解析出包名,看是否存在于package.json中,不存在则报错,存在则获取当前安装的版本号以及要升级到的目标版本号,进行升级并同时修改package.json文件。

获取哪些包可以进行升级的函数

async getUpgradable (includeNext) {
  const upgradable = []

  // get current deps
  // filter @vue/cli-service, @vue/cli-plugin-* & vue-cli-plugin-*
  // 遍历package.json中的依赖
  for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
    for (const [name, range] of Object.entries(this.pkg[depType] || {})) {
      if (name !== '@vue/cli-service' && !isPlugin(name)) {
        continue
      }
      // 获取当前安装的版本号
      const installed = await this.pm.getInstalledVersion(name)
      // 获取远程版本号
      const wanted = await this.pm.getRemoteVersion(name, range)
      // 没有获取到,则说明没有安装
      if (!installed) {
        throw new Error(`At least one dependency can't be found. Please install the dependencies before trying to upgrade`)
      }
      // 获取最新的版本号
      let latest = await this.pm.getRemoteVersion(name)
      // 如果可以安装非正式版本,则获取非正式版本号
      if (includeNext) {
        const next = await this.pm.getRemoteVersion(name, 'next')
        if (next) {
          latest = semver.gte(latest, next) ? latest : next
        }
      }
      // 如果当前安装的版本号小于最新的版本号
      if (semver.lt(installed, latest)) {
        // always list @vue/cli-service as the first one
        // as it's depended by all other plugins
        if (name === '@vue/cli-service') {
          upgradable.unshift({ name, installed, wanted, latest })
        } else {
          upgradable.push({ name, installed, wanted, latest })
        }
      }
    }
  }
  // 返回可以进行升级的npm包
  return upgradable
}

获取需要升级的包,打印出相关信息

async checkForUpdates (includeNext) {
  logWithSpinner('Gathering package information...')
  // 获取可以进行升级的包
  const upgradable = await this.getUpgradable(includeNext)
  stopSpinner()
  // 若长度为0,则代表没有可以升级的包,均是最新的
  if (!upgradable.length) {
    done('Seems all plugins are up to date. Good work!')
    return
  }

  // format the output
  // adapted from @angular/cli
  // 格式化输出升级的信息
  const names = upgradable.map(dep => dep.name)
  let namePad = Math.max(...names.map(x => x.length)) + 2
  if (!Number.isFinite(namePad)) {
    namePad = 30
  }
  const pads = [namePad, 16, 16, 16, 0]
  console.log(
    '  ' +
    ['Name', 'Installed', 'Wanted', 'Latest', 'Command to upgrade'].map(
      (x, i) => chalk.underline(x.padEnd(pads[i]))
    ).join('')
  )
  for (const p of upgradable) {
    const fields = [
      p.name,
      p.installed || 'N/A',
      p.wanted,
      p.latest,
      `vue upgrade ${p.name}${includeNext ? ' --next' : ''}`
    ]
    // TODO: highlight the diff part, like in `yarn outdated`
    console.log('  ' + fields.map((x, i) => x.padEnd(pads[i])).join(''))
  }
  // 返回需要升级的包
  return upgradable
}
}