const semver = require('semver') const { log } = require('proc-log') const pacote = require('pacote') const { read } = require('read') const Table = require('cli-table3') const { run, git, npm, pkgPath: cliPath, pkg: cli, spawn } = require('./util.js') const fs = require('fs').promises const resetdeps = () => npm('run', 'resetdeps') const op = () => spawn('op', 'item', 'get', 'npm', '--otp', { out: true, ok: true }) const getWorkspaceTag = async ({ name, version }) => { const { prerelease, major } = semver.parse(version) if (prerelease.length) { return 'prerelease' } const pack = await pacote.packument(name, { preferOnline: true }).catch(() => null) if (!pack) { // This might never happen but if we were to create a new workspace that has never // been published before it should be set to latest right away. return 'latest' } if (major >= semver.major(pack['dist-tags'].latest)) { // if the major version we are publishing is greater than the major version // of the latest dist-tag, then this should be latest too return 'latest' } // Anything else is a backport return 'backport' } const versionNotExists = async ({ name, version }) => { const spec = `${name}@${version}` let exists try { await pacote.manifest(spec, { preferOnline: true }) exists = true // if it exists, no publish needed } catch { exists = false // otherwise its needs publishing } log.info(`${spec} exists=${exists}`) return !exists } const getPublishes = async ({ force }) => { const publishPackages = [] for (const { pkg, pkgPath } of await cli.mapWorkspaces({ public: true })) { const updatePkg = async (cb) => { const data = JSON.parse(await fs.readFile(pkgPath, 'utf8')) const result = cb(data) await fs.writeFile(pkgPath, JSON.stringify(result, null, 2)) return result } if (force || await versionNotExists(pkg)) { publishPackages.push({ workspace: `--workspace=${pkg.name}`, name: pkg.name, version: pkg.version, dependencies: pkg.dependencies, devDependencies: pkg.devDependencies, tag: await getWorkspaceTag(pkg), updatePkg, }) } } if (force || await versionNotExists(cli)) { publishPackages.push({ workspace: '', name: cli.name, version: cli.version, tag: `next-${semver.major(cli.version)}`, dependencies: cli.dependencies, devDependencies: cli.devDependencies, updatePkg: async (cb) => { const result = cb(cli) await fs.writeFile(cliPath, JSON.stringify(result, null, 2)) return result }, }) } return publishPackages } const main = async (opts) => { const { test, otp, dryRun, smokePublish, packDestination } = opts const hasPackDest = !!packDestination const publishes = await getPublishes({ force: smokePublish }) if (!publishes.length) { throw new Error( 'Nothing to publish, exiting.\n' + 'All packages to publish should have their version bumped before running this script.' ) } const table = new Table({ head: ['name', 'version', 'tag'] }) for (const publish of publishes) { table.push([publish.name, publish.version, publish.tag]) } const preformOperations = hasPackDest ? ['publish', 'pack'] : ['publish'] const confirmMessage = [ `Ready to ${preformOperations.join(',')} the following packages:`, table.toString(), smokePublish ? null : 'Ok to proceed? ', ].filter(Boolean).join('\n') if (smokePublish) { log.info(confirmMessage) } else { const confirm = await read({ prompt: confirmMessage, default: 'y' }) if (confirm.trim().toLowerCase().charAt(0) !== 'y') { throw new Error('Aborted') } } await git('clean', '-fd') await resetdeps() await npm('ls', '--omit=dev', { quiet: true }) await npm('rm', '--global', '--force', 'npm') await npm('link', '--force', '--ignore-scripts') if (test) { await npm('run', 'lint-all', '--ignore-scripts') await npm('run', 'postlint', '--ignore-scripts') await npm('run', 'test-all', '--ignore-scripts') } await npm('prune', '--omit=dev', '--no-save', '--no-audit', '--no-fund') await npm('install', '-w', 'docs', '--ignore-scripts', '--no-audit', '--no-fund') if (smokePublish) { log.info(`Skipping git dirty check due to local smoke publish test being run`) } else { await git.dirty() } let count = -1 if (smokePublish) { // when we have a smoke test run we'd want to bump the version or else npm will throw an error even with dry-run // this is the equivalent of running `npm version prerelease`, but ensuring all internally used workflows are bumped for (const publish of publishes) { const { version } = await publish.updatePkg((pkg) => ({ ...pkg, version: `${pkg.version}-smoke.0` })) for (const ipublish of publishes) { if (ipublish.dependencies?.[publish.name]) { await ipublish.updatePkg((pkg) => ({ ...pkg, dependencies: { ...pkg.dependencies, [publish.name]: version, }, })) } if (ipublish.devDependencies?.[publish.name]) { await ipublish.updatePkg((pkg) => ({ ...pkg, devDependencies: { ...pkg.devDependencies, [publish.name]: version, }, })) } } } await npm('install') } for (const publish of publishes) { log.info(`Publishing ${publish.name}@${publish.version} to ${publish.tag} ${count++}/${publishes.length}`) const workspace = publish.workspace && `--workspace=${publish.name}` const publishPkg = (...args) => npm('publish', workspace, `--tag=${publish.tag}`, ...args) if (hasPackDest) { await npm( 'pack', workspace, packDestination && `--pack-destination=${packDestination}` ) } if (smokePublish) { await publishPkg('--dry-run', '--ignore-scripts') } else { await publishPkg( dryRun && '--dry-run', otp && `--otp=${otp === 'op' ? await op() : otp}` ) } } } run(main).catch(resetdeps)