How is vue.js released

Original link: github.com/upupming/vu…

The preparatory work

Read the source code for the first time to participate in reading activities, read the summary of the tool function of the last period, learn a lot, feel like if chuan big brother to learn will certainly harvest full.

This time to read the source at: github1s.com/vuejs/vue-n… .

Clone the repository and run YARN to install dependencies.

There is a release script in package.json to run this file:

"release": "node scripts/release.js".Copy the code

You can add a — DRY (empty run) parameter to not test, compile, or push git.

You can then start debugging by pressing the Debug button directly above the scripts of the package.json file.

Run it first to see what it looks like:

You can see that the test and build sections are skipped, the commit and push sections are [dryrun], and the instructions are skipped as well (just print what dryRun would run if it wasn’t).

When it is done, it is found that Changelog.md has added all the commits since the last release (but the ones that chore does not care about are ignored) :

Since the entire project is a monorepo, the version numbers of all packages in package.json and their dependent internal packages are updated correctly:

Next, let’s analyze the source code.

Depend on the package

The first few lines show all the packages that the script depends on, so let’s look at each in detail.

const args = require('minimist')(process.argv.slice(2))
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const semver = require('semver')
const currentVersion = require('.. /package.json').version
const { prompt } = require('enquirer')
const execa = require('execa')
Copy the code

minimist

www.npmjs.com/package/min…

It is used to parse command line parameters.

var argv = require('minimist')(process.argv.slice(2));
console.log(argv);
Copy the code
$ node example/parse.js -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }

$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo'.'bar'.'baz' ],
  x: 3,
  y: 4,
  n: 5,
  a: true,
  b: true,
  c: true,
  beep: 'boop' }
Copy the code

As you can see in argv, _ is an array of all inputs that are not options, which are stored as argv[‘option’]=value.

A similar bag is the Yargs

chalk

www.npmjs.com/package/cha…

Used to add color to a string.

const chalk = require('chalk');

// Use chalk. Blue to print blue
console.log(chalk.blue('Hello world! '));
Copy the code

semver

www.npmjs.com/package/sem…

The Semantic Versioning helper package has some judgment and processing logic for some version numbers.

const semver = require('semver')

semver.valid("1.2.3") / / "1.2.3"
semver.valid('a.b.c') // null
semver.clean('= v1.2.3') / / "1.2.3"
semver.satisfies("1.2.3".'1. X | | > = 2.5.0 | | 5.0.0-7.2.3') // true
semver.gt("1.2.3".'9.8.7') // false
semver.lt("1.2.3".'9.8.7') // true
semver.minVersion('> = 1.0.0') / / '1.0.0'
// coerce is used to coerce the input into semver objects
semver.valid(semver.coerce('v2')) / / '2.0.0'
semver.valid(semver.coerce('42.6.7.9.3 - alpha')) / / '42.6.7'
Copy the code

enquirer

www.npmjs.com/package/enq…

It is used for interactive command line input.

const { prompt } = require('enquirer');

const response = await prompt({
  type: 'input'.name: 'username'.message: 'What is your username? '
});

console.log(response); // { username: 'jonschlinkert' }
Copy the code

execa

www.npmjs.com/package/exe…

Nodejs’s child_process package has similar functionality, but is much easier to use.

const execa = require('execa');

(async() = > {const {stdout} = await execa('echo'['unicorns']);
    console.log(stdout);
    //=> 'unicorns'}) ();Copy the code

Obtaining Configuration Information

const currentVersion = require('.. /package.json').version
/ / semver. Prerelease (1.2.3 - alpha. '1') - > [' alpha ', 1)
const preId =
  args.preid ||
  (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
const isDryRun = args.dry
const skipTests = args.skipTests
const skipBuild = args.skipBuild
// Get the names of all subpackages, filter out.ts files and hide files
const packages = fs
  .readdirSync(path.resolve(__dirname, '.. /packages'))
  .filter(p= >! p.endsWith('.ts') && !p.startsWith('. '))

// Skip the package, there is no empty array, should not be used
const skippedPackages = []

// Upgrade options available during subsequent interactive input
const versionIncrements = [
  'patch'.'minor'.'major'. (preId ? ['prepatch'.'preminor'.'premajor'.'prerelease'] : [])]// The following are some auxiliary functions
The inc function returns the new version number given the release type
/ / semver. Inc (' 1.2.3 ', 'prerelease', 'beta') - > '1 - beta. 0'
const inc = i= > semver.inc(currentVersion, i, preId)
const bin = name= > path.resolve(__dirname, '.. /node_modules/.bin/' + name)
// Run any executable file
const run = (bin, args, opts = {}) = >
  execa(bin, args, { stdio: 'inherit'. opts })// Empty run: print the running parameters
const dryRun = (bin, args, opts = {}) = >
  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
const runIfNotDry = isDryRun ? dryRun : run
const getPkgRoot = pkg= > path.resolve(__dirname, '.. /packages/' + pkg)
const step = msg= > console.log(chalk.cyan(msg))
Copy the code

The main main process

The main process is divided into the following steps:

  • Determine the new version number to releasetargetVersion
    • Command line specification
    • The user to selectversionIncrementsOne of them gets the new version number
    • User input
  • Run the test
  • callupdateVersionsFunction updates the version numbers of all subpackages, as well as their dependencies
  • Build all package
  • runyarn changelogGenerate the changelog
  • If the file changes, git commit all changes
  • Called separately for each packagepublishPackageFunction to publish
    • Also note that the package corresponding to the project root directory is not published, and itspackage.jsonNone at allnameField, you can think of it as just a Monorepo configuration
  • Push to making

The two functions updateVersions and publishPackage are called, and we’ll see the logic later.

async function main() {
  let targetVersion = args._[0]

  if(! targetVersion) {// no explicit version, offer suggestions
    const { release } = await prompt({
      type: 'select'.name: 'release'.message: 'Select release type'.choices: versionIncrements.map(i= > `${i} (${inc(i)}) `).concat(['custom'])})if (release === 'custom') {
      targetVersion = (
        await prompt({
          type: 'input'.name: 'version'.message: 'Input custom version'.initial: currentVersion
        })
      ).version
    } else {
      targetVersion = release.match(/ / / ((. *) \)) [1]}}if(! semver.valid(targetVersion)) {throw new Error(`invalid target version: ${targetVersion}`)}const { yes } = await prompt({
    type: 'confirm'.name: 'yes'.message: `Releasing v${targetVersion}. Confirm? `
  })

  if(! yes) {return
  }

  // run tests before release
  step('\nRunning tests... ')
  if(! skipTests && ! isDryRun) {await run(bin('jest'),'--clearCache'])
    await run('yarn'['test'.'--bail'])}else {
    console.log(`(skipped)`)}// update all package versions and inter-dependencies
  step('\nUpdating cross dependencies... ')
  updateVersions(targetVersion)

  // build all packages with types
  step('\nBuilding all packages... ')
  if(! skipBuild && ! isDryRun) {await run('yarn'['build'.'--release'])
    // test generated dts files
    step('\nVerifying type declarations... ')
    await run('yarn'['test-dts-only'])}else {
    console.log(`(skipped)`)}// generate changelog
  await run(`yarn`['changelog'])

  const { stdout } = await run('git'['diff'] and {stdio: 'pipe' })
  if (stdout) {
    step('\nCommitting changes... ')
    await runIfNotDry('git'['add'.'-A'])
    await runIfNotDry('git'['commit'.'-m'.`release: v${targetVersion}`])}else {
    console.log('No changes to commit.')}// publish packages
  step('\nPublishing packages... ')
  for (const pkg of packages) {
    await publishPackage(pkg, targetVersion, runIfNotDry)
  }

  // push to GitHub
  step('\nPushing to GitHub... ')
  await runIfNotDry('git'['tag'.`v${targetVersion}`])
  await runIfNotDry('git'['push'.'origin'.`refs/tags/v${targetVersion}`])
  await runIfNotDry('git'['push'])

  if (isDryRun) {
    console.log(`\nDry run finished - run git diff to see package changes.`)}if (skippedPackages.length) {
    console.log(
      chalk.yellow(
        `The following packages are skipped and NOT published:\n- ${skippedPackages.join(
          '\n- '
        )}`))}console.log()
}

main().catch(err= > {
  console.error(err)
})
Copy the code

updateVersionsfunction

UpdateVersions accepts only one version parameter because the version number of all packages is the same from release to release. This simplifies the processing logic, but even if a package has not been updated, it will be published with the updated version number.

For the root directory and all subpackages, the package.json file is updated using the updatePackage function, which updates the Version, dependencies, and peerDependencies fields.

function updateVersions(version) {
  // 1. update root package.json
  updatePackage(path.resolve(__dirname, '.. '), version)
  // 2. update all packages
  packages.forEach(p= > updatePackage(getPkgRoot(p), version))
}

function updatePackage(pkgRoot, version) {
  const pkgPath = path.resolve(pkgRoot, 'package.json')
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  pkg.version = version
  updateDeps(pkg, 'dependencies', version)
  updateDeps(pkg, 'peerDependencies', version)
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null.2) + '\n')}function updateDeps(pkg, depType, version) {
  const deps = pkg[depType]
  if(! deps)return
  Object.keys(deps).forEach(dep= > {
    if (
      dep === 'vue' ||
      (dep.startsWith('@vue') && packages.includes(dep.replace(/^@vue\//.' ')))) {console.log(
        chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
      )
      deps[dep] = version
    }
  })
}
Copy the code

publishPackagefunction

PublishPackage is used to publish individual packages.

Vue 2 is still installed by default when NPM I vue is installed, as you can see here that Vue 3 is released with the next tag, which requires NPM I vue@next.

If an error is reported that a package with the same version number has been sent, skip it.

async function publishPackage(pkgName, version, runIfNotDry) {
  if (skippedPackages.includes(pkgName)) {
    return
  }
  const pkgRoot = getPkgRoot(pkgName)
  const pkgPath = path.resolve(pkgRoot, 'package.json')
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  if (pkg.private) {
    return
  }

  // For now, all 3.x packages except "vue" can be published as
  // `latest`, whereas "vue" will be published under the "next" tag.
  let releaseTag = null
  if (args.tag) {
    releaseTag = args.tag
  } else if (version.includes('alpha')) {
    releaseTag = 'alpha'
  } else if (version.includes('beta')) {
    releaseTag = 'beta'
  } else if (version.includes('rc')) {
    releaseTag = 'rc'
  } else if (pkgName === 'vue') {
    // TODO remove when 3.x becomes default
    releaseTag = 'next'
  }

  // TODO use demographics release channel after Official 3.0 release
  // const releaseTag = semver.prerelease(version)[0] || null

  step(`Publishing ${pkgName}. `)
  try {
    await runIfNotDry(
      'yarn'['publish'.'--new-version', version, ... (releaseTag ? ['--tag', releaseTag] : []),
        '--access'.'public'] and {cwd: pkgRoot,
        stdio: 'pipe'})console.log(chalk.green(`Successfully published ${pkgName}@${version}`))}catch (e) {
    if (e.stderr.match(/previously published/)) {
      console.log(chalk.red(`Skipping already published: ${pkgName}`))}else {
      throw e
    }
  }
}
Copy the code

Automatically generate GitHub Release

After git push, according to the GitHub Action configuration of the project, the corresponding release will be automatically generated according to the tag, such as release: v3.2.4. GitHub release is as follows:

conclusion

  • Feel very clear code logic, good at extracting tool functions, e.grunIfNotDry.getPkgRootThese things, you can reduce the amount of code
  • Learned how to use itsemverThe package automatically generates a new version number, so try not to manually pick a version number name in the future.
  • All subpackages under a monorepo keep the same version number, making it easier to operate