import type { ReleaseType } from 'semver' import { execSync } from 'child_process' import execa from 'execa' import fse from 'fs-extra' import { fileURLToPath } from 'node:url' import pLimit from 'p-limit' import path from 'path' import semver from 'semver' import { getPackageDetails } from './getPackageDetails.js' import { packagePublishList } from './publishList.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) const projectRoot = path.resolve(dirname, '../../') const rootPackageJsonPath = path.resolve(projectRoot, 'package.json') const npmPublishLimit = pLimit(5) const cwd = path.resolve(dirname, '../../') const execaOpts: execa.Options = { stdio: 'inherit' } type PackageDetails = { /** Name in package.json / npm registry */ name: string /** Full path to package relative to project root */ packagePath: `packages/${string}` /** Short name is the directory name */ shortName: string /** Version in package.json */ version: string } type PackageReleaseType = 'canary' | ReleaseType type PublishResult = { name: string success: boolean details?: string } type PublishOpts = { dryRun?: boolean tag?: 'beta' | 'canary' | 'latest' } type Workspace = { version: () => Promise tag: string packages: PackageDetails[] showVersions: () => Promise bumpVersion: (type: PackageReleaseType) => Promise build: () => Promise publish: (opts: PublishOpts) => Promise publishSync: (opts: PublishOpts) => Promise } export const getWorkspace = async () => { const build = async () => { await execa('pnpm', ['install'], execaOpts) const buildResult = await execa('pnpm', ['build:all', '--output-logs=errors-only'], execaOpts) if (buildResult.exitCode !== 0) { console.error('Build failed') console.log(buildResult.stderr) throw new Error('Build failed') } } // Publish one package at a time const publishSync: Workspace['publishSync'] = async ({ dryRun, tag = 'canary' }) => { const packageDetails = await getPackageDetails(packagePublishList) const results: PublishResult[] = [] for (const pkg of packageDetails) { const res = await publishSinglePackage(pkg, { dryRun, tag }) results.push(res) } console.log(`\n\nResults:\n`) console.log( results .map((result) => { if (!result.success) { console.error(result.details) return ` ❌ ${result.name}` } return ` ✅ ${result.name}` }) .join('\n') + '\n', ) } const publish = async () => { const packageDetails = await getPackageDetails(packagePublishList) const results = await Promise.allSettled( packageDetails.map((pkg) => publishPackageThrottled(pkg, { dryRun: true })), ) console.log(`\n\nResults:\n`) console.log( results .map((result) => { if (result.status === 'rejected') { console.error(result.reason) return ` ❌ ${String(result.reason)}` } const { name, success, details } = result.value let summary = ` ${success ? '✅' : '❌'} ${name}` if (details) { summary += `\n ${details}\n` } return summary }) .join('\n') + '\n', ) } const showVersions = async () => { const { packages, version } = await getCurrentPackageState() console.log(`\n Version: ${version}\n`) console.log(` Changes (${packages.length} packages):\n`) console.log(`${packages.map((p) => ` - ${p.name.padEnd(32)} ${p.version}`).join('\n')}\n`) } const setVersion = async (version: string) => { const rootPackageJson = await fse.readJSON(rootPackageJsonPath) rootPackageJson.version = version await fse.writeJSON(rootPackageJsonPath, rootPackageJson, { spaces: 2 }) const packageJsons = await getPackageDetails(packagePublishList) await Promise.all( packageJsons.map(async (pkg) => { const packageJson = await fse.readJSON(`${pkg.packagePath}/package.json`) packageJson.version = version await fse.writeJSON(`${pkg.packagePath}/package.json`, packageJson, { spaces: 2 }) }), ) } const bumpVersion = async (bumpType: PackageReleaseType) => { const { version: monorepoVersion, packages: packageDetails } = await getCurrentPackageState() let nextReleaseVersion if (bumpType === 'canary') { const hash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim().slice(0, 7) nextReleaseVersion = semver.inc(monorepoVersion, 'patch') + `-canary.${hash}` } else { nextReleaseVersion = semver.inc(monorepoVersion, bumpType) } console.log(`\n Version: ${monorepoVersion} => ${nextReleaseVersion}\n`) console.log(` Bump: ${bumpType}`) console.log(` Changes (${packageDetails.length} packages):\n`) console.log( `${packageDetails.map((p) => ` - ${p.name.padEnd(32)} ${p.version} => ${nextReleaseVersion}`).join('\n')}\n`, ) await setVersion(nextReleaseVersion) } const workspace: Workspace = { version: async () => (await fse.readJSON(rootPackageJsonPath)).version, tag: 'latest', packages: await getPackageDetails(packagePublishList), showVersions, bumpVersion, build, publish, publishSync, } return workspace } async function getCurrentPackageState(): Promise<{ packages: PackageDetails[] version: string }> { const packageDetails = await getPackageDetails(packagePublishList) const rootPackageJson = await fse.readJSON(rootPackageJsonPath) return { packages: packageDetails, version: rootPackageJson.version } } /** Publish with promise concurrency throttling */ async function publishPackageThrottled(pkg: PackageDetails, opts?: { dryRun?: boolean }) { const { dryRun = true } = opts ?? {} return npmPublishLimit(() => publishSinglePackage(pkg, { dryRun })) } async function publishSinglePackage(pkg: PackageDetails, opts: PublishOpts) { console.log(`🚀 ${pkg.name} publishing...`) const { dryRun, tag = 'canary' } = opts try { const cmdArgs = ['publish', '-C', pkg.packagePath, '--no-git-checks', '--tag', tag] if (dryRun) { cmdArgs.push('--dry-run') } const { exitCode, stderr } = await execa('pnpm', cmdArgs, { cwd, // stdio: ['ignore', 'ignore', 'pipe'], stdio: 'inherit', }) if (exitCode !== 0) { console.log(`\n\n❌ ${pkg.name} ERROR: pnpm publish failed\n\n${stderr}`) return { name: pkg.name, success: false, details: `Exit Code: ${exitCode}, stderr: ${stderr}`, } } console.log(`✅ ${pkg.name} published`) return { name: pkg.name, success: true } } catch (err: unknown) { console.error(err) return { name: pkg.name, success: false, details: err instanceof Error ? `Error publishing ${pkg.name}: ${err.message}` : `Unexpected error publishing ${pkg.name}: ${String(err)}`, } } }