]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
build: update release script
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 27 Nov 2024 14:49:14 +0000 (15:49 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 27 Nov 2024 14:49:14 +0000 (15:49 +0100)
package.json
pnpm-lock.yaml
scripts/release.mjs

index 642eedb230e541b4794c79ce4577009ba123981e..fa2e6853efbb1e93f4f2cc5f7e660a213e89eec2 100644 (file)
@@ -29,6 +29,7 @@
     "postinstall": "simple-git-hooks"
   },
   "devDependencies": {
+    "@posva/prompts": "^2.4.4",
     "@rollup/plugin-alias": "^5.1.0",
     "@rollup/plugin-commonjs": "^28.0.1",
     "@rollup/plugin-node-resolve": "^15.2.3",
@@ -42,7 +43,6 @@
     "@vue/server-renderer": "~3.5.13",
     "chalk": "^5.3.0",
     "conventional-changelog-cli": "^2.2.2",
-    "enquirer": "^2.4.1",
     "execa": "^9.5.1",
     "globby": "^14.0.1",
     "happy-dom": "^15.11.6",
index 15318d121274c824df08c4452ac7ce2c807c93b2..6f7dbea4db0e09d78391619ea5d946022d7183bc 100644 (file)
@@ -12,6 +12,9 @@ importers:
 
   .:
     devDependencies:
+      '@posva/prompts':
+        specifier: ^2.4.4
+        version: 2.4.4
       '@rollup/plugin-alias':
         specifier: ^5.1.0
         version: 5.1.1(rollup@4.27.3)
@@ -51,9 +54,6 @@ importers:
       conventional-changelog-cli:
         specifier: ^2.2.2
         version: 2.2.2
-      enquirer:
-        specifier: ^2.4.1
-        version: 2.4.1
       execa:
         specifier: ^9.5.1
         version: 9.5.1
@@ -1359,6 +1359,10 @@ packages:
   '@polka/url@1.0.0-next.28':
     resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
 
+  '@posva/prompts@2.4.4':
+    resolution: {integrity: sha512-8aPwklhbSV2VN/NQMBNFkuo8+hlJVdcFRXp4NCIfdcahh3qNEcaSoD8qXjru0OlN1sONJ7le7p6+YUbALaG6Mg==}
+    engines: {node: '>= 14'}
+
   '@redocly/ajv@8.11.2':
     resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
 
@@ -2642,10 +2646,6 @@ packages:
     resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
     engines: {node: '>=10.13.0'}
 
-  enquirer@2.4.1:
-    resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
-    engines: {node: '>=8.6'}
-
   entities@4.5.0:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
@@ -3324,6 +3324,10 @@ packages:
     resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
     engines: {node: '>=6'}
 
+  kleur@4.1.5:
+    resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+    engines: {node: '>=6'}
+
   klona@2.0.6:
     resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
     engines: {node: '>= 8'}
@@ -6463,6 +6467,11 @@ snapshots:
 
   '@polka/url@1.0.0-next.28': {}
 
+  '@posva/prompts@2.4.4':
+    dependencies:
+      kleur: 4.1.5
+      sisteransi: 1.0.5
+
   '@redocly/ajv@8.11.2':
     dependencies:
       fast-deep-equal: 3.1.3
@@ -7870,11 +7879,6 @@ snapshots:
       graceful-fs: 4.2.11
       tapable: 2.2.1
 
-  enquirer@2.4.1:
-    dependencies:
-      ansi-colors: 4.1.3
-      strip-ansi: 6.0.1
-
   entities@4.5.0: {}
 
   environment@1.1.0: {}
@@ -8655,6 +8659,8 @@ snapshots:
 
   kleur@3.0.3: {}
 
+  kleur@4.1.5: {}
+
   klona@2.0.6: {}
 
   knitwork@1.1.0: {}
index 42146787ad62de86df6eb496a80760a30749dc3b..3b01f0697e3e16e5cd31d6effec5e0335f56b62b 100644 (file)
@@ -1,21 +1,20 @@
-import minimist from 'minimist'
 import fs from 'node:fs/promises'
-import { join, resolve, dirname } from 'node:path'
+import { existsSync } from 'node:fs'
+import { dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
+import minimist from 'minimist'
 import chalk from 'chalk'
 import semver from 'semver'
-import enquirer from 'enquirer'
+import prompts from '@posva/prompts'
 import { execa } from 'execa'
 import pSeries from 'p-series'
 import { globby } from 'globby'
 
-const { prompt } = enquirer
-
 const __filename = fileURLToPath(import.meta.url)
 const __dirname = dirname(__filename)
 
 const args = minimist(process.argv.slice(2))
-let {
+const {
   skipBuild,
   tag: optionTag,
   dry: isDryRun,
@@ -45,24 +44,30 @@ Flags:
 }
 
 // const preId =
-//   args.preid ||
+//   args.preId ||
 //   (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
 const EXPECTED_BRANCH = 'v2'
+// this package will use tags like v1.0.0 while the rest will use the full package name like @pinia/testing@1.0.0
+const MAIN_PKG_NAME = 'pinia'
 
-const bin = (name) => resolve(__dirname, '../node_modules/.bin/' + name)
 /**
- * @param bin {string}
- * @param args {string}
- * @param opts {import('execa').CommonOptions<string>}
- * @returns
+ * @type {typeof execa}
  */
 const run = (bin, args, opts = {}) =>
   execa(bin, args, { stdio: 'inherit', ...opts })
+/**
+ * @param bin {string}
+ * @param args {string[]}
+ * @param opts {import('execa').Options}
+ */
 const dryRun = (bin, args, opts = {}) =>
-  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
+  console.log(chalk.blue(`[dry-run] ${bin} ${args.join(' ')}`), opts)
 const runIfNotDry = isDryRun ? dryRun : run
-const getPkgRoot = (pkg) => resolve(__dirname, '../packages/' + pkg)
-const step = (msg) => console.log(chalk.cyan(msg))
+
+/**
+ * @param msg {string[]}
+ */
+const step = (...msg) => console.log(chalk.cyan(...msg))
 
 async function main() {
   if (!skipCleanGitCheck) {
@@ -107,33 +112,44 @@ async function main() {
     }
   }
 
-  const changedPackages = await getChangedPackages()
+  const packagesFolders = [
+    join(__dirname, '../packages/pinia'),
+    join(__dirname, '../packages/testing'),
+    join(__dirname, '../packages/nuxt'),
+  ]
+
+  const changedPackages = await getChangedPackages(...packagesFolders)
+
   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')
+    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),
-  // })
+  // allow to select which packages
+  const { pickedPackages } = await prompts({
+    type: 'multiselect',
+    name: 'pickedPackages',
+    message: 'What packages do you want to release?',
+    instructions: false,
+    min: 1,
+    choices: changedPackages.map((pkg) => ({
+      title: pkg.name,
+      value: pkg.name,
+      selected: true,
+    })),
+  })
 
-  const packagesToRelease = changedPackages
-  // const packagesToRelease = changedPackages.filter((pkg) =>
-  //   pickedPackages.includes(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(', ')}`
+    `Ready to release ${packagesToRelease.map(({ name }) => chalk.bold.white(name)).join(', ')}`
   )
 
   const pkgWithVersions = await pSeries(
@@ -150,31 +166,46 @@ async function main() {
         ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : []),
       ]
 
-      const { release } = await prompt({
+      const betaVersion = semver.inc(version, 'prerelease', 'beta')
+
+      const { release } = await prompts({
         type: 'select',
         name: 'release',
         message: `Select release type for ${chalk.bold.white(name)}`,
         choices: versionIncrements
-          .map((i) => `${i}: ${name} (${semver.inc(version, i, preId)})`)
+          .map((release) => {
+            const newVersion = semver.inc(version, release, preId)
+            return {
+              value: newVersion,
+              title: `${release}: ${name} (${newVersion})`,
+            }
+          })
           .concat(
             optionTag === 'beta'
-              ? [`beta: ${name} (${semver.inc(version, 'prerelease', 'beta')})`]
+              ? [
+                  {
+                    title: `beta: ${name} (${betaVersion})`,
+                    value: betaVersion,
+                  },
+                ]
               : []
           )
-          .concat(['custom']),
+          .concat([{ value: 'custom', title: 'custom' }]),
       })
 
+      console.log(release)
+
       if (release === 'custom') {
         version = (
-          await prompt({
-            type: 'input',
+          await prompts({
+            type: 'text',
             name: 'version',
             message: `Input custom version (${chalk.bold.white(name)})`,
             initial: version,
           })
         ).version
       } else {
-        version = release.match(/\((.*)\)/)[1]
+        version = release
       }
 
       if (!semver.valid(version)) {
@@ -185,20 +216,21 @@ async function main() {
     })
   )
 
-  // TODO: we need to reorder packages based on dependencies
-  // pinia needs to be first
-  const piniaPkgIndx = packagesToRelease.find(({ name }) => name === 'pinia')
-  if (piniaPkgIndx > 0) {
-    packagesToRelease.unshift(packagesToRelease.splice(piniaPkgIndx, 1)[0])
+  // put the main package first as others might depend on it
+  const mainPkgIndex = packagesToRelease.find(
+    ({ name }) => name === MAIN_PKG_NAME
+  )
+  if (mainPkgIndex > 0) {
+    packagesToRelease.unshift(packagesToRelease.splice(mainPkgIndex, 1)[0])
   }
 
-  const { yes: isReleaseConfirmed } = await prompt({
+  const { yes: isReleaseConfirmed } = await prompts({
     type: 'confirm',
     name: 'yes',
     message: `Releasing \n${pkgWithVersions
       .map(
         ({ name, version }) =>
-          `  · ${chalk.white(name)}: ${chalk.yellow.bold('v' + version)}`
+          `  · ${chalk.white(name)}: ${chalk.yellow.bold(`v${version}`)}`
       )
       .join('\n')}\nConfirm?`,
   })
@@ -216,22 +248,53 @@ async function main() {
   }
 
   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 Promise.all(
+    pkgWithVersions.map(async (pkg) => {
+      step(` -> ${pkg.name} (${pkg.path})`)
+      const changelogExists = existsSync(join(pkg.path, 'CHANGELOG.md'))
+
+      if (!changelogExists) {
+        console.log(chalk.yellow(`No CHANGELOG.md found in ${pkg.name}`))
+      }
+
+      await runIfNotDry(
+        `pnpm`,
+        [
+          'exec',
+          'conventional-changelog',
+          '-i',
+          'CHANGELOG.md',
+          '--same-file',
+          '-p',
+          'conventionalcommits',
+          '-r',
+          changelogExists ? '1' : '0',
+          '--commit-path',
+          '.',
+          '--lerna-package',
+          pkg.name,
+          ...(pkg.name === MAIN_PKG_NAME
+            ? []
+            : ['--tag-prefix', `${pkg.name}@`]),
+        ],
+        { cwd: pkg.path }
+      )
+      await runIfNotDry(
+        `pnpm`,
+        ['exec', 'prettier', '--write', 'CHANGELOG.md'],
+        {
+          cwd: pkg.path,
+        }
+      )
+      // NOTE: pnpm publish automatically copies the LICENSE file
     })
-    await fs.copyFile(
-      resolve(__dirname, '../LICENSE'),
-      resolve(pkg.path, 'LICENSE')
-    )
-  }
+  )
 
-  const { yes: isChangelogCorrect } = await prompt({
+  const { yes: isChangelogCorrect } = await prompts({
     type: 'confirm',
     name: 'yes',
     message: 'Are the changelogs correct?',
+    initial: true,
   })
 
   if (!isChangelogCorrect) {
@@ -253,24 +316,21 @@ async function main() {
       'add',
       'packages/*/CHANGELOG.md',
       'packages/*/package.json',
-      'pnpm-lock.yaml',
     ])
     await runIfNotDry('git', [
       'commit',
       '-m',
-      `release: ${pkgWithVersions
-        .map(({ name, version }) => `${name}@${version}`)
-        .join(' ')}`,
+      `release: ${pkgWithVersions.map(({ name, version }) => `${name}@${version}`).join(' ')}`,
     ])
   } else {
     console.log('No changes to commit.')
   }
 
   step('\nCreating tags...')
-  let versionsToPush = []
+  const versionsToPush = []
   for (const pkg of pkgWithVersions) {
     const tagName =
-      pkg.name === 'vue-router'
+      pkg.name === MAIN_PKG_NAME
         ? `v${pkg.version}`
         : `${pkg.name}@${pkg.version}`
 
@@ -294,7 +354,7 @@ async function main() {
 
 /**
  *
- * @param packageList {{ name: string; path: string; version: string, pkg: any }}
+ * @param packageList {{ name: string; path: string; version: string, pkg: any }[]}
  */
 async function updateVersions(packageList) {
   return Promise.all(
@@ -304,7 +364,7 @@ async function updateVersions(packageList) {
         updateDeps(pkg, 'dependencies', packageList)
         updateDeps(pkg, 'peerDependencies', packageList)
       }
-      const content = JSON.stringify(pkg, null, 2) + '\n'
+      const content = `${JSON.stringify(pkg, null, 2)}\n`
       return isDryRun
         ? dryRun('write', [name], {
             version: pkg.version,
@@ -337,7 +397,7 @@ function updateDeps(pkg, depType, updatedPackages) {
             `${pkg.name} -> ${depType} -> ${dep}@>=${updatedDep.version}`
           )
         )
-        deps[dep] = '>=' + updatedDep.version
+        deps[dep] = `>=${updatedDep.version}`
       }
     }
   })
@@ -355,7 +415,7 @@ async function publishPackage(pkg) {
         ...(skipCleanGitCheck ? ['--no-git-checks'] : []),
         '--access',
         'public',
-        // specific to pinia
+        // only needed for branches other than main
         '--publish-branch',
         EXPECTED_BRANCH,
       ],
@@ -377,65 +437,101 @@ async function publishPackage(pkg) {
 }
 
 /**
- * Get the packages that have changed. Based on `lerna changed` but without lerna.
+ * Get the last tag published for a package or null if there are no tags
  *
- * @returns {Promise<{ name: string; path: string; pkg: any; version: string }[]}
+ * @param {string} pkgName - package name
+ * @returns {string} the last tag or full commit hash
  */
-async function getChangedPackages() {
-  let lastTag
-
+async function getLastTag(pkgName) {
   try {
-    const { stdout } = await run('git', ['describe', '--tags', '--abbrev=0'], {
-      stdio: 'pipe',
-    })
-    lastTag = stdout
+    const { stdout } = await run(
+      'git',
+      [
+        'describe',
+        '--tags',
+        '--abbrev=0',
+        '--match',
+        pkgName === MAIN_PKG_NAME ? 'v*' : `${pkgName}@*`,
+      ],
+      {
+        stdio: 'pipe',
+      }
+    )
+
+    return stdout
   } catch (error) {
-    // maybe there are no tags
-    console.error(`Couldn't get the last tag, using first commit...`)
+    console.log(
+      chalk.dim(
+        `Couldn't get "${chalk.bold(pkgName)}" last tag, using first commit...`
+      )
+    )
+
+    // 128 is the git exit code when there is nothing to describe
+    if (error.exitCode !== 128) {
+      console.error(error)
+    }
     const { stdout } = await run(
       'git',
       ['rev-list', '--max-parents=0', 'HEAD'],
       { stdio: 'pipe' }
     )
-    lastTag = stdout
+    return stdout
   }
-  // globby expects `/` even on windows
-  const folders = await globby(
-    join(__dirname, '../packages/*').replace(/\\/g, '/'),
-    {
-      onlyFiles: false,
-    }
-  )
+}
 
+/**
+ * Get the packages that have changed. Based on `lerna changed` but without lerna.
+ *
+ * @param {string[]} folders
+ * @returns {Promise<{ name: string; path: string; pkg: any; version: string; start: string }[]} a promise of changed packages
+ */
+async function getChangedPackages(...folders) {
   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 (!(await fs.lstat(folder)).isDirectory()) {
+        console.warn(chalk.dim(`Skipping "${folder}" as it is not a directory`))
+        return null
+      }
 
-        if (hasChanges) {
-          return {
-            path: folder,
-            name: pkg.name,
-            version: pkg.version,
-            pkg,
-          }
-        } else {
-          return null
+      const pkg = JSON.parse(
+        await fs.readFile(join(folder, 'package.json'), 'utf-8')
+      )
+      if (pkg.private) {
+        console.info(chalk.dim(`Skipping "${pkg.name}" it's private`))
+        return null
+      }
+
+      const lastTag = await getLastTag(pkg.name)
+
+      const { stdout: hasChanges } = await run(
+        'git',
+        [
+          'diff',
+          lastTag,
+          '--',
+          // apparently {src,package.json} doesn't work
+          join(folder, 'src'),
+          // TODO: should not check dev deps and should compare to last tag changes
+          join(folder, 'package.json'),
+        ],
+        { stdio: 'pipe' }
+      )
+
+      if (hasChanges) {
+        return {
+          path: folder,
+          name: pkg.name,
+          version: pkg.version,
+          pkg,
+          start: lastTag,
         }
+      } else {
+        console.warn(
+          chalk.dim(
+            `Skipping "${pkg.name}" as it has no changes since last release`
+          )
+        )
+        return null
       }
     })
   )