diff --git a/packages/sdks/express/package.json b/packages/sdks/express/package.json index deb231d2..d2e57668 100644 --- a/packages/sdks/express/package.json +++ b/packages/sdks/express/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@openpanel/sdk": "1.0.0", + "@openpanel/sdk": "1.0.0-local", "request-ip": "^3.3.0" }, "peerDependencies": { diff --git a/packages/sdks/nextjs/package.json b/packages/sdks/nextjs/package.json index 2ab10b35..cfa3cf22 100644 --- a/packages/sdks/nextjs/package.json +++ b/packages/sdks/nextjs/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@openpanel/web": "1.0.0" + "@openpanel/web": "1.0.0-local" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0", diff --git a/packages/sdks/react-native/package.json b/packages/sdks/react-native/package.json index 14e612be..49de301d 100644 --- a/packages/sdks/react-native/package.json +++ b/packages/sdks/react-native/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@openpanel/sdk": "1.0.0" + "@openpanel/sdk": "1.0.0-local" }, "devDependencies": { "@openpanel/eslint-config": "workspace:*", diff --git a/packages/sdks/web/package.json b/packages/sdks/web/package.json index deb3de77..fb2711bb 100644 --- a/packages/sdks/web/package.json +++ b/packages/sdks/web/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@openpanel/sdk": "1.0.0" + "@openpanel/sdk": "1.0.0-local" }, "devDependencies": { "@openpanel/eslint-config": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6376eea0..2b9dca07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1168,8 +1168,8 @@ importers: packages/sdks/express: dependencies: '@openpanel/sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.0-local + version: link:../sdk express: specifier: ^3.0.0 || ^4.0.0 version: 4.18.2 @@ -1208,8 +1208,8 @@ importers: packages/sdks/nextjs: dependencies: '@openpanel/web': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.0-local + version: link:../web next: specifier: ^12.0.0 || ^13.0.0 || ^14.0.0 version: 14.1.0(react-dom@18.2.0)(react@18.2.0) @@ -1248,8 +1248,8 @@ importers: packages/sdks/react-native: dependencies: '@openpanel/sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.0-local + version: link:../sdk expo-application: specifier: ^5 version: 5.3.1(expo@50.0.7) @@ -1315,8 +1315,8 @@ importers: packages/sdks/web: dependencies: '@openpanel/sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.0-local + version: link:../sdk devDependencies: '@openpanel/eslint-config': specifier: workspace:* diff --git a/tooling/publish/publish.ts b/tooling/publish/publish.ts index b1e9afeb..43c97ddd 100644 --- a/tooling/publish/publish.ts +++ b/tooling/publish/publish.ts @@ -1,257 +1,296 @@ -import { exec, execSync } from 'node:child_process'; +import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import arg from 'arg'; import type { ReleaseType } from 'semver'; import semver, { RELEASE_TYPES } from 'semver'; +// Types +interface PackageJson { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + [key: string]: any; +} + +interface PackageInfo extends PackageJson { + nextVersion: string; + localPath: string; +} + +interface PublishConfig { + registry: string; + test: boolean; +} + +// Utility functions const workspacePath = (relativePath: string) => path.resolve(__dirname, '../../', relativePath); -function savePackageJson(absPath: string, data: Record) { +const savePackageJson = (absPath: string, data: PackageJson) => { fs.writeFileSync(absPath, JSON.stringify(data, null, 2), 'utf-8'); -} - -function exit(message: string, error?: unknown) { - 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); -} - -function checkUncommittedChanges() { - try { - const changedFiles = execSync('git ls-files --exclude-standard --others') - .toString() - .trim(); - if (changedFiles !== '') { - throw new Error('Uncommitted changes'); - } - execSync('git diff HEAD --exit-code'); - console.log('โœ… No uncommitted changes'); - } catch (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; +const exit = (message: string, error?: unknown) => { + console.error(`\n\nโŒ ${message}`); + if (error) console.error('Error:', error); + process.exit(1); +}; + +const checkUncommittedChanges = () => { + try { + const uncommittedFiles = execSync('git status --porcelain') + .toString() + .trim(); + if (uncommittedFiles) throw new Error('Uncommitted changes detected'); + console.log('โœ… No uncommitted changes'); + } catch (error) { + exit('Uncommitted changes', error); + } +}; + +const getNextVersion = (version: string, type: ReleaseType): string => { + const nextVersion = semver.inc(version, type); + if (!nextVersion) throw new Error('Invalid version'); + return type.startsWith('pre') + ? nextVersion.replace(/-.*$/, '-beta') + : nextVersion; +}; + +// Core functions +const loadPackages = ( + releaseType: ReleaseType +): Record => { + const sdksPath = workspacePath('./packages/sdks'); + const sdks = fs + .readdirSync(sdksPath, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith('.')) + .map((dirent) => dirent.name); + + return Object.fromEntries( + sdks.map((sdk) => { + const pkgPath = path.join(sdksPath, sdk, 'package.json'); + const pkgJson = JSON.parse( + fs.readFileSync(pkgPath, 'utf-8') + ) as PackageJson; + return [ + pkgJson.name, + { + ...pkgJson, + version: pkgJson.version.replace(/-local$/, ''), + nextVersion: getNextVersion(pkgJson.version, releaseType), + localPath: `./packages/sdks/${sdk}`, + }, + ]; + }) + ); +}; + +const findDependents = ( + packages: Record, + targetName: string +): string[] => { + const dependents = new Set([targetName]); + const findDeps = (name: string) => { + Object.entries(packages).forEach(([pkgName, pkg]) => { + if (pkg.dependencies?.[name] && !dependents.has(pkgName)) { + dependents.add(pkgName); + findDeps(pkgName); + } + }); + }; + findDeps(targetName); + return Array.from(dependents); +}; + +const updatePackageJsonForRelease = ( + packages: Record, + name: string +): void => { + const { nextVersion, localPath, ...restPkgJson } = packages[name]!; + const newPkgJson: PackageJson = { + ...restPkgJson, + private: false, + type: 'module', + main: './dist/index.js', + module: './dist/index.mjs', + types: './dist/index.d.ts', + files: ['dist'], + exports: { + '.': { + import: './dist/index.js', + require: './dist/index.cjs', + types: './dist/index.d.ts', + }, + ...(name === '@openpanel/nextjs' + ? { + './server': { + import: './dist/server.js', + require: './dist/server.cjs', + types: './dist/server.d.ts', + }, + } + : {}), + }, + version: nextVersion, + dependencies: Object.fromEntries( + Object.entries(restPkgJson.dependencies || {}).map( + ([depName, depVersion]) => [ + depName, + packages[depName]?.nextVersion || depVersion, + ] + ) + ), + }; + + if (name === '@openpanel/nextjs') delete newPkgJson.type; + + savePackageJson(workspacePath(`${localPath}/package.json`), newPkgJson); + packages[name]!.dependencies = newPkgJson.dependencies; +}; + +const buildPackages = ( + packages: Record, + dependents: string[] +): void => { + const versionEnvs = dependents.map((dep) => { + const envName = dep + .replace(/@openpanel\//g, '') + .toUpperCase() + .replace(/[/-]/g, '_'); + return `--env.${envName}_VERSION=${packages[dep]!.nextVersion}`; + }); + + dependents.forEach((dep) => { + console.log(`๐Ÿ”จ Building ${dep}`); + execSync(`pnpm build ${versionEnvs.join(' ')}`, { + cwd: workspacePath(packages[dep]!.localPath), + }); + }); +}; + +const publishPackages = ( + packages: Record, + dependents: string[], + config: PublishConfig +): void => { + if (config.test) { + execSync('rm -rf ~/.local/share/verdaccio/storage/@openpanel'); + } + + dependents.forEach((dep) => { + console.log(`๐Ÿš€ Publishing ${dep} to ${config.registry}`); + execSync(`npm publish --access=public --registry ${config.registry}`, { + cwd: workspacePath(packages[dep]!.localPath), + }); + + if (dep === '@openpanel/web') { + execSync( + `cp ${workspacePath('packages/sdks/web/dist/src/tracker.global.js')} ${workspacePath('./apps/public/public/op1.js')}` + ); + } + }); +}; + +const restoreAndUpdateLocal = ( + packages: Record, + dependents: string[] +): void => { + const filesToRestore = dependents + .map((dep) => workspacePath(packages[dep]!.localPath)) + .join(' '); + execSync(`git checkout ${filesToRestore}`); + + dependents.forEach((dep) => { + const { nextVersion, localPath, ...restPkgJson } = packages[dep]!; + console.log(`๐Ÿš€ Updating ${dep} (${nextVersion}-local)`); + + const updatedPkgJson: PackageJson = { + ...restPkgJson, + version: `${nextVersion}-local`, + dependencies: Object.fromEntries( + Object.entries(restPkgJson.dependencies || {}).map( + ([depName, depVersion]) => [ + depName, + dependents.includes(depName) + ? `${packages[depName]!.nextVersion}-local` + : packages[depName] + ? `${packages[depName]!.version}-local` + : depVersion, + ] + ) + ), + devDependencies: Object.fromEntries( + Object.entries(restPkgJson.devDependencies || {}).map( + ([depName, depVersion]) => [ + depName, + dependents.includes(depName) + ? `${packages[depName]!.nextVersion}-local` + : packages[depName] + ? `${packages[depName]!.version}-local` + : depVersion, + ] + ) + ), + }; + + savePackageJson(workspacePath(`${localPath}/package.json`), updatedPkgJson); + }); }; function main() { + // Main execution const args = arg({ '--name': String, '--publish': Boolean, '--test': Boolean, '--skip-git': Boolean, - // Semver - '--type': String, // major, minor, patch, premajor, preminor, prepatch, or prerelease + '--type': String, }); if (!args['--skip-git']) { checkUncommittedChanges(); } - const pkgName = args['--name']; - const type = args['--type'] as ReleaseType; - const test = args['--test']; - const publish = args['--publish']; - const packages: Record = {}; - const registry = test - ? 'http://localhost:4873' - : 'https://registry.npmjs.org'; + if (!args['--name']) { + return exit('--name is required'); + } - if (!RELEASE_TYPES.includes(type)) { + if (!RELEASE_TYPES.includes(args['--type'] as ReleaseType)) { return exit( `Invalid release type. Valid types are: ${RELEASE_TYPES.join(', ')}` ); } - 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]; + const packages = loadPackages(args['--type'] as ReleaseType); + const target = packages[args['--name']]; 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); + const dependents = findDependents(packages, target.name); - function updatePackageJsonForRelease(name: string) { - const { nextVersion, localPath, ...restPkgJson } = packages[name]!; - const newPkgJson = JSON.parse( - JSON.stringify({ - ...restPkgJson, - private: false, - type: 'module', - main: './dist/index.js', - module: './dist/index.mjs', - types: './dist/index.d.ts', - files: ['dist'], - exports: { - '.': { - import: './dist/index.js', - require: './dist/index.cjs', - types: './dist/index.d.ts', - }, - ...(name === '@openpanel/nextjs' - ? { - './server': { - import: './dist/server.js', - require: './dist/server.cjs', - types: './dist/server.d.ts', - }, - } - : {}), - }, - version: nextVersion, - dependencies: Object.entries(restPkgJson.dependencies || {}).reduce( - (acc, [depName, depVersion]) => { - const dep = packages[depName]; - if (!dep) { - return { ...acc, [depName]: depVersion }; - } - - return { - ...acc, - [depName]: dependents.includes(depName) - ? dep.nextVersion - : depVersion, - }; - }, - {} - ), - }) - ) as IPackageJson; - - if (name === '@openpanel/nextjs') { - delete newPkgJson.type; - } - - savePackageJson(workspacePath(`${localPath}/package.json`), newPkgJson); - packages[name]!.dependencies = newPkgJson.dependencies; - } - - dependents.forEach((dependent) => { + dependents.forEach((dep) => { console.log( - `๐Ÿ“ฆ ${dependent} ยท Old Version: ${packages[dependent]?.version} ยท Next Version: ${packages[dependent]?.nextVersion}` + `๐Ÿ“ฆ ${dep} ยท Old Version: ${packages[dep]!.version} ยท Next Version: ${packages[dep]!.nextVersion}` ); - updatePackageJsonForRelease(dependent); + updatePackageJsonForRelease(packages, dep); }); - const versionEnvs = dependents.map((dependent) => { - const { nextVersion } = packages[dependent]!; - const env = dependent - .replace(/@openpanel\//g, '') - .toUpperCase() - .replace(/\//g, '_') - .replace('-', '_'); - return `--env.${env}_VERSION=${nextVersion}`; - }); + buildPackages(packages, dependents); - console.log('versionEnvs', versionEnvs); + if (args['--publish']) { + const config: PublishConfig = { + registry: args['--test'] + ? 'http://localhost:4873' + : 'https://registry.npmjs.org', + test: args['--test'] || false, + }; - dependents.forEach((dependent) => { - console.log(`๐Ÿ”จ Building ${dependent}`); - execSync(`pnpm build ${versionEnvs.join(' ')}`, { - cwd: workspacePath(packages[dependent]!.localPath), - }); - }); - - // Publish - if (publish) { - if (test) { - execSync('rm -rf ~/.local/share/verdaccio/storage/@openpanel'); - } - dependents.forEach((dependent) => { - console.log(`๐Ÿš€ Publishing ${dependent} to ${registry}`); - execSync(`npm publish --access=public --registry ${registry}`, { - cwd: workspacePath(packages[dependent]!.localPath), - }); - - if (dependent === '@openpanel/web') { - execSync( - `cp ${workspacePath('packages/sdks/web/dist/src/tracker.global.js')} ${workspacePath('./apps/public/public/op1.js')}` - ); - } - }); - - // 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, - }); - }); + publishPackages(packages, dependents, config); + restoreAndUpdateLocal(packages, dependents); } console.log('โœ… All done!');