From e980ce82183a79d0bb166acd895a289230863df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Levasseur?= Date: Fri, 12 Apr 2024 10:40:33 -0400 Subject: [PATCH] feat(depsynky): CLI to synchronize dependencies accross a pnpm mono-repo (#237) --- .github/workflows/depsynky.yml | 35 ++ depsynky/bin.js | 2 + depsynky/package.json | 36 ++ depsynky/pnpm-lock.yaml | 686 ++++++++++++++++++++++++ depsynky/readme.md | 9 + depsynky/src/commands/bump-versions.ts | 85 +++ depsynky/src/commands/check-versions.ts | 49 ++ depsynky/src/commands/list-versions.ts | 21 + depsynky/src/commands/sync-versions.ts | 48 ++ depsynky/src/config.ts | 22 + depsynky/src/errors.ts | 1 + depsynky/src/index.ts | 47 ++ depsynky/src/utils/index.ts | 4 + depsynky/src/utils/logging.ts | 14 + depsynky/src/utils/objects.ts | 11 + depsynky/src/utils/pkgjson.ts | 37 ++ depsynky/src/utils/pnpm.ts | 50 ++ depsynky/tsconfig.json | 32 ++ 18 files changed, 1189 insertions(+) create mode 100644 .github/workflows/depsynky.yml create mode 100644 depsynky/bin.js create mode 100644 depsynky/package.json create mode 100644 depsynky/pnpm-lock.yaml create mode 100644 depsynky/readme.md create mode 100644 depsynky/src/commands/bump-versions.ts create mode 100644 depsynky/src/commands/check-versions.ts create mode 100644 depsynky/src/commands/list-versions.ts create mode 100644 depsynky/src/commands/sync-versions.ts create mode 100644 depsynky/src/config.ts create mode 100644 depsynky/src/errors.ts create mode 100644 depsynky/src/index.ts create mode 100644 depsynky/src/utils/index.ts create mode 100644 depsynky/src/utils/logging.ts create mode 100644 depsynky/src/utils/objects.ts create mode 100644 depsynky/src/utils/pkgjson.ts create mode 100644 depsynky/src/utils/pnpm.ts create mode 100644 depsynky/tsconfig.json diff --git a/.github/workflows/depsynky.yml b/.github/workflows/depsynky.yml new file mode 100644 index 00000000..cf4ca46f --- /dev/null +++ b/.github/workflows/depsynky.yml @@ -0,0 +1,35 @@ +name: Depsynky + +on: + push: + branches: + - master + + pull_request: + paths: + - 'depsynky/**' + + workflow_dispatch: {} + +defaults: + run: + working-directory: ./depsynky + +jobs: + depsynky: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3.0.0 + with: + version: 8 + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm type:check + - name: Publish + if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' + uses: botpress/gh-actions/publish-if-not-exists@master + with: + path: './depsynky' + token: '${{ secrets.NPM_ACCESS_TOKEN }}' diff --git a/depsynky/bin.js b/depsynky/bin.js new file mode 100644 index 00000000..7410d7b8 --- /dev/null +++ b/depsynky/bin.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./dist/index.js') diff --git a/depsynky/package.json b/depsynky/package.json new file mode 100644 index 00000000..98822b19 --- /dev/null +++ b/depsynky/package.json @@ -0,0 +1,36 @@ +{ + "name": "@bpinternal/depsynky", + "version": "0.0.1", + "description": "CLI to synchronize dependencies accross a pnpm mono-repo", + "main": "dist/index.js", + "scripts": { + "type:check": "tsc --noEmit", + "build": "esbuild --bundle --platform=node --target=node18 --outdir=dist src/index.ts", + "dev": "ts-node -T src/index.ts", + "start": "node dist/index.js" + }, + "bin": { + "depsynky": "./bin.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@bpinternal/yargs-extra": "^0.0.3", + "chalk": "^4.1.2", + "glob": "^9.3.4", + "prettier": "^2.8.1", + "prompts": "^2.4.2", + "semver": "^7.5.1", + "yaml": "^2.3.1" + }, + "devDependencies": { + "@types/node": "^18.11.18", + "@types/prettier": "^2.7.3", + "@types/prompts": "^2.0.14", + "@types/semver": "^7.3.11", + "esbuild": "^0.20.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" + } +} diff --git a/depsynky/pnpm-lock.yaml b/depsynky/pnpm-lock.yaml new file mode 100644 index 00000000..c28d884f --- /dev/null +++ b/depsynky/pnpm-lock.yaml @@ -0,0 +1,686 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@bpinternal/yargs-extra': + specifier: ^0.0.3 + version: 0.0.3 + chalk: + specifier: ^4.1.2 + version: 4.1.2 + glob: + specifier: ^9.3.4 + version: 9.3.4 + prettier: + specifier: ^2.8.1 + version: 2.8.1 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + semver: + specifier: ^7.5.1 + version: 7.5.1 + yaml: + specifier: ^2.3.1 + version: 2.3.1 + +devDependencies: + '@types/node': + specifier: ^18.11.18 + version: 18.11.18 + '@types/prettier': + specifier: ^2.7.3 + version: 2.7.3 + '@types/prompts': + specifier: ^2.0.14 + version: 2.0.14 + '@types/semver': + specifier: ^7.3.11 + version: 7.3.11 + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@18.11.18)(typescript@4.9.4) + typescript: + specifier: ^4.9.4 + version: 4.9.4 + +packages: + + /@bpinternal/yargs-extra@0.0.3: + resolution: {integrity: sha512-e/unlq0LX4CJUv1jGOv1UgwB/h2M0NCXnwD4lEw496GpkQikO668RS+BBlRhkqdGfZmvKDkXZZ96xJCn+i6Ymg==} + dependencies: + '@types/yargs': 17.0.32 + decamelize: 5.0.1 + json-schema: 0.4.0 + lodash: 4.17.21 + yargs: 17.7.2 + yn: 4.0.0 + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/node@18.11.18: + resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + dev: true + + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + dev: true + + /@types/prompts@2.0.14: + resolution: {integrity: sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==} + dependencies: + '@types/node': 18.11.18 + dev: true + + /@types/semver@7.3.11: + resolution: {integrity: sha512-R9HhjC4aKx3jL0FLwU7x6qMTysTvLh7jesRslXmxgCOXZwyh5dsnmrPQQToMyess8D4U+8G9x9mBFZoC/1o/Tw==} + dev: true + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: false + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: false + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /glob@9.3.4: + resolution: {integrity: sha512-qaSc49hojMOv1EPM4EuyITjDSgSKI0rthoHnvE81tcOi1SCVndHko7auqxdQ14eiQG2NDBJBE86+2xIrbIvrbA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.10.2 + dev: false + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + dev: false + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: false + + /prettier@2.8.1: + resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: false + + /ts-node@10.9.1(@types/node@18.11.18)(typescript@4.9.4): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.11.18 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /typescript@4.9.4: + resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: false + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yn@4.0.0: + resolution: {integrity: sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==} + engines: {node: '>=10'} + dev: false diff --git a/depsynky/readme.md b/depsynky/readme.md new file mode 100644 index 00000000..b6af2b13 --- /dev/null +++ b/depsynky/readme.md @@ -0,0 +1,9 @@ +# DepSynky + +CLI to synchronize dependencies accross a pnpm mono-repo + +## Disclaimer ⚠️ + +This package is published under the `@bpinternal` organization. All packages of this organization are meant to be used by the [Botpress](https://github.com/botpress/botpress) team internally and are not meant for our community. However, these packages were still left intentionally public for an important reason : We Love Open-Source. Therefore, if you wish to install this package feel absolutly free to do it. We strongly recomand that you tag your versions properly. + +The Botpress Engineering team. diff --git a/depsynky/src/commands/bump-versions.ts b/depsynky/src/commands/bump-versions.ts new file mode 100644 index 00000000..2e258e3e --- /dev/null +++ b/depsynky/src/commands/bump-versions.ts @@ -0,0 +1,85 @@ +import { YargsConfig } from '@bpinternal/yargs-extra' +import * as prompts from 'prompts' +import * as semver from 'semver' +import * as config from '../config' +import * as errors from '../errors' +import * as utils from '../utils' +import { listVersions } from './list-versions' +import { syncVersions } from './sync-versions' + +const { logger } = utils.logging + +type VersionJump = 'major' | 'minor' | 'patch' | 'none' + +const promptJump = async (pkgName: string, pkgVersion: string): Promise => { + const { jump: promptedJump } = await prompts.prompt({ + type: 'select', + name: 'jump', + message: `Bump ${pkgName} version from ${pkgVersion}`, + choices: [ + { title: 'Patch', value: 'patch' }, + { title: 'Minor', value: 'minor' }, + { title: 'Major', value: 'major' }, + { title: 'None', value: 'none' } + ] + }) + return promptedJump +} + +const promptPackage = async (publicPkgs: string[]): Promise => { + if (publicPkgs.length === 0) { + throw new errors.DepSynkyError('No public packages found') + } + + const { pkgName } = await prompts.prompt({ + type: 'select', + name: 'pkgName', + message: 'Select a package to bump', + choices: publicPkgs.map((name) => ({ title: name, value: name })) + }) + + if (!pkgName) { + throw new errors.DepSynkyError('No package selected') + } + + return pkgName +} + +export const bumpVersion = async (argv: YargsConfig & { pkgName?: string }) => { + let pkgName = argv.pkgName + if (!pkgName) { + const publicPkgs = utils.pnpm.listPublicPackages(argv.rootDir) + pkgName = await promptPackage(publicPkgs) + } + + const { dependency, dependents } = utils.pnpm.findReferences(argv.rootDir, pkgName) + const targetPackages = [dependency, ...dependents] + + const currentVersions = utils.pnpm.versions(targetPackages) + const targetVersions = { ...currentVersions } + + for (const { path: pkgPath, content } of targetPackages) { + if (content.private) { + continue // no need to bump the version of private packages + } + + const jump = await promptJump(content.name, content.version) + if (jump === 'none') { + continue + } + + const next = semver.inc(content.version, jump) + if (!next) { + throw new errors.DepSynkyError(`Invalid version jump: ${jump}`) + } + + targetVersions[content.name] = next + utils.pkgjson.update(pkgPath, { version: next }) + } + + listVersions(argv) + if (argv.sync) { + logger.info('Syncing versions...') + syncVersions(argv, { targetVersions }) + } +} diff --git a/depsynky/src/commands/check-versions.ts b/depsynky/src/commands/check-versions.ts new file mode 100644 index 00000000..68b1b607 --- /dev/null +++ b/depsynky/src/commands/check-versions.ts @@ -0,0 +1,49 @@ +import { YargsConfig } from '@bpinternal/yargs-extra' +import * as config from '../config' +import * as errors from '../errors' +import * as utils from '../utils' + +const { logger } = utils.logging + +export type CheckVersionsOpts = { + targetVersions: Record +} + +const LOCAL_VERSION = 'workspace:*' + +const checker = + (pkg: utils.pkgjson.PackageJson) => (current: Record, target: Record) => { + for (const [name, version] of utils.objects.entries(target)) { + const currentVersion = current[name] + if (!currentVersion) { + continue + } + const isLocal = currentVersion === LOCAL_VERSION + const isPublic = !pkg.private + if (isPublic && isLocal) { + throw new errors.DepSynkyError( + `Package ${pkg.name} is public and cannot depend on local package ${name}. To keep reference on local package, make ${pkg.name} private.` + ) + } + if (!isLocal && currentVersion !== version) { + throw new errors.DepSynkyError( + `Dependency ${name} is out of sync in ${pkg.name}: ${currentVersion} != ${version}` + ) + } + } + } + +export const checkVersions = (argv: YargsConfig, opts: Partial = {}) => { + const allPackages = utils.pnpm.searchWorkspaces(argv.rootDir) + const targetVersions = opts.targetVersions ?? utils.pnpm.versions(allPackages) + + for (const { path: pkgPath, content } of allPackages) { + const { dependencies, devDependencies } = utils.pkgjson.read(pkgPath) + + const check = checker(content) + dependencies && check(dependencies, targetVersions) + devDependencies && check(devDependencies, targetVersions) + } + + logger.info('All versions are in sync') +} diff --git a/depsynky/src/commands/list-versions.ts b/depsynky/src/commands/list-versions.ts new file mode 100644 index 00000000..ff4b7edf --- /dev/null +++ b/depsynky/src/commands/list-versions.ts @@ -0,0 +1,21 @@ +import { YargsConfig } from '@bpinternal/yargs-extra' +import * as util from 'util' +import * as config from '../config' +import * as utils from '../utils' + +const { logger } = utils.logging + +export const listVersions = (argv: YargsConfig) => { + const allPackages = utils.pnpm.searchWorkspaces(argv.rootDir) + + const versions: Record = {} + + for (const { content } of allPackages) { + if (content.private) { + continue + } + versions[content.name] = content.version + } + + logger.info('versions:', util.inspect(versions, { depth: Infinity, colors: true })) +} diff --git a/depsynky/src/commands/sync-versions.ts b/depsynky/src/commands/sync-versions.ts new file mode 100644 index 00000000..195f83a0 --- /dev/null +++ b/depsynky/src/commands/sync-versions.ts @@ -0,0 +1,48 @@ +import { YargsConfig } from '@bpinternal/yargs-extra' +import * as config from '../config' +import * as utils from '../utils' +import { searchWorkspaces } from '../utils/pnpm' + +export type SyncVersionsOpts = { + targetVersions: Record +} + +const LOCAL_VERSION = 'workspace:*' +const updater = + (pkg: utils.pkgjson.PackageJson) => (current: Record, target: Record) => { + for (const [name, version] of utils.objects.entries(target)) { + const currentVersion = current[name] + if (!currentVersion) { + continue + } + const isLocal = currentVersion === LOCAL_VERSION + const isPublic = !pkg.private + if (!isLocal) { + current[name] = version + continue + } + if (isPublic && isLocal) { + utils.logging.logger.warn( + `Package ${pkg.name} is public and cannot depend on local package ${name}. To keep reference on local package, make ${pkg.name} private.` + ) + current[name] = version + continue + } + } + return current + } + +export const syncVersions = (argv: YargsConfig, opts: Partial = {}) => { + const allPackages = searchWorkspaces(argv.rootDir) + const targetVersions = opts.targetVersions ?? utils.pnpm.versions(allPackages) + + for (const { path: pkgPath, content } of allPackages) { + const { dependencies, devDependencies } = utils.pkgjson.read(pkgPath) + + const update = updater(content) + const updatedDeps = dependencies && update(dependencies, targetVersions) + const updatedDevDeps = devDependencies && update(devDependencies, targetVersions) + + utils.pkgjson.update(pkgPath, { dependencies: updatedDeps, devDependencies: updatedDevDeps }) + } +} diff --git a/depsynky/src/config.ts b/depsynky/src/config.ts new file mode 100644 index 00000000..5bded44c --- /dev/null +++ b/depsynky/src/config.ts @@ -0,0 +1,22 @@ +import { YargsSchema } from '@bpinternal/yargs-extra' + +const defaultOptions = { + rootDir: { + type: 'string', + default: process.cwd(), + }, +} satisfies YargsSchema + +export const bumpSchema = { + ...defaultOptions, + sync: { + type: 'boolean', + default: true, + }, +} satisfies YargsSchema + +export const syncSchema = defaultOptions satisfies YargsSchema + +export const checkSchema = defaultOptions satisfies YargsSchema + +export const listSchema = defaultOptions satisfies YargsSchema diff --git a/depsynky/src/errors.ts b/depsynky/src/errors.ts new file mode 100644 index 00000000..b6250365 --- /dev/null +++ b/depsynky/src/errors.ts @@ -0,0 +1 @@ +export class DepSynkyError extends Error {} diff --git a/depsynky/src/index.ts b/depsynky/src/index.ts new file mode 100644 index 00000000..6d35e93d --- /dev/null +++ b/depsynky/src/index.ts @@ -0,0 +1,47 @@ +import yargs from '@bpinternal/yargs-extra' +import { bumpVersion } from './commands/bump-versions' +import { checkVersions } from './commands/check-versions' +import { listVersions } from './commands/list-versions' +import { syncVersions } from './commands/sync-versions' +import * as config from './config' +import * as errors from './errors' +import { logger } from './utils/logging' + +const onError = (thrown: unknown): never => { + const err: Error = thrown instanceof Error ? thrown : new Error(`${thrown}`) + const hideStack: boolean = err instanceof errors.DepSynkyError || !err.stack + const message = hideStack ? err.message : `${err.message}\n${err.stack}` + logger.error(message) + process.exit(1) +} + +const yargsFail = (msg: string, err: Error) => { + err ? onError(err) : onError(msg) +} + +process.on('unhandledRejection', onError) +process.on('uncaughtException', onError) + +void yargs + .command( + 'bump [package]', + 'Bump version of a package', + () => yargs.positional('package', { type: 'string', demandOption: false }).options(config.bumpSchema), + (argv) => { + void bumpVersion(argv) + } + ) + .command('sync', 'Sync versions of all packages', config.syncSchema, (argv) => { + void syncVersions(argv) + }) + .command('check', 'Check if all packages have the target version', config.checkSchema, (argv) => { + void checkVersions(argv) + }) + .command('ls', 'List version of all public packages', config.checkSchema, (argv) => { + void listVersions(argv) + }) + .strict() + .help() + .fail(yargsFail) + .demandCommand(1) + .parse() diff --git a/depsynky/src/utils/index.ts b/depsynky/src/utils/index.ts new file mode 100644 index 00000000..b27dbb88 --- /dev/null +++ b/depsynky/src/utils/index.ts @@ -0,0 +1,4 @@ +export * as pkgjson from './pkgjson' +export * as logging from './logging' +export * as pnpm from './pnpm' +export * as objects from './objects' diff --git a/depsynky/src/utils/logging.ts b/depsynky/src/utils/logging.ts new file mode 100644 index 00000000..a8b4318d --- /dev/null +++ b/depsynky/src/utils/logging.ts @@ -0,0 +1,14 @@ +import chalk from 'chalk' + +export type Logger = { + info: (...messages: string[]) => void + warn: (...messages: string[]) => void + error: (...messages: string[]) => void +} + +const { log } = console +export const logger: Logger = { + info: (...messages: string[]) => log(chalk.green('info'), ...messages), + warn: (...messages: string[]) => log(chalk.yellow('warn'), ...messages), + error: (...messages: string[]) => log(chalk.red('error'), ...messages), +} diff --git a/depsynky/src/utils/objects.ts b/depsynky/src/utils/objects.ts new file mode 100644 index 00000000..929698b0 --- /dev/null +++ b/depsynky/src/utils/objects.ts @@ -0,0 +1,11 @@ +export const keys = (obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[] + +export const entries = (obj: Record): [K, V][] => Object.entries(obj) as [K, V][] + +export const fromEntries = (pairs: [K, V][]): Record => { + const obj = {} as Record + for (const [key, value] of pairs) { + obj[key] = value + } + return obj +} diff --git a/depsynky/src/utils/pkgjson.ts b/depsynky/src/utils/pkgjson.ts new file mode 100644 index 00000000..ae493425 --- /dev/null +++ b/depsynky/src/utils/pkgjson.ts @@ -0,0 +1,37 @@ +import * as fs from 'fs' +import * as prettier from 'prettier' +import * as objects from './objects' + +export type PackageJson = { + name: string + version: string + private?: boolean + dependencies?: Record + devDependencies?: Record +} + +export const read = (filePath: string): PackageJson => { + const strContent = fs.readFileSync(filePath, 'utf-8') + const content = JSON.parse(strContent) + return content +} + +export const write = (filePath: string, content: PackageJson) => { + let strContent = JSON.stringify(content, null, 2) + strContent = prettier.format(strContent, { parser: 'json' }) + fs.writeFileSync(filePath, strContent) +} + +export const update = (filePath: string, content: Partial) => { + const currentPackage = read(filePath) + + // this preserves the order of the keys + const newPackage = objects.keys(currentPackage).reduce((acc, key) => { + if (key in content) { + return { ...acc, [key]: content[key] } + } + return acc + }, currentPackage) + + write(filePath, newPackage) +} diff --git a/depsynky/src/utils/pnpm.ts b/depsynky/src/utils/pnpm.ts new file mode 100644 index 00000000..26333561 --- /dev/null +++ b/depsynky/src/utils/pnpm.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs' +import * as glob from 'glob' +import * as pathlib from 'path' +import * as yaml from 'yaml' +import * as errors from '../errors' +import * as objects from './objects' +import * as pkgjson from './pkgjson' + +const abs = (rootDir: string) => (p: string) => pathlib.resolve(rootDir, p) + +export type PnpmWorkspace = { + path: string + content: pkgjson.PackageJson +} + +const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml' + +export const searchWorkspaces = (rootDir: string): PnpmWorkspace[] => { + const pnpmWorkspacesFile = pathlib.join(rootDir, PNPM_WORKSPACE_FILE) + if (!fs.existsSync(pnpmWorkspacesFile)) { + throw new errors.DepSynkyError(`Could not find ${PNPM_WORKSPACE_FILE} at "${rootDir}"`) + } + const pnpmWorkspacesContent = fs.readFileSync(pnpmWorkspacesFile, 'utf-8') + const pnpmWorkspaces: string[] = yaml.parse(pnpmWorkspacesContent).packages + const globMatches = pnpmWorkspaces.flatMap((ws) => glob.globSync(ws, { absolute: false, cwd: rootDir })) + const absGlobMatches = globMatches.map(abs(rootDir)) + const packageJsonPaths = absGlobMatches.map((p) => pathlib.join(p, 'package.json')) + const actualPackages = packageJsonPaths.filter(fs.existsSync) + const absolutePaths = actualPackages.map(abs(rootDir)) + return absolutePaths.map((p) => ({ path: p, content: pkgjson.read(p) })) +} + +export const findReferences = (rootDir: string, pkgName: string) => { + const workspaces = searchWorkspaces(rootDir) + const dependency = workspaces.find((w) => w.content.name === pkgName) + if (!dependency) { + throw new errors.DepSynkyError(`Could not find package "${pkgName}"`) + } + const dependents = workspaces.filter((w) => w.content.dependencies?.[pkgName] || w.content.devDependencies?.[pkgName]) + return { dependency, dependents } +} + +export const versions = (workspaces: PnpmWorkspace[]): Record => { + return objects.fromEntries(workspaces.map(({ content: { name, version } }) => [name, version])) +} + +export const listPublicPackages = (rootDir: string): string[] => { + const workspaces = searchWorkspaces(rootDir) + return workspaces.filter((w) => !w.content.private).map((w) => w.content.name) +} diff --git a/depsynky/tsconfig.json b/depsynky/tsconfig.json new file mode 100644 index 00000000..cc03fa18 --- /dev/null +++ b/depsynky/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "lib": ["es2022"], + "module": "commonjs", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "composite": true, + "incremental": true, + "exactOptionalPropertyTypes": false, + "resolveJsonModule": true, + "noPropertyAccessFromIndexSignature": false, + "noUnusedLocals": false, + "noImplicitOverride": false, + "checkJs": false, + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "typeRoots": ["node_modules/@types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}