]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
build: add new release script
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 10 Jun 2022 14:29:14 +0000 (16:29 +0200)
committerEduardo San Martin Morote <posva@users.noreply.github.com>
Thu, 30 Jun 2022 07:59:00 +0000 (09:59 +0200)
.gitignore
LICENSE [moved from packages/router/LICENSE with 95% similarity]
package.json
packages/router/package.json
pnpm-lock.yaml
scripts/release.mjs [new file with mode: 0644]
scripts/release.sh [deleted file]

index bc9ecae6c15710173df272eb7912777f0be778d1..bdf5aad180db241fc49daf38a1b8693c877ac9df 100644 (file)
@@ -15,3 +15,4 @@ yalc.lock
 .yalc
 local.log
 _selenium-server.log
+packages/*/LICENSE
similarity index 95%
rename from packages/router/LICENSE
rename to LICENSE
index 2d297f23ee1080f602baa0dfd279aed274ade19f..0c77562bea0465ab994162cc740914ed609d700b 100644 (file)
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2020 Eduardo San Martin Morote
+Copyright (c) 2019-present Eduardo San Martin Morote
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
index 5b13861c8279bf918e11add5ada3e69eb5e61dc0..4a4d235f43ea45f52fe1f2adfe503dc1ceac656b 100644 (file)
     "test": "pnpm run -r test"
   },
   "devDependencies": {
-    "chalk": "^4.1.0",
+    "chalk": "^4.1.2",
+    "enquirer": "^2.3.6",
+    "execa": "^6.1.0",
+    "globby": "^13.1.1",
     "lint-staged": "^13.0.0",
+    "minimist": "^1.2.6",
+    "p-series": "^3.0.0",
+    "semver": "^7.3.7",
     "yorkie": "^2.0.0"
   },
   "gitHooks": {
index 5b21dd22eb7a7c8ef76fbeb2846ae86a731325fa..d40bf673efadd450911725bd99d4d4340d89c946 100644 (file)
@@ -48,7 +48,6 @@
   ],
   "scripts": {
     "dev": "vite --config playground/vite.config.js",
-    "release": "bash scripts/release.sh",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
     "build": "rimraf dist && rollup -c rollup.config.js",
     "build:dts": "api-extractor run --local --verbose && tail -n +9 src/globalExtensions.ts >> dist/vue-router.d.ts",
index 683f2928e09332765f7815e1b8424bfb3d0d6fb2..b03cb8cd3e2e7a6d9b15f386f8b74469ac108323 100644 (file)
@@ -4,12 +4,24 @@ importers:
 
   .:
     specifiers:
-      chalk: ^4.1.0
+      chalk: ^4.1.2
+      enquirer: ^2.3.6
+      execa: ^6.1.0
+      globby: ^13.1.1
       lint-staged: ^13.0.0
+      minimist: ^1.2.6
+      p-series: ^3.0.0
+      semver: ^7.3.7
       yorkie: ^2.0.0
     devDependencies:
       chalk: 4.1.2
-      lint-staged: 13.0.0
+      enquirer: 2.3.6
+      execa: 6.1.0
+      globby: 13.1.1
+      lint-staged: 13.0.0_enquirer@2.3.6
+      minimist: 1.2.6
+      p-series: 3.0.0
+      semver: 7.3.7
       yorkie: 2.0.0
 
   packages/docs:
@@ -2443,6 +2455,13 @@ packages:
       once: 1.4.0
     dev: true
 
+  /enquirer/2.3.6:
+    resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
+    engines: {node: '>=8.6'}
+    dependencies:
+      ansi-colors: 4.1.1
+    dev: true
+
   /envinfo/7.8.1:
     resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==}
     engines: {node: '>=4'}
@@ -3102,6 +3121,17 @@ packages:
       slash: 3.0.0
     dev: true
 
+  /globby/13.1.1:
+    resolution: {integrity: sha512-XMzoDZbGZ37tufiv7g0N4F/zp3zkwdFtVbV3EHsVl1KQr4RPLfNoT068/97RPshz2J5xYNEjLKKBKaGHifBd3Q==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dependencies:
+      dir-glob: 3.0.1
+      fast-glob: 3.2.11
+      ignore: 5.2.0
+      merge2: 1.4.1
+      slash: 4.0.0
+    dev: true
+
   /got/11.8.2:
     resolution: {integrity: sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==}
     engines: {node: '>=10.19.0'}
@@ -4174,7 +4204,7 @@ packages:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
     dev: true
 
-  /lint-staged/13.0.0:
+  /lint-staged/13.0.0_enquirer@2.3.6:
     resolution: {integrity: sha512-vWban5utFt78VZohbosUxNIa46KKJ+KOQTDWTQ8oSl1DLEEVl9zhUtaQbiiydAmx+h2wKJK2d0+iMaRmknuWRQ==}
     engines: {node: ^14.13.1 || >=16.0.0}
     hasBin: true
@@ -4185,7 +4215,7 @@ packages:
       debug: 4.3.4
       execa: 6.1.0
       lilconfig: 2.0.5
-      listr2: 4.0.5
+      listr2: 4.0.5_enquirer@2.3.6
       micromatch: 4.0.5
       normalize-path: 3.0.0
       object-inspect: 1.12.2
@@ -4197,7 +4227,7 @@ packages:
       - supports-color
     dev: true
 
-  /listr2/4.0.5:
+  /listr2/4.0.5_enquirer@2.3.6:
     resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==}
     engines: {node: '>=12'}
     peerDependencies:
@@ -4208,6 +4238,7 @@ packages:
     dependencies:
       cli-truncate: 2.1.0
       colorette: 2.0.17
+      enquirer: 2.3.6
       log-update: 4.0.0
       p-map: 4.0.0
       rfdc: 1.3.0
@@ -4854,6 +4885,11 @@ packages:
       aggregate-error: 3.1.0
     dev: true
 
+  /p-series/3.0.0:
+    resolution: {integrity: sha512-geaabIwiqy+jN4vuJROl1rpMJT/myHAMAfdubPQGJT3Grr8td+ogWvTk2qLsNlhYXcoZZAfl01pfq7lK3/gYKQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dev: true
+
   /p-try/1.0.0:
     resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
     engines: {node: '>=4'}
@@ -5412,6 +5448,11 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /slash/4.0.0:
+    resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
+    engines: {node: '>=12'}
+    dev: true
+
   /slice-ansi/3.0.0:
     resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
     engines: {node: '>=8'}
diff --git a/scripts/release.mjs b/scripts/release.mjs
new file mode 100644 (file)
index 0000000..d77723e
--- /dev/null
@@ -0,0 +1,385 @@
+import minimist from 'minimist'
+import _fs from 'fs'
+import { join, resolve, dirname } from 'path'
+import { fileURLToPath } from 'url'
+import chalk from 'chalk'
+import semver from 'semver'
+import enquirer from 'enquirer'
+import { execa } from 'execa'
+import pSeries from 'p-series'
+import { globby } from 'globby'
+
+const { prompt } = enquirer
+const fs = _fs.promises
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+const args = minimist(process.argv.slice(2))
+let {
+  skipBuild,
+  tag: optionTag,
+  dry: isDryRun,
+  skipCleanCheck: skipCleanGitCheck,
+} = args
+
+// const preId =
+//   args.preid ||
+//   (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
+const EXPECTED_BRANCH = 'main'
+
+const incrementVersion = increment =>
+  semver.inc(currentVersion, increment, preId)
+const bin = name => resolve(__dirname, '../node_modules/.bin/' + name)
+/**
+ * @param bin {string}
+ * @param args {string}
+ * @param opts {import('execa').CommonOptions<string>}
+ * @returns
+ */
+const run = (bin, args, opts = {}) =>
+  execa(bin, args, { stdio: 'inherit', ...opts })
+const dryRun = (bin, args, opts = {}) =>
+  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
+const runIfNotDry = isDryRun ? dryRun : run
+const getPkgRoot = pkg => resolve(__dirname, '../packages/' + pkg)
+const step = msg => console.log(chalk.cyan(msg))
+
+async function main() {
+  if (!skipCleanGitCheck) {
+    const isDirtyGit = !!(
+      await run('git', ['status', '--porcelain'], { stdio: 'pipe' })
+    ).stdout
+
+    if (isDirtyGit) {
+      console.log(chalk.red(`Git repo isn't clean.`))
+      return
+    }
+
+    const currentBranch = (
+      await run('git', ['branch', '--show-current'], { stdio: 'pipe' })
+    ).stdout
+
+    if (currentBranch !== EXPECTED_BRANCH) {
+      console.log(
+        chalk.red(
+          `You should be on branch "${EXPECTED_BRANCH}" but are on "${currentBranch}"`
+        )
+      )
+      return
+    }
+  } else {
+    console.log(chalk.bold.white(`Skipping git checks...`))
+  }
+
+  if (!skipCleanGitCheck) {
+    const isOutdatedRE = new RegExp(
+      `\\W${EXPECTED_BRANCH}\\W.*(?:fast-forwardable|local out of date)`,
+      'i'
+    )
+
+    const isOutdatedGit = isOutdatedRE.test(
+      (await run('git', ['remote', 'show', 'origin'], { stdio: 'pipe' })).stdout
+    )
+
+    if (isOutdatedGit) {
+      console.log(chalk.red(`Git branch is not in sync with remote`))
+      return
+    }
+  }
+
+  const changedPackages = await getChangedPackages()
+  if (!changedPackages.length) {
+    console.log(chalk.red(`No packages have changed since last release`))
+    return
+  }
+
+  if (isDryRun) {
+    console.log('\n' + chalk.bold.blue('This is a dry run') + '\n')
+  }
+
+  // NOTE: I'm unsure if this would mess up the changelog
+  // const { pickedPackages } = await prompt({
+  //   type: 'multiselect',
+  //   name: 'pickedPackages',
+  //   messages: 'What packages do you want to release?',
+  //   choices: changedPackages.map((pkg) => pkg.name),
+  // })
+
+  const packagesToRelease = changedPackages
+  // const packagesToRelease = changedPackages.filter((pkg) =>
+  //   pickedPackages.includes(pkg.name)
+  // )
+
+  step(
+    `Ready to release ${packagesToRelease
+      .map(({ name }) => chalk.bold.white(name))
+      .join(', ')}`
+  )
+
+  const pkgWithVersions = await pSeries(
+    packagesToRelease.map(({ name, path, pkg }) => async () => {
+      let { version } = pkg
+
+      const prerelease = semver.prerelease(version)
+      const preId = prerelease && prerelease[0]
+
+      const versionIncrements = [
+        'patch',
+        'minor',
+        'major',
+        ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : []),
+      ]
+
+      const { release } = await prompt({
+        type: 'select',
+        name: 'release',
+        message: `Select release type for ${chalk.bold.white(name)}`,
+        choices: versionIncrements
+          .map(i => `${i}: ${name} (${semver.inc(version, i, preId)})`)
+          .concat(['custom']),
+      })
+
+      if (release === 'custom') {
+        version = (
+          await prompt({
+            type: 'input',
+            name: 'version',
+            message: `Input custom version (${chalk.bold.white(name)})`,
+            initial: version,
+          })
+        ).version
+      } else {
+        version = release.match(/\((.*)\)/)[1]
+      }
+
+      if (!semver.valid(version)) {
+        throw new Error(`invalid target version: ${version}`)
+      }
+
+      return { name, path, version, pkg }
+    })
+  )
+
+  const { yes: isReleaseConfirmed } = await prompt({
+    type: 'confirm',
+    name: 'yes',
+    message: `Releasing \n${pkgWithVersions
+      .map(
+        ({ name, version }) =>
+          `  ยท ${chalk.white(name)}: ${chalk.yellow.bold('v' + version)}`
+      )
+      .join('\n')}\nConfirm?`,
+  })
+
+  if (!isReleaseConfirmed) {
+    return
+  }
+
+  step('\nUpdating versions in package.json files...')
+  await updateVersions(pkgWithVersions)
+
+  step('\nGenerating changelogs...')
+  for (const pkg of pkgWithVersions) {
+    step(` -> ${pkg.name} (${pkg.path})`)
+    await runIfNotDry(`pnpm`, ['run', 'changelog'], { cwd: pkg.path })
+    await runIfNotDry(`pnpm`, ['exec', 'prettier', '--write', 'CHANGELOG.md'], {
+      cwd: pkg.path,
+    })
+    await fs.copyFile(
+      resolve(__dirname, '../LICENSE'),
+      resolve(pkg.path, 'LICENSE')
+    )
+  }
+
+  const { yes: isChangelogCorrect } = await prompt({
+    type: 'confirm',
+    name: 'yes',
+    message: 'Are the changelogs correct?',
+  })
+
+  if (!isChangelogCorrect) {
+    return
+  }
+
+  step('\nBuilding all packages...')
+  if (!skipBuild && !isDryRun) {
+    await run('pnpm', ['run', 'build'])
+    await run('pnpm', ['run', 'build:dts'])
+  } else {
+    console.log(`(skipped)`)
+  }
+
+  const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
+  if (stdout) {
+    step('\nCommitting changes...')
+    await runIfNotDry('git', [
+      'add',
+      'packages/*/CHANGELOG.md',
+      'packages/*/package.json',
+    ])
+    await runIfNotDry('git', [
+      'commit',
+      '-m',
+      `release: ${pkgWithVersions
+        .map(({ name, version }) => `${name}@${version}`)
+        .join(' ')}`,
+    ])
+  } else {
+    console.log('No changes to commit.')
+  }
+
+  step('\nCreating tags...')
+  let versionsToPush = []
+  for (const pkg of pkgWithVersions) {
+    versionsToPush.push(`refs/tags/${pkg.name}@${pkg.version}`)
+    await runIfNotDry('git', ['tag', `${pkg.name}@${pkg.version}`])
+  }
+
+  step('\nPublishing packages...')
+  for (const pkg of pkgWithVersions) {
+    await publishPackage(pkg)
+  }
+
+  step('\nPushing to Github...')
+  await runIfNotDry('git', ['push', 'origin', ...versionsToPush])
+  await runIfNotDry('git', ['push'])
+}
+
+/**
+ *
+ * @param packageList {{ name: string; path: string; version: string, pkg: any }}
+ */
+async function updateVersions(packageList) {
+  return Promise.all(
+    packageList.map(({ pkg, version, path, name }) => {
+      pkg.version = version
+      updateDeps(pkg, 'dependencies', packageList)
+      updateDeps(pkg, 'peerDependencies', packageList)
+      const content = JSON.stringify(pkg, null, 2) + '\n'
+      return isDryRun
+        ? dryRun('write', [name], {
+            dependencies: pkg.dependencies,
+            peerDependencies: pkg.peerDependencies,
+          })
+        : fs.writeFile(join(path, 'package.json'), content)
+    })
+  )
+}
+
+function updateDeps(pkg, depType, updatedPackages) {
+  const deps = pkg[depType]
+  if (!deps) return
+  step(`Updating ${chalk.bold(depType)} for ${chalk.bold.white(pkg.name)}...`)
+  Object.keys(deps).forEach(dep => {
+    const updatedDep = updatedPackages.find(pkg => pkg.name === dep)
+    // avoid updated peer deps that are external like @vue/devtools-api
+    if (dep && updatedDep) {
+      console.log(
+        chalk.yellow(
+          `${pkg.name} -> ${depType} -> ${dep}@~${updatedDep.version}`
+        )
+      )
+      deps[dep] = '>=' + updatedDep.version
+    }
+  })
+}
+
+async function publishPackage(pkg) {
+  step(`Publishing ${pkg.name}...`)
+
+  try {
+    await runIfNotDry(
+      'pnpm',
+      [
+        'publish',
+        ...(optionTag ? ['--tag', optionTag] : []),
+        '--access',
+        'public',
+        '--publish-branch',
+        EXPECTED_BRANCH,
+      ],
+      {
+        cwd: pkg.path,
+        stdio: 'pipe',
+      }
+    )
+    console.log(
+      chalk.green(`Successfully published ${pkg.name}@${pkg.version}`)
+    )
+  } catch (e) {
+    if (e.stderr.match(/previously published/)) {
+      console.log(chalk.red(`Skipping already published: ${pkg.name}`))
+    } else {
+      throw e
+    }
+  }
+}
+
+/**
+ * Get the packages that have changed. Based on `lerna changed` but without lerna.
+ *
+ * @returns {Promise<{ name: string; path: string; pkg: any; version: string }[]}
+ */
+async function getChangedPackages() {
+  let lastTag
+
+  try {
+    const { stdout } = await run('git', ['describe', '--tags', '--abbrev=0'], {
+      stdio: 'pipe',
+    })
+    lastTag = stdout
+  } catch (error) {
+    // maybe there are no tags
+    console.error(`Couldn't get the last tag, using first commit...`)
+    const { stdout } = await run(
+      'git',
+      ['rev-list', '--max-parents=0', 'HEAD'],
+      { stdio: 'pipe' }
+    )
+    lastTag = stdout
+  }
+  const folders = await globby(join(__dirname, '../packages/*'), {
+    onlyFiles: false,
+  })
+
+  const pkgs = await Promise.all(
+    folders.map(async folder => {
+      if (!(await fs.lstat(folder)).isDirectory()) return null
+
+      const pkg = JSON.parse(await fs.readFile(join(folder, 'package.json')))
+      if (!pkg.private) {
+        const { stdout: hasChanges } = await run(
+          'git',
+          [
+            'diff',
+            lastTag,
+            '--',
+            // apparently {src,package.json} doesn't work
+            join(folder, 'src'),
+            join(folder, 'package.json'),
+          ],
+          { stdio: 'pipe' }
+        )
+
+        if (hasChanges) {
+          return {
+            path: folder,
+            name: pkg.name,
+            version: pkg.version,
+            pkg,
+          }
+        } else {
+          return null
+        }
+      }
+    })
+  )
+
+  return pkgs.filter(p => p)
+}
+
+main().catch(error => {
+  console.error(error)
+  process.exit(1)
+})
diff --git a/scripts/release.sh b/scripts/release.sh
deleted file mode 100644 (file)
index f3bea32..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-set -e
-echo "Current version:" $(grep version package.json | sed -E 's/^.*"([0-9][^"]+)".*$/\1/')
-echo "Enter version e.g., 4.0.1: "
-read VERSION
-
-read -p "Releasing v$VERSION - are you sure? (y/n)" -n 1 -r
-echo    # (optional) move to a new line
-if [[ $REPLY =~ ^[Yy]$ ]]
-then
-  echo "Releasing v$VERSION ..."
-
-  # clear existing ts cache
-  rm -rf node_modules/.rts2_cache
-
-  # generate the version so that the changelog can be generated too
-  yarn version --no-git-tag-version --no-commit-hooks --new-version $VERSION
-
-  yarn run build
-  yarn run build:dts
-  yarn run test:dts
-
-  # changelog
-  yarn run changelog
-  yarn prettier --write CHANGELOG.md
-  echo "Please check the git history and the changelog and press enter"
-  read OKAY
-
-  # commit and tag
-  git add CHANGELOG.md package.json
-  git commit -m "release: v$VERSION"
-  git tag "v$VERSION"
-
-  # commit
-  yarn publish --tag latest --new-version "$VERSION" --no-commit-hooks --no-git-tag-version
-
-  # publish
-  git push origin refs/tags/v$VERSION
-  git push
-fi