import { execSync } from 'child_process'; import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; const PACKAGES = [ 'libs/plugin-types', 'libs/plugins-styles', 'libs/plugins-runtime', ]; const determineArgs = async () => { return await yargs(hideBin(process.argv)) .version(false) .option('dryRun', { alias: 'd', description: 'Whether or not to perform a dry-run of the release process, defaults to true', type: 'boolean', default: true, }) .option('verbose', { description: 'Whether or not to enable verbose logging, defaults to false', type: 'boolean', default: false, }) .option('version', { description: 'Explicit version specifier to use (e.g., 1.5.0, patch, minor, major)', type: 'string', demandOption: true, }) .option('skip-publish', { description: 'Skip publishing the package to the registry', type: 'boolean', default: false, }) .option('preid', { description: 'The prerelease identifier to use (e.g., next, beta, alpha)', type: 'string', default: undefined, }) .option('latest', { description: 'Publish the package with the latest tag', type: 'boolean', default: true, }) .parseAsync(); }; interface PackageJson { name: string; version: string; [key: string]: unknown; } const readPackageJson = (packagePath: string): PackageJson => { const filePath = join(process.cwd(), packagePath, 'package.json'); return JSON.parse(readFileSync(filePath, 'utf-8')); }; const writePackageJson = (packagePath: string, content: PackageJson): void => { const filePath = join(process.cwd(), packagePath, 'package.json'); writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n'); }; const incrementVersion = ( currentVersion: string, specifier: string, preid?: string, ): string => { const [major, minor, patch] = currentVersion .split('-')[0] .split('.') .map(Number); switch (specifier) { case 'major': return preid ? `${major + 1}.0.0-${preid}.0` : `${major + 1}.0.0`; case 'minor': return preid ? `${major}.${minor + 1}.0-${preid}.0` : `${major}.${minor + 1}.0`; case 'patch': return preid ? `${major}.${minor}.${patch + 1}-${preid}.0` : `${major}.${minor}.${patch + 1}`; case 'premajor': return `${major + 1}.0.0-${preid || 'next'}.0`; case 'preminor': return `${major}.${minor + 1}.0-${preid || 'next'}.0`; case 'prepatch': return `${major}.${minor}.${patch + 1}-${preid || 'next'}.0`; case 'prerelease': { const preMatch = currentVersion.match(/-([^.]+)\.(\d+)$/); if (preMatch) { const preId = preid || preMatch[1]; const preNum = parseInt(preMatch[2], 10) + 1; return `${major}.${minor}.${patch}-${preId}.${preNum}`; } return `${major}.${minor}.${patch + 1}-${preid || 'next'}.0`; } default: // If specifier is an exact version (e.g., "1.5.0"), use it directly if (/^\d+\.\d+\.\d+/.test(specifier)) { return specifier; } throw new Error(`Unknown version specifier: ${specifier}`); } }; const log = (message: string, verbose: boolean, forceLog = false): void => { if (verbose || forceLog) { console.log(message); } }; (async () => { const args = await determineArgs(); // Get current version from one of the packages const currentPkg = readPackageJson(PACKAGES[0]); const currentVersion = currentPkg.version; const newVersion = incrementVersion(currentVersion, args.version, args.preid); console.log(`\n📦 Release: ${currentVersion} → ${newVersion}`); console.log(` Mode: ${args.dryRun ? 'DRY RUN' : 'REAL RELEASE'}`); console.log(` Tag: ${args.latest ? 'latest' : 'next'}\n`); // Update version in all packages for (const packagePath of PACKAGES) { const pkg = readPackageJson(packagePath); const oldVersion = pkg.version; pkg.version = newVersion; // Update internal dependencies const deps = pkg['dependencies'] as Record | undefined; if (deps && typeof deps === 'object') { for (const dep of Object.keys(deps)) { if (dep.startsWith('@penpot/')) { deps[dep] = `^${newVersion}`; } } } log(` ${pkg.name}: ${oldVersion} → ${newVersion}`, args.verbose, true); if (!args.dryRun) { writePackageJson(packagePath, pkg); } } console.log('\n🔨 Building packages...\n'); // Build all packages if (!args.dryRun) { execSync( 'pnpm --filter @penpot/plugins-runtime --filter @penpot/plugin-styles --filter @penpot/plugin-types build', { cwd: process.cwd(), stdio: 'inherit', }, ); } else { console.log(' [DRY RUN] Skipping build\n'); } // Publish packages if (!args.skipPublish) { console.log('\n📤 Publishing packages...\n'); const tag = args.latest ? 'latest' : 'next'; for (const packagePath of PACKAGES) { const pkg = readPackageJson(packagePath); const distPath = join('dist', packagePath.split('/').pop()!); if (args.dryRun) { console.log( ` [DRY RUN] Would publish ${pkg.name}@${newVersion} with tag "${tag}"`, ); } else { try { execSync( `pnpm publish --tag ${tag} --access public --no-git-checks`, { cwd: join(process.cwd(), distPath), stdio: args.verbose ? 'inherit' : 'pipe', }, ); console.log(` ✅ Published ${pkg.name}@${newVersion}`); } catch (error) { console.error(` ❌ Failed to publish ${pkg.name}`); throw error; } } } } else { console.log('\n⏭️ Skipping publish (--skip-publish)\n'); } console.log('\n✨ Release complete!\n'); if (!args.dryRun) { console.log('Next steps:'); console.log(` 1. Review the changes`); console.log( ` 2. Commit: git commit -am ":arrow_up: Updated plugins release to ${newVersion}"`, ); console.log(` 3. Push: git push\n`); } process.exit(0); })();