diff --git a/tooling/publish/publish.ts b/tooling/publish/publish.ts index 71d1d345..93ccc8cf 100644 --- a/tooling/publish/publish.ts +++ b/tooling/publish/publish.ts @@ -2,9 +2,8 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import arg from 'arg'; -import semver from 'semver'; - -const sdkPackages = ['sdk', 'react-native', 'web', 'nextjs', 'express']; +import type { ReleaseType } from 'semver'; +import semver, { RELEASE_TYPES } from 'semver'; const workspacePath = (relativePath: string) => path.resolve(__dirname, '../../', relativePath); @@ -14,13 +13,15 @@ function savePackageJson(absPath: string, data: Record) { } function exit(message: string, error?: unknown) { - console.log(`โŒ ${message}`); + console.log(`\n\nโŒ ${message}`); if (error instanceof Error) { console.log(`Error: ${error.message}`); console.log(error); } else if (typeof error === 'string') { console.log(`Error: ${error}`); } + console.log('\n'); + process.exit(1); } @@ -35,36 +36,111 @@ function checkUncommittedChanges() { execSync('git diff HEAD --exit-code'); console.log('โœ… No uncommitted changes'); } catch (error) { - exit('Uncommitted changes', error); + exit('Uncommitted changes'); } } +function getNextVersion(version: string, type: ReleaseType) { + const nextVersion = semver.inc(version, type); + if (!nextVersion) { + throw new Error('Invalid version'); + } + + if (type.startsWith('pre')) { + return nextVersion.replace(/-.*$/, '-beta'); + } + + return nextVersion; +} + +type IPackageJson = { + type?: string; + name: string; + version: string; + dependencies: Record; + devDependencies: Record; +}; + +type IPackageJsonWithExtra = IPackageJson & { + nextVersion: string; + localPath: string; +}; + function main() { const args = arg({ - // Types - '--version': String, + '--name': String, '--test': Boolean, '--skip-git': Boolean, - // Aliases - '-v': '--version', + // Semver + '--type': String, // major, minor, patch, premajor, preminor, prepatch, or prerelease }); if (!args['--skip-git']) { checkUncommittedChanges(); } - const version = args['--version']; + const pkgName = args['--name']; + const type = args['--type'] as ReleaseType; const test = args['--test']; + const packages: Record = {}; + const registry = test + ? 'http://localhost:4873' + : 'https://registry.npmjs.org'; - if (version && !semver.valid(version)) { - return console.error('Version is not valid'); + if (!RELEASE_TYPES.includes(type)) { + return exit( + `Invalid release type. Valid types are: ${RELEASE_TYPES.join(', ')}` + ); } - try { - for (const name of sdkPackages) { - const properties: Record = { + if (!pkgName) { + return exit('--name is requred'); + } + + // Get all SDKs + const sdks = fs + .readdirSync(workspacePath('./packages/sdks'), { + withFileTypes: true, + }) + .filter((item) => item.isDirectory() && !item.name.match(/^[._]/)) + .map((item) => item.name); + + // Get all SDK package.json + for (const name of sdks) { + const pkgJson = fs.readFileSync( + workspacePath(`./packages/sdks/${name}/package.json`), + 'utf-8' + ); + const parsed = JSON.parse(pkgJson) as IPackageJsonWithExtra; + parsed.nextVersion = getNextVersion(parsed.version, type); + parsed.localPath = `./packages/sdks/${name}`; + packages[parsed.name] = parsed; + } + + const target = packages[pkgName]; + + if (!target) { + return exit('Selected package does not exist'); + } + + // Find if any package is dependent on the target + const dependents: string[] = [target.name]; + function findDependents(visitPackageName: string) { + Object.entries(packages).forEach(([_name, pkg]) => { + if (pkg.dependencies?.[visitPackageName]) { + dependents.push(pkg.name); + findDependents(pkg.name); + } + }); + } + findDependents(target.name); + + function updatePackageJsonForRelease(name: string) { + const { nextVersion, localPath, ...restPkgJson } = packages[name]!; + const newPkgJson = JSON.parse( + JSON.stringify({ + ...restPkgJson, private: false, - version, type: 'module', main: './dist/index.js', module: './dist/index.mjs', @@ -74,58 +150,72 @@ function main() { import: './dist/index.js', require: './dist/index.cjs', }, - }; + version: nextVersion, + dependencies: Object.entries(restPkgJson.dependencies).reduce( + (acc, [depName, depVersion]) => { + const dep = packages[depName]; + if (!dep) { + return acc; + } - // Not sure if I even should have type: module for any sdk - if (name === 'nextjs') { - delete properties.type; - } - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const pkgJson = require( - workspacePath(`./packages/sdks/${name}/package.json`) - ); - savePackageJson(workspacePath(`./packages/sdks/${name}/package.json`), { - ...pkgJson, - ...properties, - dependencies: Object.entries(pkgJson.dependencies).reduce( - (acc, [depName, depVersion]) => ({ - ...acc, - [depName]: depName.startsWith('@openpanel') ? version : depVersion, - }), + return { + ...acc, + [depName]: dependents.includes(depName) + ? dep.nextVersion + : depVersion, + }; + }, {} ), - }); + }) + ) as IPackageJson; + + if (name === '@openpanel/nextjs') { + delete newPkgJson.type; } - } catch (error) { - exit('Update JSON files', error); + + savePackageJson(workspacePath(`${localPath}/package.json`), newPkgJson); + packages[name]!.dependencies = newPkgJson.dependencies; } - console.log('โœ… Update JSON files'); + dependents.forEach((dependent) => { + console.log( + `๐Ÿ“ฆ ${dependent} ยท Old Version: ${packages[dependent]?.version} ยท Next Version: ${packages[dependent]?.nextVersion}` + ); + updatePackageJsonForRelease(dependent); + }); - try { - for (const name of sdkPackages) { - execSync('pnpm build', { - cwd: workspacePath(`./packages/sdks/${name}`), - }); - } - } catch (error) { - exit('Failed build packages', error); - } + dependents.forEach((dependent) => { + console.log(`๐Ÿ”จ Building ${dependent}`); + execSync('pnpm build', { + cwd: workspacePath(packages[dependent]!.localPath), + }); + }); - console.log('โœ… Built packages'); + // Publish + dependents.forEach((dependent) => { + console.log(`๐Ÿš€ Publishing ${dependent} to ${registry}`); + execSync(`npm publish --access=public --registry ${registry}`, { + cwd: workspacePath(packages[dependent]!.localPath), + }); + }); - if (!test) { - try { - for (const name of sdkPackages) { - execSync('npm publish --access=public', { - cwd: workspacePath(`./packages/sdks/${name}`), - }); - } - } catch (error) { - exit('Failed publish packages', error); - } - } + // Restoring package.json + const filesToRestore = dependents + .map((dependent) => workspacePath(packages[dependent]!.localPath)) + .join(' '); + + execSync(`git checkout ${filesToRestore}`); + + // // Save new versions only ๐Ÿ˜ˆ + dependents.forEach((dependent) => { + const { nextVersion, localPath, ...restPkgJson } = packages[dependent]!; + console.log(`๐Ÿš€ Saving ${dependent} (${nextVersion})`); + savePackageJson(workspacePath(`${localPath}/package.json`), { + ...restPkgJson, + version: nextVersion, + }); + }); console.log('โœ… All done!'); }