From a93bda31b22c82c72f784ba9cc9695346bee109b Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 4 Dec 2025 19:00:31 +0100 Subject: [PATCH 01/35] refactor: remove unused test utilities and related tests This commit deletes the `_shared.ts` file and several test files that were no longer needed, including `changelog.authors.test.ts`, `changelog.test.ts`, `git.test.ts`, and `commits.test.ts`. The removed tests were primarily focused on creating mock data and testing functionalities that are either deprecated or have been refactored in other parts of the codebase. This cleanup helps streamline the test suite and reduces maintenance overhead. --- .vscode/settings.json | 3 + package.json | 5 + pnpm-lock.yaml | 543 ++++++++++++++++++++++++++++ src/core/changelog.ts | 375 ------------------- src/core/git.ts | 434 ---------------------- src/core/github.ts | 297 --------------- src/core/prompts.ts | 92 ----- src/core/workspace.ts | 168 --------- src/errors.ts | 26 ++ src/index.ts | 147 +++++++- src/publish.ts | 8 - src/release.ts | 440 ---------------------- src/services/git.service.ts | 117 ++++++ src/services/github.service.ts | 269 ++++++++++++++ src/services/workspace.service.ts | 129 +++++++ src/shared/options.ts | 146 -------- src/shared/utils.ts | 135 ------- src/verify.ts | 146 -------- src/versioning/commits.ts | 297 --------------- src/versioning/package.ts | 198 ---------- src/versioning/version.ts | 426 ---------------------- test/_shared.ts | 126 ------- test/core/changelog.authors.test.ts | 41 --- test/core/changelog.test.ts | 505 -------------------------- test/core/git.test.ts | 311 ---------------- test/versioning/commits.test.ts | 83 ----- tsconfig.json | 7 +- 27 files changed, 1232 insertions(+), 4242 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/core/changelog.ts delete mode 100644 src/core/git.ts delete mode 100644 src/core/github.ts delete mode 100644 src/core/prompts.ts delete mode 100644 src/core/workspace.ts create mode 100644 src/errors.ts delete mode 100644 src/publish.ts delete mode 100644 src/release.ts create mode 100644 src/services/git.service.ts create mode 100644 src/services/github.service.ts create mode 100644 src/services/workspace.service.ts delete mode 100644 src/shared/options.ts delete mode 100644 src/shared/utils.ts delete mode 100644 src/verify.ts delete mode 100644 src/versioning/commits.ts delete mode 100644 src/versioning/package.ts delete mode 100644 src/versioning/version.ts delete mode 100644 test/_shared.ts delete mode 100644 test/core/changelog.authors.test.ts delete mode 100644 test/core/changelog.test.ts delete mode 100644 test/core/git.test.ts delete mode 100644 test/versioning/commits.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/package.json b/package.json index f1c6776..c714e15 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@effect/platform": "0.93.3", + "@effect/platform-node": "0.101.1", "@luxass/utils": "2.7.2", "commit-parser": "1.3.0", + "effect": "3.19.6", "farver": "1.0.0-beta.1", "mri": "1.2.0", "prompts": "2.4.2", @@ -45,6 +48,8 @@ "tinyexec": "1.0.2" }, "devDependencies": { + "@effect/language-service": "^0.59.0", + "@effect/vitest": "0.27.0", "@luxass/eslint-config": "6.0.1", "@types/node": "22.18.12", "@types/prompts": "2.4.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd44192..f65340b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,24 @@ importers: .: dependencies: + '@effect/cli': + specifier: 0.72.1 + version: 0.72.1(@effect/platform@0.93.3(effect@3.19.6))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + '@effect/platform': + specifier: 0.93.3 + version: 0.93.3(effect@3.19.6) + '@effect/platform-node': + specifier: 0.101.1 + version: 0.101.1(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) '@luxass/utils': specifier: 2.7.2 version: 2.7.2 commit-parser: specifier: 1.3.0 version: 1.3.0 + effect: + specifier: 3.19.6 + version: 3.19.6 farver: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1 @@ -30,6 +42,12 @@ importers: specifier: 1.0.2 version: 1.0.2 devDependencies: + '@effect/language-service': + specifier: ^0.59.0 + version: 0.59.0 + '@effect/vitest': + specifier: 0.27.0 + version: 0.27.0(effect@3.19.6)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) '@luxass/eslint-config': specifier: 6.0.1 version: 6.0.1(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) @@ -93,6 +111,107 @@ packages: '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@effect/cli@0.72.1': + resolution: {integrity: sha512-HGDMGD23TxFW9tCSX6g+M2u0robikMA0mP0SqeJMj7FWXTdcQ+cQsJE99bxi9iu+5YID7MIrVJMs8TUwXUV2sg==} + peerDependencies: + '@effect/platform': ^0.93.0 + '@effect/printer': ^0.47.0 + '@effect/printer-ansi': ^0.47.0 + effect: ^3.19.3 + + '@effect/cluster@0.53.5': + resolution: {integrity: sha512-eXPHIizdG5sOqxmkpWyEM6YoqMRakguxRped3lYcEopKj4N1K4nE9JANbKHXqzxPjnAvit+r7zDSuwHUw9nfAw==} + peerDependencies: + '@effect/platform': ^0.93.3 + '@effect/rpc': ^0.72.2 + '@effect/sql': ^0.48.0 + '@effect/workflow': ^0.13.0 + effect: ^3.19.6 + + '@effect/experimental@0.57.4': + resolution: {integrity: sha512-1qbOGSugeDoCoXGdIsTBk53pKwk4aImmPIghhya1MBUSSExHi9YRn6u0PNQecungNyeKDz6A8k1+rZIbsMQy4g==} + peerDependencies: + '@effect/platform': ^0.93.3 + effect: ^3.19.5 + ioredis: ^5 + lmdb: ^3 + peerDependenciesMeta: + ioredis: + optional: true + lmdb: + optional: true + + '@effect/language-service@0.59.0': + resolution: {integrity: sha512-jFl5oqxPrWNoKL6u8m8i1xbJ9hT9YeuPY25tMiuB7YU1GjIHJHXwBbVspMsQ8JdDrP+vdXJj+yX/Tk6J2bbvuw==} + hasBin: true + + '@effect/platform-node-shared@0.54.0': + resolution: {integrity: sha512-prTgG3CXqmrxB4Rg6utfwCTqjlGwjAEvK7R4g3HzVdFpfFRum+FQBpGHUcjyz7EejkDtBY2MWJC3Wr1QKDPjPw==} + peerDependencies: + '@effect/cluster': ^0.53.0 + '@effect/platform': ^0.93.3 + '@effect/rpc': ^0.72.2 + '@effect/sql': ^0.48.0 + effect: ^3.19.5 + + '@effect/platform-node@0.101.1': + resolution: {integrity: sha512-uShujtpWU0VbdhRKhoo6tXzTG1xT0bnj8u5Q1BHpanwKPmzOhf4n0XLlMl5PaihH5Cp7xHuQlwgZlqHzhqSHzw==} + peerDependencies: + '@effect/cluster': ^0.53.4 + '@effect/platform': ^0.93.3 + '@effect/rpc': ^0.72.2 + '@effect/sql': ^0.48.0 + effect: ^3.19.6 + + '@effect/platform@0.93.3': + resolution: {integrity: sha512-s88zctkeXba24Mjy7MEFMuam1p5sXmsG7uQjPIDE6EiC+2IFUQd8976TtangiU0e8qu0SALpjIH1P1QyC7/1og==} + peerDependencies: + effect: ^3.19.4 + + '@effect/printer-ansi@0.47.0': + resolution: {integrity: sha512-tDEQ9XJpXDNYoWMQJHFRMxKGmEOu6z32x3Kb8YLOV5nkauEKnKmWNs7NBp8iio/pqoJbaSwqDwUg9jXVquxfWQ==} + peerDependencies: + '@effect/typeclass': ^0.38.0 + effect: ^3.19.0 + + '@effect/printer@0.47.0': + resolution: {integrity: sha512-VgR8e+YWWhMEAh9qFOjwiZ3OXluAbcVLIOtvp2S5di1nSrPOZxj78g8LE77JSvyfp5y5bS2gmFW+G7xD5uU+2Q==} + peerDependencies: + '@effect/typeclass': ^0.38.0 + effect: ^3.19.0 + + '@effect/rpc@0.72.2': + resolution: {integrity: sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ==} + peerDependencies: + '@effect/platform': ^0.93.3 + effect: ^3.19.5 + + '@effect/sql@0.48.0': + resolution: {integrity: sha512-tubdizHriDwzHUnER9UsZ/0TtF6O2WJckzeYDbVSRPeMkrpdpyEzEsoKctechTm65B3Bxy6JIixGPg2FszY72A==} + peerDependencies: + '@effect/experimental': ^0.57.0 + '@effect/platform': ^0.93.0 + effect: ^3.19.0 + + '@effect/typeclass@0.38.0': + resolution: {integrity: sha512-lMUcJTRtG8KXhXoczapZDxbLK5os7M6rn0zkvOgncJW++A0UyelZfMVMKdT5R+fgpZcsAU/1diaqw3uqLJwGxA==} + peerDependencies: + effect: ^3.19.0 + + '@effect/vitest@0.27.0': + resolution: {integrity: sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ==} + peerDependencies: + effect: ^3.19.0 + vitest: ^3.2.0 + + '@effect/workflow@0.13.0': + resolution: {integrity: sha512-RbEZSk+UuZxMgb9Kg0kSWYDRFylE2iSqSHxi9w0yxPn4EU46Fctwlz3j/sFE3XVaa4Qhje3qYFnvzg4NkHgbkw==} + peerDependencies: + '@effect/experimental': ^0.57.3 + '@effect/platform': ^0.93.3 + '@effect/rpc': ^0.72.2 + effect: ^3.19.5 + '@emnapi/core@1.7.0': resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} @@ -402,6 +521,36 @@ packages: resolution: {integrity: sha512-l2tPbXLeXR/c5ADU3YwLqULwm5UGFSRwBtyqyClY3zu3uKG3KbHq3FntyqQhJXdVW4VNZ3p/8fsALmQ7+YN/cw==} engines: {node: '>=20'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} @@ -420,6 +569,88 @@ packages: '@oxc-project/types@0.96.0': resolution: {integrity: sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==} + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -990,6 +1221,15 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1010,6 +1250,9 @@ packages: oxc-resolver: optional: true + effect@3.19.6: + resolution: {integrity: sha512-Eh1E/CI+xCAcMSDC5DtyE29yWJINC0zwBbwHappQPorjKyS69rCA8qzpsHpfhKnPDYgxdg8zkknii8mZ+6YMQA==} + electron-to-chromium@1.5.245: resolution: {integrity: sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==} @@ -1260,6 +1503,10 @@ packages: resolution: {integrity: sha512-rHj+XLOnEJ44miIXJ2W68GKnys5TYQgGhpClfbSzdpKAcYpwdjJjDJMjzj9uLVP243fszLaKDgDFwC89YB37cg==} engines: {node: '>=20'} + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1296,6 +1543,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -1385,6 +1635,10 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -1458,6 +1712,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1616,6 +1873,11 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1633,6 +1895,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + multipasta@0.2.7: + resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1645,6 +1917,13 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -1742,6 +2021,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -1915,6 +2197,9 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1979,6 +2264,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + engines: {node: '>=20.18.1'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -2003,6 +2292,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + vite@7.1.12: resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2103,6 +2396,18 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -2162,6 +2467,109 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@effect/cli@0.72.1(@effect/platform@0.93.3(effect@3.19.6))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/platform': 0.93.3(effect@3.19.6) + '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) + '@effect/printer-ansi': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) + effect: 3.19.6 + ini: 4.1.3 + toml: 3.0.0 + yaml: 2.8.1 + + '@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/platform': 0.93.3(effect@3.19.6) + '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/workflow': 0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + effect: 3.19.6 + kubernetes-types: 1.30.0 + + '@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/platform': 0.93.3(effect@3.19.6) + effect: 3.19.6 + uuid: 11.1.0 + + '@effect/language-service@0.59.0': {} + + '@effect/platform-node-shared@0.54.0(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/cluster': 0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + '@effect/platform': 0.93.3(effect@3.19.6) + '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@parcel/watcher': 2.5.1 + effect: 3.19.6 + multipasta: 0.2.7 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@0.101.1(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/cluster': 0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + '@effect/platform': 0.93.3(effect@3.19.6) + '@effect/platform-node-shared': 0.54.0(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + effect: 3.19.6 + mime: 3.0.0 + undici: 7.16.0 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform@0.93.3(effect@3.19.6)': + dependencies: + effect: 3.19.6 + find-my-way-ts: 0.1.6 + msgpackr: 1.11.5 + multipasta: 0.2.7 + + '@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) + '@effect/typeclass': 0.38.0(effect@3.19.6) + effect: 3.19.6 + + '@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/typeclass': 0.38.0(effect@3.19.6) + effect: 3.19.6 + + '@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/platform': 0.93.3(effect@3.19.6) + effect: 3.19.6 + msgpackr: 1.11.5 + + '@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/experimental': 0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/platform': 0.93.3(effect@3.19.6) + effect: 3.19.6 + uuid: 11.1.0 + + '@effect/typeclass@0.38.0(effect@3.19.6)': + dependencies: + effect: 3.19.6 + + '@effect/vitest@0.27.0(effect@3.19.6)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + dependencies: + effect: 3.19.6 + vitest: 4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) + + '@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + dependencies: + '@effect/experimental': 0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/platform': 0.93.3(effect@3.19.6) + '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + effect: 3.19.6 + '@emnapi/core@1.7.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2429,6 +2837,24 @@ snapshots: dependencies: p-retry: 7.1.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.7.0 @@ -2450,6 +2876,66 @@ snapshots: '@oxc-project/types@0.96.0': {} + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + '@pkgr/core@0.2.9': {} '@quansync/fs@0.1.5': @@ -2982,6 +3468,11 @@ snapshots: dequal@2.0.3: {} + detect-libc@1.0.3: {} + + detect-libc@2.1.2: + optional: true + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -2992,6 +3483,11 @@ snapshots: dts-resolver@2.1.2: {} + effect@3.19.6: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.245: {} empathic@2.0.0: {} @@ -3334,6 +3830,10 @@ snapshots: dependencies: termenv: 1.0.2 + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3368,6 +3868,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-my-way-ts@0.1.6: {} + find-up-simple@1.0.1: {} find-up@5.0.0: @@ -3432,6 +3934,8 @@ snapshots: indent-string@5.0.0: {} + ini@4.1.3: {} + is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -3483,6 +3987,8 @@ snapshots: kleur@3.0.3: {} + kubernetes-types@1.30.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3828,6 +4334,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime@3.0.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3847,12 +4355,37 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + + multipasta@0.2.7: {} + nanoid@3.3.11: {} natural-compare@1.4.0: {} natural-orderby@5.0.0: {} + node-addon-api@7.1.1: {} + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-releases@2.0.27: {} nth-check@2.1.1: @@ -3946,6 +4479,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -4122,6 +4657,8 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 + toml@3.0.0: {} + tree-kill@1.2.2: {} ts-api-utils@2.1.0(typescript@5.9.3): @@ -4178,6 +4715,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.16.0: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -4209,6 +4748,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + vite@7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -4290,6 +4831,8 @@ snapshots: word-wrap@1.2.5: {} + ws@8.18.3: {} + xml-name-validator@4.0.0: {} yaml-eslint-parser@1.3.0: diff --git a/src/core/changelog.ts b/src/core/changelog.ts deleted file mode 100644 index 8d1b4e9..0000000 --- a/src/core/changelog.ts +++ /dev/null @@ -1,375 +0,0 @@ -import type { NormalizedReleaseOptions } from "#shared/options"; -import type { AuthorInfo, CommitGroup } from "#shared/types"; -import type { GitCommit } from "commit-parser"; -import type { GitHubClient } from "./github"; -import type { WorkspacePackage } from "./workspace"; -import { writeFile } from "node:fs/promises"; -import { join, relative } from "node:path"; -import { logger } from "#shared/utils"; -import { dedent } from "@luxass/utils"; -import { groupByType } from "commit-parser"; -import { Eta } from "eta"; -import { readFileFromGit } from "./git"; - -const globalAuthorCache = new Map(); - -export const DEFAULT_CHANGELOG_TEMPLATE = dedent` - <% if (it.previousVersion) { -%> - ## [<%= it.version %>](<%= it.compareUrl %>) (<%= it.date %>) - <% } else { -%> - ## <%= it.version %> (<%= it.date %>) - <% } %> - - <% it.groups.forEach((group) => { %> - <% if (group.commits.length > 0) { %> - - ### <%= group.title %> - <% group.commits.forEach((commit) => { %> - - * <%= commit.line %> - <% }); %> - - <% } %> - <% }); %> -`; - -export async function generateChangelogEntry(options: { - packageName: string; - version: string; - previousVersion?: string; - date: string; - commits: GitCommit[]; - owner: string; - repo: string; - groups: CommitGroup[]; - template?: string; - githubClient: GitHubClient; -}): Promise { - const { - packageName, - version, - previousVersion, - date, - commits, - owner, - repo, - groups, - template, - githubClient, - } = options; - - // Build compare URL - const compareUrl = previousVersion - ? `https://github.com/${owner}/${repo}/compare/${packageName}@${previousVersion}...${packageName}@${version}` - : undefined; - - // Group commits by type using commit-parser - const grouped = groupByType(commits, { - includeNonConventional: false, - mergeKeys: Object.fromEntries( - groups.map((g) => [g.name, g.types]), - ) as Record, - }); - - const commitAuthors = await resolveCommitAuthors(commits, githubClient); - - // Format commits for each group - const templateGroups = groups.map((group) => { - const commitsInGroup = grouped.get(group.name) ?? []; - - if (commitsInGroup.length > 0) { - logger.verbose(`Found ${commitsInGroup.length} commits for group "${group.name}".`); - } - - // Format each commit - const formattedCommits = commitsInGroup.map((commit) => ({ - line: formatCommitLine({ - commit, - owner, - repo, - authors: commitAuthors.get(commit.hash) ?? [], - }), - })); - - return { - name: group.name, - title: group.title, - commits: formattedCommits, - }; - }); - - const templateData = { - packageName, - version, - previousVersion, - date, - compareUrl, - owner, - repo, - groups: templateGroups, - }; - - const eta = new Eta(); - const templateToUse = template || DEFAULT_CHANGELOG_TEMPLATE; - - return eta.renderString(templateToUse, templateData).trim(); -} - -export async function updateChangelog(options: { - normalizedOptions: NormalizedReleaseOptions; - workspacePackage: WorkspacePackage; - version: string; - previousVersion?: string; - commits: GitCommit[]; - date: string; - githubClient: GitHubClient; -}): Promise { - const { - version, - previousVersion, - commits, - date, - normalizedOptions, - workspacePackage, - githubClient, - } = options; - - const changelogPath = join(workspacePackage.path, "CHANGELOG.md"); - - const changelogRelativePath = relative( - normalizedOptions.workspaceRoot, - join(workspacePackage.path, "CHANGELOG.md"), - ); - - // Read the changelog from the default branch to get clean state without unreleased entries - // This ensures that if a previous release PR was abandoned, we don't keep the old entry - const existingContent = await readFileFromGit( - normalizedOptions.workspaceRoot, - normalizedOptions.branch.default, - changelogRelativePath, - ); - - logger.verbose("Existing content found: ", Boolean(existingContent)); - - // Generate the new changelog entry - const newEntry = await generateChangelogEntry({ - packageName: workspacePackage.name, - version, - previousVersion, - date, - commits, - owner: normalizedOptions.owner!, - repo: normalizedOptions.repo!, - groups: normalizedOptions.groups, - template: normalizedOptions.changelog?.template, - githubClient, - }); - - let updatedContent: string; - - if (!existingContent) { - updatedContent = `# ${workspacePackage.name}\n\n${newEntry}\n`; - - await writeFile(changelogPath, updatedContent, "utf-8"); - return; - } - - const parsed = parseChangelog(existingContent); - const lines = existingContent.split("\n"); - - // Check if this version already exists - const existingVersionIndex = parsed.versions.findIndex((v) => v.version === version); - - if (existingVersionIndex !== -1) { - // Version exists - append new commits to it (PR update scenario) - const existingVersion = parsed.versions[existingVersionIndex]!; - - // For now, just replace the entire version entry - // TODO: In future, we could parse commits and only add new ones - const before = lines.slice(0, existingVersion.lineStart); - const after = lines.slice(existingVersion.lineEnd + 1); - - updatedContent = [...before, newEntry, ...after].join("\n"); - } else { - // Version doesn't exist - insert new entry at top (below package header) - const insertAt = parsed.headerLineEnd + 1; - - const before = lines.slice(0, insertAt); - const after = lines.slice(insertAt); - - // Add empty line after header if needed - if (before.length > 0 && before[before.length - 1] !== "") { - before.push(""); - } - - updatedContent = [...before, newEntry, "", ...after].join("\n"); - } - - // Write updated content back - await writeFile(changelogPath, updatedContent, "utf-8"); -} - -async function resolveCommitAuthors( - commits: GitCommit[], - githubClient: GitHubClient, -): Promise> { - const authorsToResolve = new Set(); - const commitAuthors = new Map(); - - for (const commit of commits) { - const authorsForCommit: AuthorInfo[] = []; - - commit.authors.forEach((author, idx) => { - if (!author.email || !author.name) { - return; - } - - let info = globalAuthorCache.get(author.email); - - if (!info) { - info = { - commits: [], - name: author.name, - email: author.email, - }; - globalAuthorCache.set(author.email, info); - } - - if (idx === 0) { - info.commits.push(commit.shortHash); - } - - authorsForCommit.push(info); - - if (!info.login) { - authorsToResolve.add(info); - } - }); - - commitAuthors.set(commit.hash, authorsForCommit); - } - - await Promise.all( - Array.from(authorsToResolve).map((info) => githubClient.resolveAuthorInfo(info)), - ); - - return commitAuthors; -} - -interface FormatCommitLineOptions { - commit: GitCommit; - owner: string; - repo: string; - authors: AuthorInfo[]; -} - -function formatCommitLine({ commit, owner, repo, authors }: FormatCommitLineOptions): string { - const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`; - let line = `${commit.description}`; - const references = commit.references ?? []; - - if (references.length > 0) { - logger.verbose("Located references in commit", references.length); - } - - for (const ref of references) { - if (!ref.value) continue; - - const number = Number.parseInt(ref.value.replace(/^#/, ""), 10); - if (Number.isNaN(number)) continue; - - if (ref.type === "issue") { - line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`; - continue; - } - - line += ` ([PR ${ref.value}](https://github.com/${owner}/${repo}/pull/${number}))`; - } - - line += ` ([${commit.shortHash}](${commitUrl}))`; - - if (authors.length > 0) { - const authorList = authors - .map((author) => { - if (author.login) { - return `[@${author.login}](https://github.com/${author.login})`; - } - - return author.name; - }) - .join(", "); - - line += ` (by ${authorList})`; - } - - return line; -} - -export function parseChangelog(content: string) { - const lines = content.split("\n"); - - let packageName: string | null = null; - - // We need to start at -1, since some changelogs might not have a package name header - // which will cause us to miss the first version entry otherwise. - let headerLineEnd = -1; - const versions: { - version: string; - lineStart: number; - lineEnd: number; - content: string; - }[] = []; - - // Extract package name from first heading (# @package/name) - for (let i = 0; i < lines.length; i++) { - const line = lines[i]!.trim(); - - if (line.startsWith("# ")) { - packageName = line.slice(2).trim(); - headerLineEnd = i; - break; - } - } - - // Find all version entries (## version or ## [version](link)) - for (let i = headerLineEnd + 1; i < lines.length; i++) { - const line = lines[i]!.trim(); - - if (line.startsWith("## ")) { - // Extract version from various formats: - // ## 0.1.0 - // ## [0.1.0](link) (date) - // ## 0.1.0 - const versionMatch = line.match(/##\s+(?:)?\[?([^\](\s<]+)/); - - if (versionMatch) { - const version = versionMatch[1]!; - const lineStart = i; - - // Find where this version entry ends (next ## or end of file) - let lineEnd = lines.length - 1; - for (let j = i + 1; j < lines.length; j++) { - if (lines[j]!.trim().startsWith("## ")) { - lineEnd = j - 1; - break; - } - } - - const versionContent = lines.slice(lineStart, lineEnd + 1).join("\n"); - - versions.push({ - version, - lineStart, - lineEnd, - content: versionContent, - }); - } - } - } - - return { - packageName, - versions, - headerLineEnd, - }; -} diff --git a/src/core/git.ts b/src/core/git.ts deleted file mode 100644 index 31938ba..0000000 --- a/src/core/git.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { - exitWithError, - logger, - run, - runIfNotDry, -} from "#shared/utils"; -import farver from "farver"; - -/** - * Check if the working directory is clean (no uncommitted changes) - * @param {string} workspaceRoot - The root directory of the workspace - * @returns {Promise} A Promise resolving to true if clean, false otherwise - */ -export async function isWorkingDirectoryClean( - workspaceRoot: string, -): Promise { - try { - const result = await run("git", ["status", "--porcelain"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - if (result.stdout.trim() !== "") { - return false; - } - - return true; - } catch (err: any) { - logger.error("Error checking git status:", err); - return false; - } -} - -/** - * Check if a git branch exists locally - * @param {string} branch - The branch name to check - * @param {string} workspaceRoot - The root directory of the workspace - * @returns {Promise} Promise resolving to true if branch exists, false otherwise - */ -export async function doesBranchExist( - branch: string, - workspaceRoot: string, -): Promise { - try { - await run("git", ["rev-parse", "--verify", branch], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return true; - } catch { - return false; - } -} - -/** - * Retrieves the default branch name from the remote repository. - * Falls back to "main" if the default branch cannot be determined. - * @returns {Promise} A Promise resolving to the default branch name as a string. - */ -export async function getDefaultBranch(workspaceRoot: string): Promise { - try { - const result = await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const ref = result.stdout.trim(); - const match = ref.match(/^refs\/remotes\/origin\/(.+)$/); - if (match && match[1]) { - return match[1]; - } - - return "main"; // Fallback - } catch { - return "main"; // Fallback - } -} - -/** - * Retrieves the name of the current branch in the repository. - * @param {string} workspaceRoot - The root directory of the workspace - * @returns {Promise} A Promise resolving to the current branch name as a string - */ -export async function getCurrentBranch( - workspaceRoot: string, -): Promise { - try { - const result = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return result.stdout.trim(); - } catch (err) { - logger.error("Error getting current branch:", err); - throw err; - } -} - -/** - * Retrieves the list of available branches in the repository. - * @param {string} workspaceRoot - The root directory of the workspace - * @returns {Promise} A Promise resolving to an array of branch names - */ -export async function getAvailableBranches( - workspaceRoot: string, -): Promise { - try { - const result = await run("git", ["branch", "--list"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return result.stdout - .split("\n") - .map((line) => line.replace("*", "").trim()) - .filter((line) => line.length > 0); - } catch (err) { - logger.error("Error getting available branches:", err); - throw err; - } -} - -/** - * Creates a new branch from the specified base branch. - * @param {string} branch - The name of the new branch to create - * @param {string} base - The base branch to create the new branch from - * @param {string} workspaceRoot - The root directory of the workspace - * @returns {Promise} A Promise that resolves when the branch is created - */ -export async function createBranch( - branch: string, - base: string, - workspaceRoot: string, -): Promise { - try { - logger.info(`Creating branch: ${farver.green(branch)} from ${farver.cyan(base)}`); - await runIfNotDry("git", ["branch", branch, base], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - } catch { - exitWithError( - `Failed to create branch: ${branch}`, - `Make sure the branch doesn't already exist and you have a clean working directory`, - ); - } -} - -export async function checkoutBranch( - branch: string, - workspaceRoot: string, -): Promise { - try { - logger.info(`Switching to branch: ${farver.green(branch)}`); - const result = await run("git", ["checkout", branch], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const output = result.stderr.trim(); - const match = output.match(/Switched to branch '(.+)'/); - if (match && match[1] === branch) { - logger.info(`Successfully switched to branch: ${farver.green(branch)}`); - return true; - } - - return false; - } catch { - return false; - } -} - -export async function pullLatestChanges( - branch: string, - workspaceRoot: string, -): Promise { - try { - await run("git", ["pull", "origin", branch], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - return true; - } catch { - return false; - } -} - -export async function rebaseBranch( - ontoBranch: string, - workspaceRoot: string, -): Promise { - try { - logger.info(`Rebasing onto: ${farver.cyan(ontoBranch)}`); - await runIfNotDry("git", ["rebase", ontoBranch], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return true; - } catch { - exitWithError( - `Failed to rebase onto: ${ontoBranch}`, - `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`, - ); - } -} - -export async function isBranchAheadOfRemote( - branch: string, - workspaceRoot: string, -): Promise { - try { - const result = await run("git", ["rev-list", `origin/${branch}..${branch}`, "--count"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const commitCount = Number.parseInt(result.stdout.trim(), 10); - return commitCount > 0; - } catch { - // If remote branch doesn't exist, consider it as ahead - return true; - } -} - -export async function commitChanges( - message: string, - workspaceRoot: string, -): Promise { - try { - // Stage all changes - await run("git", ["add", "."], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - // Check if there are changes to commit - const isClean = await isWorkingDirectoryClean(workspaceRoot); - if (isClean) { - return false; - } - - // Commit - logger.info(`Committing changes: ${farver.dim(message)}`); - await runIfNotDry("git", ["commit", "-m", message], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return true; - } catch { - exitWithError( - `Failed to commit changes`, - `Make sure you have git configured properly with user.name and user.email`, - ); - } -} - -export async function pushBranch( - branch: string, - workspaceRoot: string, - options?: { force?: boolean; forceWithLease?: boolean }, -): Promise { - try { - const args = ["push", "origin", branch]; - - if (options?.forceWithLease) { - args.push("--force-with-lease"); - logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`); - } else if (options?.force) { - args.push("--force"); - logger.info(`Force pushing branch: ${farver.green(branch)}`); - } else { - logger.info(`Pushing branch: ${farver.green(branch)}`); - } - - await runIfNotDry("git", args, { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return true; - } catch { - exitWithError( - `Failed to push branch: ${branch}`, - `Make sure you have permission to push to the remote repository`, - ); - } -} - -export async function readFileFromGit( - workspaceRoot: string, - ref: string, - filePath: string, -): Promise { - try { - const result = await run("git", ["show", `${ref}:${filePath}`], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - return result.stdout; - } catch { - return null; - } -} - -export async function getMostRecentPackageTag( - workspaceRoot: string, - packageName: string, -): Promise { - try { - // Tags for each package follow the format: packageName@version - const { stdout } = await run("git", ["tag", "--list", `${packageName}@*`], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean); - if (tags.length === 0) { - return undefined; - } - - // Find the last tag for the specified package - return tags.reverse()[0]; - } catch (err) { - logger.warn( - `Failed to get tags for package ${packageName}: ${(err as Error).message}`, - ); - return undefined; - } -} - -/** - * Builds a mapping of commit SHAs to the list of files changed in each commit - * within a given inclusive range. - * - * Internally runs: - * git log --name-only --format=%H ^.. - * - * Notes - * - This includes the commit identified by `from` (via `from^..to`). - * - Order of commits in the resulting Map follows `git log` output - * (reverse chronological, newest first). - * - On failure (e.g., invalid refs), the function returns null. - * - * @param {string} workspaceRoot Absolute path to the git repository root used as cwd. - * @param {string} from Starting commit/ref (inclusive). - * @param {string} to Ending commit/ref (inclusive). - * @returns {Promise | null>} Promise resolving to a Map where keys are commit SHAs and values are - * arrays of file paths changed by that commit, or null on error. - */ -export async function getGroupedFilesByCommitSha( - workspaceRoot: string, - from: string, - to: string, -): Promise | null> { - // commit hash file paths - const commitsMap = new Map(); - - try { - const { stdout } = await run("git", ["log", "--name-only", "--format=%H", `${from}^..${to}`], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const lines = stdout.trim().split("\n").filter((line) => line.trim() !== ""); - - let currentSha: string | null = null; - const HASH_REGEX = /^[0-9a-f]{40}$/i; - - for (const line of lines) { - const trimmedLine = line.trim(); - - // Found a new commit hash - if (HASH_REGEX.test(trimmedLine)) { - currentSha = trimmedLine; - commitsMap.set(currentSha, []); - - continue; - } - - if (currentSha === null) { - // Malformed output: file path found before any commit hash - continue; - } - - // Found a file path, and we have a current hash to assign it to - // Note: In case of merge commits, an empty line might appear which is already filtered. - // If the line is NOT a hash, it must be a file path. - - // The file path is added to the array associated with the most recent hash. - commitsMap.get(currentSha)!.push(trimmedLine); - } - - return commitsMap; - } catch { - return null; - } -} diff --git a/src/core/github.ts b/src/core/github.ts deleted file mode 100644 index 449c7dd..0000000 --- a/src/core/github.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { AuthorInfo, PackageRelease } from "#shared/types"; -import { logger } from "#shared/utils"; -import { dedent } from "@luxass/utils"; -import { Eta } from "eta"; -import farver from "farver"; - -interface SharedGitHubOptions { - owner: string; - repo: string; - githubToken: string; -} - -export interface GitHubPullRequest { - number: number; - title: string; - body: string; - draft: boolean; - html_url?: string; - head?: { - sha: string; - }; -} - -export type CommitStatusState = "error" | "failure" | "pending" | "success"; - -export interface CommitStatusOptions { - state: CommitStatusState; - targetUrl?: string; - description?: string; - context: string; -} - -export interface UpsertPullRequestOptions { - title: string; - body: string; - head?: string; - base?: string; - pullNumber?: number; -} - -export class GitHubClient { - private readonly owner: string; - private readonly repo: string; - private readonly githubToken: string; - private readonly apiBase = "https://api.github.com"; - - constructor({ owner, repo, githubToken }: SharedGitHubOptions) { - this.owner = owner; - this.repo = repo; - this.githubToken = githubToken; - } - - private async request(path: string, init: RequestInit = {}): Promise { - const url = path.startsWith("http") ? path : `${this.apiBase}${path}`; - - const res = await fetch(url, { - ...init, - headers: { - ...init.headers, - "Accept": "application/vnd.github.v3+json", - "Authorization": `token ${this.githubToken}`, - "User-Agent": "ucdjs-release-scripts (+https://github.com/ucdjs/ucdjs-release-scripts)", - }, - }); - - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`GitHub API request failed with status ${res.status}: ${errorText || "No response body"}`); - } - - if (res.status === 204) { - return undefined as T; - } - - return res.json() as Promise; - } - - async getExistingPullRequest(branch: string): Promise { - const head = branch.includes(":") ? branch : `${this.owner}:${branch}`; - const endpoint = `/repos/${this.owner}/${this.repo}/pulls?state=open&head=${encodeURIComponent(head)}`; - - logger.verbose(`Requesting pull request for branch: ${branch} (url: ${this.apiBase}${endpoint})`); - const pulls = await this.request(endpoint); - - if (!Array.isArray(pulls) || pulls.length === 0) { - return null; - } - - const firstPullRequest: unknown = pulls[0]; - - if ( - typeof firstPullRequest !== "object" - || firstPullRequest === null - || !("number" in firstPullRequest) - || typeof firstPullRequest.number !== "number" - || !("title" in firstPullRequest) - || typeof firstPullRequest.title !== "string" - || !("body" in firstPullRequest) - || typeof firstPullRequest.body !== "string" - || !("draft" in firstPullRequest) - || typeof firstPullRequest.draft !== "boolean" - || !("html_url" in firstPullRequest) - || typeof firstPullRequest.html_url !== "string" - ) { - throw new TypeError("Pull request data validation failed"); - } - - const pullRequest: GitHubPullRequest = { - number: firstPullRequest.number, - title: firstPullRequest.title, - body: firstPullRequest.body, - draft: firstPullRequest.draft, - html_url: firstPullRequest.html_url, - head: "head" in firstPullRequest - && typeof firstPullRequest.head === "object" - && firstPullRequest.head !== null - && "sha" in firstPullRequest.head - && typeof firstPullRequest.head.sha === "string" - ? { sha: firstPullRequest.head.sha } - : undefined, - }; - - logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`); - return pullRequest; - } - - async upsertPullRequest({ - title, - body, - head, - base, - pullNumber, - }: UpsertPullRequestOptions): Promise { - const isUpdate = typeof pullNumber === "number"; - const endpoint = isUpdate - ? `/repos/${this.owner}/${this.repo}/pulls/${pullNumber}` - : `/repos/${this.owner}/${this.repo}/pulls`; - - const requestBody = isUpdate - ? { title, body } - : { title, body, head, base, draft: true }; - - logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${this.apiBase}${endpoint})`); - - const pr = await this.request(endpoint, { - method: isUpdate ? "PATCH" : "POST", - body: JSON.stringify(requestBody), - }); - - if ( - typeof pr !== "object" - || pr === null - || !("number" in pr) - || typeof pr.number !== "number" - || !("title" in pr) - || typeof pr.title !== "string" - || !("body" in pr) - || typeof pr.body !== "string" - || !("draft" in pr) - || typeof pr.draft !== "boolean" - || !("html_url" in pr) - || typeof pr.html_url !== "string" - ) { - throw new TypeError("Pull request data validation failed"); - } - - const action = isUpdate ? "Updated" : "Created"; - logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`); - - return { - number: pr.number, - title: pr.title, - body: pr.body, - draft: pr.draft, - html_url: pr.html_url, - }; - } - - async setCommitStatus({ - sha, - state, - targetUrl, - description, - context, - }: CommitStatusOptions & { sha: string }): Promise { - const endpoint = `/repos/${this.owner}/${this.repo}/statuses/${sha}`; - - logger.verbose(`Setting commit status on ${sha} to ${state} (url: ${this.apiBase}${endpoint})`); - - await this.request(endpoint, { - method: "POST", - body: JSON.stringify({ - state, - target_url: targetUrl, - description: description || "", - context, - }), - }); - - logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`); - } - - async resolveAuthorInfo(info: AuthorInfo): Promise { - if (info.login) { - return info; - } - - try { - // https://docs.github.com/en/search-github/searching-on-github/searching-users#search-only-users-or-organizations - const q = encodeURIComponent(`${info.email} type:user in:email`); - const data = await this.request<{ - items?: Array<{ login: string }>; - }>(`/search/users?q=${q}`); - - if (!data.items || data.items.length === 0) { - return info; - } - - info.login = data.items[0]!.login; - } catch (err) { - logger.warn(`Failed to resolve author info for email ${info.email}: ${(err as Error).message}`); - } - - if (info.login) { - return info; - } - - if (info.commits.length > 0) { - try { - const data = await this.request<{ - author: { - login: string; - }; - }>( - `/repos/${this.owner}/${this.repo}/commits/${info.commits[0]}`, - ); - - if (data.author && data.author.login) { - info.login = data.author.login; - } - } catch (err) { - logger.warn(`Failed to resolve author info from commits for email ${info.email}: ${(err as Error).message}`); - } - } - - return info; - } -} - -export function createGitHubClient(options: SharedGitHubOptions): GitHubClient { - return new GitHubClient(options); -} - -export const DEFAULT_PR_BODY_TEMPLATE = dedent` - This PR was automatically generated by the release script. - - The following packages have been prepared for release: - - <% it.packages.forEach((pkg) => { %> - - **<%= pkg.name %>**: <%= pkg.currentVersion %> → <%= pkg.newVersion %> (<%= pkg.bumpType %>) - <% }) %> - - Please review the changes and merge when ready. - - For a more in-depth look at the changes, please refer to the individual package changelogs. - - > [!NOTE] - > When this PR is merged, the release process will be triggered automatically, publishing the new package versions to the registry. -`; - -function dedentString(str: string): string { - const lines = str.split("\n"); - const minIndent = lines - .filter((line) => line.trim().length > 0) - .reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity); - - return lines - .map((line) => (minIndent === Infinity ? line : line.slice(minIndent))) - .join("\n") - .trim(); -} - -export function generatePullRequestBody(updates: PackageRelease[], body?: string): string { - const eta = new Eta(); - - const bodyTemplate = body ? dedentString(body) : DEFAULT_PR_BODY_TEMPLATE; - - return eta.renderString(bodyTemplate, { - packages: updates.map((u) => ({ - name: u.package.name, - currentVersion: u.currentVersion, - newVersion: u.newVersion, - bumpType: u.bumpType, - hasDirectChanges: u.hasDirectChanges, - })), - }); -} diff --git a/src/core/prompts.ts b/src/core/prompts.ts deleted file mode 100644 index 216ecbe..0000000 --- a/src/core/prompts.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { WorkspacePackage } from "#core/workspace"; -import type { BumpKind } from "#shared/types"; -import { getNextVersion, isValidSemver } from "#versioning/version"; -import farver from "farver"; -import prompts from "prompts"; - -export async function selectPackagePrompt( - packages: WorkspacePackage[], -): Promise { - const response = await prompts({ - type: "multiselect", - name: "selectedPackages", - message: "Select packages to release", - choices: packages.map((pkg) => ({ - title: `${pkg.name} (${farver.bold(pkg.version)})`, - value: pkg.name, - selected: true, - })), - min: 1, - hint: "Space to select/deselect. Return to submit.", - instructions: false, - }); - - if (!response.selectedPackages || response.selectedPackages.length === 0) { - return []; - } - - return response.selectedPackages; -} - -export async function selectVersionPrompt( - workspaceRoot: string, - pkg: WorkspacePackage, - currentVersion: string, - suggestedVersion: string, -): Promise { - const answers = await prompts([ - { - type: "autocomplete", - name: "version", - message: `${pkg.name}: ${farver.green(pkg.version)}`, - choices: [ - { value: "skip", title: `skip ${farver.dim("(no change)")}` }, - { value: "major", title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}` }, - { value: "minor", title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}` }, - { value: "patch", title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}` }, - - { value: "suggested", title: `suggested ${farver.bold(suggestedVersion)}` }, - { value: "as-is", title: `as-is ${farver.dim("(keep current version)")}` }, - - { value: "custom", title: "custom" }, - ], - initial: suggestedVersion === currentVersion ? 0 : 4, // Default to "skip" if no change, otherwise "suggested" - }, - { - type: (prev) => prev === "custom" ? "text" : null, - name: "custom", - message: "Enter the new version number:", - initial: suggestedVersion, - validate: (custom: string) => { - if (isValidSemver(custom)) { - return true; - } - - return "That's not a valid version number"; - }, - }, - ]); - - // User cancelled (Ctrl+C) - if (!answers.version) { - return null; - } - - if (answers.version === "skip") { - return null; - } else if (answers.version === "suggested") { - return suggestedVersion; - } else if (answers.version === "custom") { - if (!answers.custom) { - return null; - } - - return answers.custom; - } else if (answers.version === "as-is") { - // TODO: verify that there isn't any tags already existing for this version? - return currentVersion; - } else { - // It's a bump type - return getNextVersion(pkg.version, answers.version as BumpKind); - } -} diff --git a/src/core/workspace.ts b/src/core/workspace.ts deleted file mode 100644 index 1ea9302..0000000 --- a/src/core/workspace.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { - FindWorkspacePackagesOptions, - PackageJson, - SharedOptions, -} from "#shared/types"; -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { selectPackagePrompt } from "#core/prompts"; -import { exitWithError, isCI, logger, run } from "#shared/utils"; -import farver from "farver"; - -interface RawProject { - name: string; - path: string; - version: string; - private: boolean; - dependencies?: Record; - devDependencies?: Record; -} - -export interface WorkspacePackage { - name: string; - version: string; - path: string; - packageJson: PackageJson; - workspaceDependencies: string[]; - workspaceDevDependencies: string[]; -} - -export async function discoverWorkspacePackages( - workspaceRoot: string, - options: SharedOptions, -): Promise { - let workspaceOptions: FindWorkspacePackagesOptions; - let explicitPackages: string[] | undefined; - - // Normalize package options and determine if packages were explicitly specified - if (options.packages == null || options.packages === true) { - workspaceOptions = { excludePrivate: false }; - } else if (Array.isArray(options.packages)) { - workspaceOptions = { excludePrivate: false, include: options.packages }; - explicitPackages = options.packages; - } else { - workspaceOptions = options.packages; - if (options.packages.include) { - explicitPackages = options.packages.include; - } - } - - let workspacePackages = await findWorkspacePackages( - workspaceRoot, - workspaceOptions, - ); - - // If specific packages were requested, validate they were all found - if (explicitPackages) { - const foundNames = new Set(workspacePackages.map((p) => p.name)); - const missing = explicitPackages.filter((p) => !foundNames.has(p)); - - if (missing.length > 0) { - exitWithError( - `Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`, - "Check your package names or run 'pnpm ls' to see available packages", - ); - } - } - - // Show interactive prompt only if: - // 1. Not in CI - // 2. Prompt is enabled - // 3. No explicit packages were specified (user didn't pre-select specific packages) - const isPackagePromptEnabled = options.prompts?.packages !== false; - if (!isCI && isPackagePromptEnabled && !explicitPackages) { - const selectedNames = await selectPackagePrompt(workspacePackages); - workspacePackages = workspacePackages.filter((pkg) => - selectedNames.includes(pkg.name), - ); - } - - return workspacePackages; -} - -async function findWorkspacePackages( - workspaceRoot: string, - options?: FindWorkspacePackagesOptions, -): Promise { - try { - const result = await run("pnpm", ["-r", "ls", "--json"], { - nodeOptions: { - cwd: workspaceRoot, - stdio: "pipe", - }, - }); - - const rawProjects: RawProject[] = JSON.parse(result.stdout); - - const allPackageNames = new Set(rawProjects.map((p) => p.name)); - const excludedPackages = new Set(); - - const promises = rawProjects.map(async (rawProject) => { - const packageJsonPath = join(rawProject.path, "package.json"); - const content = await readFile(packageJsonPath, "utf-8"); - const packageJson: PackageJson = JSON.parse(content); - - if (!shouldIncludePackage(packageJson, options)) { - excludedPackages.add(rawProject.name); - return null; - } - - return { - name: rawProject.name, - version: rawProject.version, - path: rawProject.path, - packageJson, - workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => { - return allPackageNames.has(dep); - }), - workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => { - return allPackageNames.has(dep); - }), - }; - }); - - const packages = await Promise.all(promises); - - if (excludedPackages.size > 0) { - logger.info(`Excluded packages: ${farver.green( - Array.from(excludedPackages).join(", "), - )}`); - } - - // Filter out excluded packages (nulls) - return packages.filter( - (pkg): pkg is WorkspacePackage => pkg !== null, - ); - } catch (err) { - logger.error("Error discovering workspace packages:", err); - throw err; - } -} - -function shouldIncludePackage( - pkg: PackageJson, - options?: FindWorkspacePackagesOptions, -): boolean { - if (!options) { - return true; - } - - // Check if private packages should be excluded - if (options.excludePrivate && pkg.private) { - return false; - } - - // Check include list (if specified, only these packages are included) - if (options.include && options.include.length > 0) { - if (!options.include.includes(pkg.name)) { - return false; - } - } - - // Check exclude list - if (options.exclude?.includes(pkg.name)) { - return false; - } - - return true; -} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..86ee333 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,26 @@ +import { Data } from "effect"; + +export class GitError extends Data.TaggedError("GitError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class GitCommandError extends Data.TaggedError("GitCommandError")<{ + readonly command: string; + readonly exitCode: number; + readonly stderr: string; +}> {} + +export class GitNotRepositoryError extends Data.TaggedError("GitNotRepositoryError")<{ + readonly path: string; +}> {} + +export class BranchNotFoundError extends Data.TaggedError("BranchNotFoundError")<{ + readonly branchName: string; +}> {} + +export class WorkspaceError extends Data.TaggedError("WorkspaceError")<{ + message: string; + operation?: string; + cause?: unknown; +}> { } diff --git a/src/index.ts b/src/index.ts index 11b3e8c..4d39be6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,134 @@ -export { - publish, - type PublishOptions, -} from "#publish"; -export { - release, - type ReleaseOptions, - type ReleaseResult, -} from "#release"; -export { - verify, - type VerifyOptions, -} from "#verify"; +import type { WorkspacePackage } from "./services/workspace.service.js"; +import process from "node:process"; +import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; +import { Effect, Layer } from "effect"; +import { GitService } from "./services/git.service.js"; +import { GitHubService } from "./services/github.service.js"; +import { WorkspaceService } from "./services/workspace.service.js"; + +export interface Options { + /** + * Repository identifier (e.g., "owner/repo") + */ + repo: `${string}/${string}`; + + /** + * Root directory of the workspace (defaults to process.cwd()) + */ + workspaceRoot?: string; + + /** + * Specific packages to prepare for release. + * - true: discover all packages + * - FindWorkspacePackagesOptions: discover with filters + * - string[]: specific package names + */ + packages?: true | unknown | string[]; + + /** + * GitHub token for authentication + */ + githubToken: string; + + branch?: { + release?: string; + default?: string; + }; + + safeguards?: boolean; +} + +export interface ReleaseScriptsAPI { + verify: () => Promise; + packages: { + list: () => Promise; + get: (packageName: string) => Promise; + }; +} + +export async function createReleaseScripts(config: Options): Promise { + const { + workspaceRoot: cwd = process.cwd(), + } = config; + + const MainLayer = Layer.mergeAll( + GitService.Default, + WorkspaceService.Default, + GitHubService.Default, + ).pipe( + Layer.provide(NodeCommandExecutor.layer), + Layer.provide(NodeFileSystem.layer), + ); + + const runProgram = (program: Effect.Effect): Promise => + Effect.runPromise(Effect.provide(program, MainLayer) as Effect.Effect); + + const initProgram = Effect.gen(function* () { + const git = yield* GitService; + + const isRepository = yield* git.isRepository; + if (!isRepository) { + return yield* Effect.fail(new Error(`The directory ${cwd} is not a git repository.`)); + } + + const hasChanges = yield* git.hasChanges; + if (hasChanges) { + return yield* Effect.fail(new Error("The git repository has uncommitted changes.")); + } + + return yield* Effect.succeed(void 0); + }); + + await Effect.runPromise(Effect.provide(initProgram, MainLayer).pipe( + Effect.catchAll((err) => { + console.error(`❌ Initialization failed: ${err.message}`); + return Effect.exit(Effect.fail(err)); + }), + )); + + return { + async verify(): Promise { + const program = Effect.gen(function* () { + const git = yield* GitService; + const github = yield* GitHubService; + + const isRepository = yield* git.isRepository; + if (!isRepository) { + return yield* Effect.fail(new Error(`The directory ${cwd} is not a git repository.`)); + } + + const releasePullRequest = yield* github.getCurrentPullRequest(); + + if (!releasePullRequest || !releasePullRequest.head) { + return yield* Effect.fail(new Error("No pull request found for the current branch.")); + } + + const originalBranch = yield* git.getCurrentBranch; + + console.log(`✅ Verification successful on branch ${originalBranch}.`); + }); + + return runProgram(program); + }, + + packages: { + async list(): Promise { + const program = Effect.gen(function* () { + const workspace = yield* WorkspaceService; + return yield* workspace.listPackages; + }); + + return runProgram(program); + }, + async get(packageName: string): Promise { + const program = Effect.gen(function* () { + const workspace = yield* WorkspaceService; + const pkg = yield* workspace.findPackageByName(packageName); + return pkg || null; + }); + + return runProgram(program); + }, + }, + }; +} diff --git a/src/publish.ts b/src/publish.ts deleted file mode 100644 index c095317..0000000 --- a/src/publish.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { SharedOptions } from "#shared/types"; - -// eslint-disable-next-line ts/no-empty-object-type -export interface PublishOptions extends SharedOptions {} - -export function publish(_options: PublishOptions) { - -} diff --git a/src/release.ts b/src/release.ts deleted file mode 100644 index 2a609cb..0000000 --- a/src/release.ts +++ /dev/null @@ -1,440 +0,0 @@ -import type { GitHubClient } from "#core/github"; -import type { - GlobalCommitMode, - PackageRelease, - SharedOptions, -} from "#shared/types"; -import type { VersionOverrides } from "#versioning/version"; -import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { updateChangelog } from "#core/changelog"; -import { - checkoutBranch, - commitChanges, - createBranch, - doesBranchExist, - getCurrentBranch, - isBranchAheadOfRemote, - isWorkingDirectoryClean, - pullLatestChanges, - pushBranch, - rebaseBranch, -} from "#core/git"; -import { - createGitHubClient, - generatePullRequestBody, -} from "#core/github"; -import { discoverWorkspacePackages } from "#core/workspace"; -import { normalizeReleaseOptions } from "#shared/options"; -import { - exitWithError, - logger, - ucdjsReleaseOverridesPath, -} from "#shared/utils"; -import { - getGlobalCommitsPerPackage, - getWorkspacePackageGroupedCommits, -} from "#versioning/commits"; -import { calculateAndPrepareVersionUpdates } from "#versioning/version"; -import farver from "farver"; -import { compare } from "semver"; - -export interface ReleaseOptions extends SharedOptions { - branch?: { - /** - * Branch name for the release PR (defaults to "release/next") - */ - release?: string; - - /** - * Default branch name (e.g., "main") - */ - default?: string; - }; - - /** - * Whether to enable safety safeguards (e.g., checking for clean working directory) - * @default true - */ - safeguards?: boolean; - - /** - * Pull request configuration - */ - pullRequest?: { - /** - * Title for the release pull request - */ - title?: string; - - /** - * Body for the release pull request - * - * If not provided, a default body will be generated. - * - * NOTE: - * You can use custom template expressions, see [h3js/rendu](https://github.com/h3js/rendu) - */ - body?: string; - }; - - changelog?: { - /** - * Whether to generate or update changelogs - * @default true - */ - enabled?: boolean; - - /** - * Custom changelog entry template (ETA format) - */ - template?: string; - }; - - globalCommitMode?: GlobalCommitMode; -} - -export interface ReleaseResult { - /** - * Packages that will be updated - */ - updates: PackageRelease[]; - - /** - * URL of the created or updated PR - */ - prUrl?: string; - - /** - * Whether a new PR was created (vs updating existing) - */ - created: boolean; -} - -export async function release( - options: ReleaseOptions, -): Promise { - const { - workspaceRoot, - ...normalizedOptions - } = await normalizeReleaseOptions(options); - - if (normalizedOptions.safeguards && !(await isWorkingDirectoryClean(workspaceRoot))) { - exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding."); - } - - const workspacePackages = await discoverWorkspacePackages( - workspaceRoot, - options, - ); - - if (workspacePackages.length === 0) { - logger.warn("No packages found to release"); - return null; - } - - logger.section("📦 Workspace Packages"); - logger.item(`Found ${workspacePackages.length} packages`); - - for (const pkg of workspacePackages) { - logger.item(`${farver.cyan(pkg.name)} (${farver.bold(pkg.version)})`); - logger.item(` ${farver.gray("→")} ${farver.gray(pkg.path)}`); - } - - logger.emptyLine(); - - // Get all commits grouped by their package. - // Each package's commits are determined based on its own release history. - // So, for example, if package A was last released at v1.2.0 and package B at v2.0.0, - // we will get all commits since v1.2.0 for package A, and all commits since v2.0.0 for package B. - const groupedPackageCommits = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages); - - // Get global commits per-package based on each package's own timeline - const globalCommitsPerPackage = await getGlobalCommitsPerPackage( - workspaceRoot, - groupedPackageCommits, - workspacePackages, - normalizedOptions.globalCommitMode, - ); - - const githubClient = createGitHubClient({ - owner: normalizedOptions.owner, - repo: normalizedOptions.repo, - githubToken: normalizedOptions.githubToken, - }); - - const prOps = await orchestrateReleasePullRequest({ - workspaceRoot, - githubClient, - releaseBranch: normalizedOptions.branch.release, - defaultBranch: normalizedOptions.branch.default, - pullRequestTitle: options.pullRequest?.title, - pullRequestBody: options.pullRequest?.body, - }); - - // Prepare the release branch (checkout, rebase, etc.) - await prOps.prepareBranch(); - - const overridesPath = join(workspaceRoot, ucdjsReleaseOverridesPath); - let existingOverrides: VersionOverrides = {}; - try { - const overridesContent = await readFile(overridesPath, "utf-8"); - existingOverrides = JSON.parse(overridesContent); - logger.info("Found existing version overrides file."); - } catch { - logger.info("No existing version overrides file found. Continuing..."); - } - - // Calculate version updates and prepare apply function - const { allUpdates, applyUpdates, overrides: newOverrides } = await calculateAndPrepareVersionUpdates({ - workspacePackages, - packageCommits: groupedPackageCommits, - workspaceRoot, - showPrompt: options.prompts?.versions !== false, - globalCommitsPerPackage, - overrides: existingOverrides, - }); - - // If there are any overrides, write them to the overrides file. - if (Object.keys(newOverrides).length > 0) { - logger.info("Writing version overrides file..."); - try { - await mkdir(join(workspaceRoot, ".github"), { recursive: true }); - await writeFile(overridesPath, JSON.stringify(newOverrides, null, 2), "utf-8"); - logger.success("Successfully wrote version overrides file."); - } catch (e) { - logger.error("Failed to write version overrides file:", e); - } - } - - // But if there is no overrides, ensure that the past overrides doesn't conflict with the new calculation. - // If the new calculation results is greater than what the overrides dictated, we should remove the overrides file. - if (Object.keys(newOverrides).length === 0 && Object.keys(existingOverrides).length > 0) { - let shouldRemoveOverrides = false; - for (const update of allUpdates) { - const overriddenVersion = existingOverrides[update.package.name]; - if (overriddenVersion) { - if (compare(update.newVersion, overriddenVersion.version) > 0) { - shouldRemoveOverrides = true; - break; - } - } - } - - if (shouldRemoveOverrides) { - logger.info("Removing obsolete version overrides file..."); - try { - await rm(overridesPath); - logger.success("Successfully removed obsolete version overrides file."); - } catch (e) { - logger.error("Failed to remove obsolete version overrides file:", e); - } - } - } - - if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) { - logger.warn("No packages have changes requiring a release"); - } - - logger.section("🔄 Version Updates"); - logger.item(`Updating ${allUpdates.length} packages (including dependents)`); - - for (const update of allUpdates) { - logger.item(`${update.package.name}: ${update.currentVersion} → ${update.newVersion}`); - } - - // Apply version updates to package.json files - await applyUpdates(); - - // If the changelog option is enabled, update changelogs - if (normalizedOptions.changelog.enabled) { - logger.step("Updating changelogs"); - - const changelogPromises = allUpdates.map((update) => { - const pkgCommits = groupedPackageCommits.get(update.package.name) || []; - - const globalCommits = globalCommitsPerPackage.get(update.package.name) || []; - const allCommits = [...pkgCommits, ...globalCommits]; - - if (allCommits.length === 0) { - logger.verbose(`No commits for ${update.package.name}, skipping changelog`); - return Promise.resolve(); - } - - logger.verbose(`Updating changelog for ${farver.cyan(update.package.name)}`); - - return updateChangelog({ - normalizedOptions: { - ...normalizedOptions, - workspaceRoot, - }, - githubClient, - workspacePackage: update.package, - version: update.newVersion, - previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : undefined, - commits: allCommits, - date: new Date().toISOString().split("T")[0]!, - }); - }).filter((p): p is Promise => p != null); - - const updates = await Promise.all(changelogPromises); - - logger.success(`Updated ${updates.length} changelog(s)`); - } - - // Commit and push changes - const hasChangesToPush = await prOps.syncChanges(true); - - if (!hasChangesToPush) { - if (prOps.doesReleasePRExist && prOps.existingPullRequest) { - logger.item("No updates needed, PR is already up to date"); - - const { pullRequest, created } = await prOps.syncPullRequest(allUpdates); - - await prOps.cleanup(); - - return { - updates: allUpdates, - prUrl: pullRequest?.html_url, - created, - }; - } else { - logger.error("No changes to commit, and no existing PR. Nothing to do."); - return null; - } - } - - // Create or update PR - const { pullRequest, created } = await prOps.syncPullRequest(allUpdates); - - await prOps.cleanup(); - - if (pullRequest?.html_url) { - logger.section("🚀 Pull Request"); - logger.success(`Pull request ${created ? "created" : "updated"}: ${pullRequest.html_url}`); - } - - return { - updates: allUpdates, - prUrl: pullRequest?.html_url, - created, - }; -} - -async function orchestrateReleasePullRequest({ - workspaceRoot, - githubClient, - releaseBranch, - defaultBranch, - pullRequestTitle, - pullRequestBody, -}: { - workspaceRoot: string; - githubClient: GitHubClient; - releaseBranch: string; - defaultBranch: string; - pullRequestTitle?: string; - pullRequestBody?: string; -}) { - const currentBranch = await getCurrentBranch(workspaceRoot); - - if (currentBranch !== defaultBranch) { - exitWithError( - `Current branch is '${currentBranch}'. Please switch to the default branch '${defaultBranch}' before proceeding.`, - `git checkout ${defaultBranch}`, - ); - } - - const existingPullRequest = await githubClient.getExistingPullRequest(releaseBranch); - - const doesReleasePRExist = !!existingPullRequest; - - if (doesReleasePRExist) { - logger.item("Found existing release pull request"); - } else { - logger.item("Will create new pull request"); - } - - const branchExists = await doesBranchExist(releaseBranch, workspaceRoot); - - return { - existingPullRequest, - doesReleasePRExist, - async prepareBranch() { - if (!branchExists) { - await createBranch(releaseBranch, defaultBranch, workspaceRoot); - } - - // The following operations should be done in the correct order! - // First we will checkout the release branch, then pull the latest changes if it exists remotely, - // then rebase onto the default branch to get the latest changes from main, and only after that - // we will apply our updates. - logger.step(`Checking out release branch: ${releaseBranch}`); - const hasCheckedOut = await checkoutBranch(releaseBranch, workspaceRoot); - if (!hasCheckedOut) { - throw new Error(`Failed to checkout branch: ${releaseBranch}`); - } - - // If the branch already exists, we will just pull the latest changes. - // Since the branch could have been updated remotely since we last checked it out. - if (branchExists) { - logger.step("Pulling latest changes from remote"); - const hasPulled = await pullLatestChanges(releaseBranch, workspaceRoot); - if (!hasPulled) { - logger.warn("Failed to pull latest changes, continuing anyway"); - } - } - - // After we have pulled the latest changes, we will rebase our changes onto the default branch - // to ensure we have the latest updates. - logger.step(`Rebasing onto ${defaultBranch}`); - const rebased = await rebaseBranch(defaultBranch, workspaceRoot); - if (!rebased) { - throw new Error(`Failed to rebase onto ${defaultBranch}. Please resolve conflicts manually.`); - } - }, - async syncChanges(hasChanges: boolean) { - // If there are any changes, we will commit them. - const hasCommitted = hasChanges ? await commitChanges("chore: update release versions", workspaceRoot) : false; - - // Check if branch is ahead of remote (has commits to push) - const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot); - - if (!hasCommitted && !isBranchAhead) { - logger.item("No changes to commit and branch is in sync with remote"); - return false; - } - - // Push with --force-with-lease for safety - logger.step("Pushing changes to remote"); - const pushed = await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true }); - if (!pushed) { - throw new Error(`Failed to push changes to ${releaseBranch}. Remote may have been updated.`); - } - - return true; - }, - async syncPullRequest(updates: PackageRelease[]) { - const prTitle = existingPullRequest?.title || pullRequestTitle || "chore: update package versions"; - const prBody = generatePullRequestBody(updates, pullRequestBody); - - const pullRequest = await githubClient.upsertPullRequest({ - pullNumber: existingPullRequest?.number, - title: prTitle, - body: prBody, - head: releaseBranch, - base: defaultBranch, - }); - - logger.success(`${doesReleasePRExist ? "Updated" : "Created"} pull request: ${pullRequest?.html_url}`); - - return { - pullRequest, - created: !doesReleasePRExist, - }; - }, - async cleanup() { - await checkoutBranch(defaultBranch, workspaceRoot); - }, - }; -} diff --git a/src/services/git.service.ts b/src/services/git.service.ts new file mode 100644 index 0000000..e04d92e --- /dev/null +++ b/src/services/git.service.ts @@ -0,0 +1,117 @@ +import { Command, CommandExecutor } from "@effect/platform"; +import { NodeCommandExecutor } from "@effect/platform-node"; +import { Effect } from "effect"; +import { GitCommandError, GitError } from "../errors"; + +export class GitService extends Effect.Service()("@ucdjs/release-scripts/GitService", { + effect: Effect.gen(function* () { + const executor = yield* CommandExecutor.CommandExecutor; + + const execGitCommand = (args: readonly string[]): Effect.Effect => + executor.string(Command.make("git", ...args)).pipe( + Effect.mapError((error: any) => + new GitCommandError({ + command: `git ${args.join(" ")}`, + exitCode: error.exitCode || 1, + stderr: error.stderr || error.message || "Unknown error", + }), + ), + ); + + const getCurrentBranch: Effect.Effect = Effect.gen(function* () { + const output = yield* execGitCommand(["rev-parse", "--abbrev-ref", "HEAD"]); + const branch = output.trim(); + + if (branch === "HEAD") { + return yield* Effect.fail( + new GitError({ message: "Repository is in detached HEAD state" }), + ); + } + + return branch; + }); + + const listBranches: Effect.Effect = Effect.gen(function* () { + const output = yield* execGitCommand(["branch", "--format=%(refname:short)"]); + return output + .trim() + .split("\n") + .filter((branch: string) => branch.length > 0) + .map((branch: string) => branch.trim()); + }); + + const isRepository: Effect.Effect = execGitCommand(["rev-parse", "--git-dir"]).pipe( + Effect.map(() => true), + Effect.catchAll(() => Effect.succeed(false)), + ); + + const getRemoteUrl: Effect.Effect = execGitCommand(["config", "--get", "remote.origin.url"]).pipe( + Effect.map((output) => { + const url = output.trim(); + return url.length > 0 ? url : undefined; + }), + Effect.catchAll(() => Effect.succeed(undefined)), + ); + + const hasChanges: Effect.Effect = Effect.gen(function* () { + const output = yield* execGitCommand(["status", "--porcelain"]); + return output.trim().length > 0; + }); + + const hasStagedChanges: Effect.Effect = Effect.gen(function* () { + const output = yield* execGitCommand(["diff", "--cached", "--name-only"]); + return output.trim().length > 0; + }); + + const getLastCommitHash: Effect.Effect = Effect.gen(function* () { + const output = yield* execGitCommand(["rev-parse", "HEAD"]); + return output.trim(); + }); + + const branchExists = (branchName: string): Effect.Effect => + execGitCommand(["rev-parse", "--verify", `refs/heads/${branchName}`]).pipe( + Effect.map(() => true), + Effect.catchAll(() => Effect.succeed(false)), + ); + + const addFiles = (files: readonly string[]): Effect.Effect => + execGitCommand(["add", ...files]).pipe(Effect.asVoid); + + const commit = (message: string): Effect.Effect => + execGitCommand(["commit", "-m", message]).pipe(Effect.asVoid); + + const createTag = (tagName: string, message?: string): Effect.Effect => { + const args = message + ? ["tag", "-a", tagName, "-m", message] + : ["tag", tagName]; + return execGitCommand(args).pipe(Effect.asVoid); + }; + + const push = (remote = "origin", branch?: string): Effect.Effect => { + const args = branch ? ["push", remote, branch] : ["push", remote]; + return execGitCommand(args).pipe(Effect.asVoid); + }; + + const pushTags = (remote = "origin"): Effect.Effect => + execGitCommand(["push", remote, "--tags"]).pipe(Effect.asVoid); + + return { + getCurrentBranch, + listBranches, + isRepository, + getRemoteUrl, + hasChanges, + hasStagedChanges, + getLastCommitHash, + branchExists, + addFiles, + commit, + createTag, + push, + pushTags, + } as const; + }), + dependencies: [ + NodeCommandExecutor.layer, + ], +}) {} diff --git a/src/services/github.service.ts b/src/services/github.service.ts new file mode 100644 index 0000000..20950f4 --- /dev/null +++ b/src/services/github.service.ts @@ -0,0 +1,269 @@ +/* eslint-disable no-console */ +import process from "node:process"; +import { Effect, Schema } from "effect"; +import { GitService } from "./git.service.js"; + +// Schema definitions for GitHub API types +export const PullRequestSchema = Schema.Struct({ + number: Schema.Number, + title: Schema.String, + body: Schema.String, + head: Schema.Struct({ + ref: Schema.String, + sha: Schema.String, + }), + base: Schema.Struct({ + ref: Schema.String, + sha: Schema.String, + }), + state: Schema.Literal("open", "closed", "merged"), + draft: Schema.Boolean, + mergeable: Schema.NullOr(Schema.Boolean), + url: Schema.String, + html_url: Schema.String, +}); + +export const CreatePullRequestOptionsSchema = Schema.Struct({ + title: Schema.String, + body: Schema.String, + head: Schema.String, + base: Schema.String, + draft: Schema.optional(Schema.Boolean), +}); + +export const UpdatePullRequestOptionsSchema = Schema.Struct({ + title: Schema.optional(Schema.String), + body: Schema.optional(Schema.String), + state: Schema.optional(Schema.Literal("open", "closed")), +}); + +export const CommitStatusSchema = Schema.Struct({ + state: Schema.Literal("pending", "success", "error", "failure"), + target_url: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + context: Schema.String, +}); + +export const RepositoryInfoSchema = Schema.Struct({ + owner: Schema.String, + repo: Schema.String, +}); + +// Type inference from schemas +export type PullRequest = Schema.Schema.Type; +export type CreatePullRequestOptions = Schema.Schema.Type; +export type UpdatePullRequestOptions = Schema.Schema.Type; +export type CommitStatus = Schema.Schema.Type; +export type RepositoryInfo = Schema.Schema.Type; + +export class GitHubService extends Effect.Service()("@ucdjs/release-scripts/GitHubService", { + effect: Effect.gen(function* () { + const git = yield* GitService; + + const getRepositoryInfo = (): Effect.Effect => + Effect.gen(function* () { + const remoteUrl = yield* git.getRemoteUrl; + if (!remoteUrl) { + return yield* Effect.fail(new Error("No git remote found")); + } + + const repoMatch = remoteUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)(?:\.git)?$/); + if (!repoMatch) { + return yield* Effect.fail(new Error("Could not parse GitHub repository from remote URL")); + } + + const [, owner, repo] = repoMatch; + return yield* Schema.decodeUnknown(RepositoryInfoSchema)({ owner, repo }); + }); + + const makeRequest = ( + endpoint: string, + schema: Schema.Schema, + options: RequestInit = {}, + ): Effect.Effect => Effect.gen(function* () { + const token = process.env.GITHUB_TOKEN; + if (!token) { + return yield* Effect.fail(new Error("GITHUB_TOKEN environment variable is required")); + } + + const repoInfo = yield* getRepositoryInfo(); + const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/${endpoint}`; + + const response = yield* Effect.tryPromise(() => + fetch(url, { + ...options, + headers: { + "Authorization": `token ${token}`, + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json", + ...options.headers, + }, + }), + ).pipe( + Effect.mapError((error) => new Error(`Failed to fetch: ${error}`)), + ); + + if (!response.ok) { + const errorText = yield* Effect.tryPromise(() => response.text()).pipe( + Effect.mapError(() => new Error("Could not read error response")), + ); + return yield* Effect.fail( + new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`), + ); + } + + const data = yield* Effect.tryPromise(() => response.json()).pipe( + Effect.mapError((error) => new Error(`Failed to parse JSON: ${error}`)), + ); + + // Parse and validate the response using the schema + return yield* Schema.decodeUnknown(schema)(data).pipe( + Effect.mapError((error) => new Error(`Schema validation failed: ${error}`)) + ); + }); + + const getPullRequest = (prNumber: number): Effect.Effect => + makeRequest(`pulls/${prNumber}`, PullRequestSchema); + + const createPullRequest = (options: CreatePullRequestOptions): Effect.Effect => + Effect.gen(function* () { + // Validate input + const validatedOptions = yield* Schema.decodeUnknown(CreatePullRequestOptionsSchema)(options); + + return yield* makeRequest("pulls", PullRequestSchema, { + method: "POST", + body: JSON.stringify(validatedOptions), + }); + }); + + const updatePullRequest = ( + prNumber: number, + options: UpdatePullRequestOptions, + ): Effect.Effect => + Effect.gen(function* () { + // Validate input + const validatedOptions = yield* Schema.decodeUnknown(UpdatePullRequestOptionsSchema)(options); + + return yield* makeRequest(`pulls/${prNumber}`, PullRequestSchema, { + method: "PATCH", + body: JSON.stringify(validatedOptions), + }); + }); + + const createCommitStatus = ( + sha: string, + status: CommitStatus, + ): Effect.Effect => + Effect.gen(function* () { + // Validate input + const validatedStatus = yield* Schema.decodeUnknown(CommitStatusSchema)(status); + + const token = process.env.GITHUB_TOKEN; + if (!token) { + return yield* Effect.fail(new Error("GITHUB_TOKEN environment variable is required")); + } + + const repoInfo = yield* getRepositoryInfo(); + const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/statuses/${sha}`; + + const response = yield* Effect.promise(() => + fetch(url, { + method: "POST", + headers: { + "Authorization": `token ${token}`, + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json", + }, + body: JSON.stringify(validatedStatus), + }), + ); + + if (!response.ok) { + const errorText = yield* Effect.promise(() => response.text()); + return yield* Effect.fail( + new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`), + ); + } + }); + + const getCurrentPullRequest = (): Effect.Effect => + Effect.gen(function* () { + const currentBranch = yield* git.getCurrentBranch; + const repoInfo = yield* getRepositoryInfo(); + + const pulls = yield* makeRequest( + `pulls?head=${repoInfo.owner}:${currentBranch}&state=open`, + Schema.Array(PullRequestSchema), + ).pipe( + Effect.catchAll(() => Effect.succeed([])) + ); + + return pulls.length > 0 ? pulls[0]! : null; + }); + + const createPullRequestFromCurrentBranch = ( + options: Omit, + ): Effect.Effect => + Effect.gen(function* () { + const currentBranch = yield* git.getCurrentBranch; + + return yield* createPullRequest({ + ...options, + head: currentBranch, + }); + }); + + const pushCurrentBranch = (remote = "origin"): Effect.Effect => + Effect.gen(function* () { + const currentBranch = yield* git.getCurrentBranch; + yield* git.push(remote, currentBranch); + }); + + const createPullRequestWorkflow = ( + options: Omit & { pushFirst?: boolean }, + ): Effect.Effect => + Effect.gen(function* () { + const { pushFirst = true, ...prOptions } = options; + + // Push current branch if requested + if (pushFirst) { + console.log("📤 Pushing current branch to remote..."); + yield* pushCurrentBranch(); + } + + // Check if PR already exists + const existingPr = yield* getCurrentPullRequest(); + if (existingPr) { + console.log(`✓ Pull request already exists: #${existingPr.number}`); + return existingPr; + } + + // Create new PR + console.log("📝 Creating pull request..."); + const pr = yield* createPullRequestFromCurrentBranch(prOptions); + console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`); + + return pr; + }); + + const setCommitStatusForCurrentBranch = (status: CommitStatus): Effect.Effect => + Effect.gen(function* () { + const commitHash = yield* git.getLastCommitHash; + yield* createCommitStatus(commitHash, status); + }); + + return { + getPullRequest, + createPullRequest, + updatePullRequest, + createCommitStatus, + getCurrentPullRequest, + createPullRequestFromCurrentBranch, + pushCurrentBranch, + createPullRequestWorkflow, + setCommitStatusForCurrentBranch, + getRepositoryInfo, + } as const; + }), + dependencies: [GitService.Default], +}) {} diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts new file mode 100644 index 0000000..40aa7d4 --- /dev/null +++ b/src/services/workspace.service.ts @@ -0,0 +1,129 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { Command, CommandExecutor } from "@effect/platform"; +import { NodeTerminal } from "@effect/platform-node"; +import { Effect, Schema } from "effect"; +import { WorkspaceError } from "../errors.js"; + +const PackageJsonSchema = Schema.Struct({ + name: Schema.String, + dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), + devDependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), +}); + +const WorkspacePackageSchema = Schema.Struct({ + name: Schema.String, + version: Schema.optional(Schema.String), + path: Schema.String, +}); + +// pnpm list --json output can be an array of packages or an object with a packages property +const WorkspaceListSchema = Schema.Union( + Schema.Array(WorkspacePackageSchema), + Schema.transform( + Schema.Struct({ packages: Schema.Array(WorkspacePackageSchema) }), + Schema.Array(WorkspacePackageSchema), + { + decode: (obj) => obj.packages, + encode: (arr) => ({ packages: arr }), + }, + ), +); + +export type WorkspacePackage = Schema.Schema.Type; + +export class WorkspaceService extends Effect.Service()("@ucdjs/release-scripts/WorkspaceService", { + effect: Effect.gen(function* () { + const executor = yield* CommandExecutor.CommandExecutor; + + const listPackages = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json")).pipe( + Effect.flatMap((stdout) => + Effect.try({ + try: () => JSON.parse(stdout), + catch: (e) => + new WorkspaceError({ + message: "Failed to parse pnpm JSON output", + operation: "discover", + cause: e, + }), + }), + ), + Effect.flatMap((json) => + Schema.decodeUnknown(WorkspaceListSchema)(json).pipe( + Effect.mapError( + (e) => + new WorkspaceError({ + message: "Failed to decode pnpm output", + operation: "discover", + cause: e, + }), + ), + ), + ), + Effect.cached, + ); + + const findPackageByName = (name: string) => + listPackages.pipe( + Effect.map((pkgs) => pkgs.find((p) => p.name === name)), + ); + + const readPackageJson = (pkgPath: string) => + Effect.tryPromise({ + try: async () => + JSON.parse( + await fs.readFile(path.join(pkgPath, "package.json"), "utf8"), + ), + catch: (e) => + new WorkspaceError({ + message: `Failed to read package.json for ${pkgPath}`, + cause: e, + operation: "readPackageJson", + }), + }).pipe( + Effect.flatMap((json) => + Schema.decodeUnknown(PackageJsonSchema)(json).pipe( + Effect.mapError( + (e) => + new WorkspaceError({ + message: `Invalid package.json for ${pkgPath}`, + cause: e, + operation: "readPackageJson", + }), + ), + ), + ), + ); + + const getPackageGraph = listPackages.pipe( + Effect.flatMap((pkgs) => + Effect.forEach(pkgs, (p) => + p.path + ? readPackageJson(p.path).pipe( + Effect.map((pj) => ({ + name: p.name, + deps: [ + ...Object.keys(pj.dependencies ?? {}), + ...Object.keys(pj.devDependencies ?? {}), + ], + })), + ) + : Effect.succeed({ name: p.name, deps: [] })).pipe( + Effect.map((entries) => { + const graph: Record = {}; + for (const e of entries) graph[e.name] = e.deps; + return graph; + }), + ), + ), + ); + + return { + listPackages, + findPackageByName, + readPackageJson, + getPackageGraph, + } as const; + }), + dependencies: [], +}) {} diff --git a/src/shared/options.ts b/src/shared/options.ts deleted file mode 100644 index ac17a33..0000000 --- a/src/shared/options.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { ReleaseOptions } from "#release"; -import type { CommitGroup, SharedOptions } from "./types"; -import process from "node:process"; -import { DEFAULT_CHANGELOG_TEMPLATE } from "#core/changelog"; -import { doesBranchExist, getAvailableBranches, getDefaultBranch } from "#core/git"; -import { DEFAULT_PR_BODY_TEMPLATE } from "#core/github"; -import farver from "farver"; -import { exitWithError, logger } from "./utils"; - -export const DEFAULT_COMMIT_GROUPS: CommitGroup[] = [ - { name: "features", title: "Features", types: ["feat"] }, - { name: "fixes", title: "Bug Fixes", types: ["fix", "perf"] }, - { name: "refactor", title: "Refactoring", types: ["refactor"] }, - { name: "docs", title: "Documentation", types: ["docs"] }, -]; - -type DeepRequired = Required<{ - [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired -}>; - -export type NormalizedSharedOptions = DeepRequired> & { - /** - * Repository owner (extracted from repo) - */ - owner: string; - - /** - * Repository name (extracted from repo) - */ - repo: string; -}; - -export function normalizeSharedOptions(options: SharedOptions): NormalizedSharedOptions { - const { - workspaceRoot = process.cwd(), - githubToken = "", - repo: fullRepo, - packages = true, - prompts = { - packages: true, - versions: true, - }, - groups = DEFAULT_COMMIT_GROUPS, - } = options; - - if (!githubToken.trim()) { - exitWithError( - "GitHub token is required", - "Set GITHUB_TOKEN environment variable or pass it in options", - ); - } - - if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) { - exitWithError( - "Repository (repo) is required", - "Specify the repository in 'owner/repo' format (e.g., 'octocat/hello-world')", - ); - } - - const [owner, repo] = fullRepo.split("/"); - if (!owner || !repo) { - exitWithError( - `Invalid repo format: "${fullRepo}"`, - "Expected format: \"owner/repo\" (e.g., \"octocat/hello-world\")", - ); - } - - const normalizedPackages = typeof packages === "object" && !Array.isArray(packages) - ? { - exclude: packages.exclude ?? [], - include: packages.include ?? [], - excludePrivate: packages.excludePrivate ?? false, - } - : packages; - return { - packages: normalizedPackages, - prompts: { - packages: prompts?.packages ?? true, - versions: prompts?.versions ?? true, - }, - workspaceRoot, - githubToken, - owner, - repo, - groups, - }; -} - -export type NormalizedReleaseOptions = DeepRequired> & NormalizedSharedOptions; - -export async function normalizeReleaseOptions(options: ReleaseOptions): Promise { - const normalized = normalizeSharedOptions(options); - - let defaultBranch = options.branch?.default?.trim(); - const releaseBranch = options.branch?.release?.trim() ?? "release/next"; - - if (defaultBranch == null || defaultBranch === "") { - defaultBranch = await getDefaultBranch(normalized.workspaceRoot); - - if (!defaultBranch) { - exitWithError( - "Could not determine default branch", - "Please specify the default branch in options", - ); - } - } - - // Ensure that default branch is available, and not the same as release branch - if (defaultBranch === releaseBranch) { - exitWithError( - `Default branch and release branch cannot be the same: "${defaultBranch}"`, - "Specify different branches for default and release", - ); - } - - const localBranchExists = await doesBranchExist(defaultBranch, normalized.workspaceRoot); - const remoteBranchExists = await doesBranchExist(`origin/${defaultBranch}`, normalized.workspaceRoot); - - if (!localBranchExists && !remoteBranchExists) { - const availableBranches = await getAvailableBranches(normalized.workspaceRoot); - exitWithError( - `Default branch "${defaultBranch}" does not exist in the repository`, - `Couldn't find it locally or on the remote 'origin'.\nAvailable local branches: ${availableBranches.join(", ")}`, - ); - } - - logger.verbose(`Using default branch: ${farver.green(defaultBranch)}`); - - return { - ...normalized, - branch: { - release: releaseBranch, - default: defaultBranch, - }, - safeguards: options.safeguards ?? true, - globalCommitMode: options.globalCommitMode ?? "dependencies", - pullRequest: { - title: options.pullRequest?.title ?? "chore: release new version", - body: options.pullRequest?.body ?? DEFAULT_PR_BODY_TEMPLATE, - }, - changelog: { - enabled: options.changelog?.enabled ?? true, - template: options.changelog?.template ?? DEFAULT_CHANGELOG_TEMPLATE, - }, - }; -} diff --git a/src/shared/utils.ts b/src/shared/utils.ts deleted file mode 100644 index 20105bf..0000000 --- a/src/shared/utils.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - Options as TinyExecOptions, - Result as TinyExecResult, -} from "tinyexec"; -import process from "node:process"; -import readline from "node:readline"; -import farver from "farver"; -import mri from "mri"; -import { exec } from "tinyexec"; - -export const args = mri(process.argv.slice(2)); - -const isDryRun = !!args.dry; -const isVerbose = !!args.verbose; -const isForce = !!args.force; - -export const ucdjsReleaseOverridesPath = ".github/ucdjs-release.overrides.json"; - -export const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false"; - -export const logger = { - info: (...args: unknown[]) => { - // eslint-disable-next-line no-console - console.info(...args); - }, - warn: (...args: unknown[]) => { - console.warn(` ${farver.yellow("⚠")}`, ...args); - }, - error: (...args: unknown[]) => { - console.error(` ${farver.red("✖")}`, ...args); - }, - - // Only log if verbose mode is enabled - verbose: (...args: unknown[]) => { - if (!isVerbose) { - return; - } - if (args.length === 0) { - // eslint-disable-next-line no-console - console.log(); - return; - } - - // If there is more than one argument, and the first is a string, treat it as a highlight - if (args.length > 1 && typeof args[0] === "string") { - // eslint-disable-next-line no-console - console.log(farver.dim(args[0]), ...args.slice(1)); - return; - } - - // eslint-disable-next-line no-console - console.log(...args); - }, - - section: (title: string) => { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log(` ${farver.bold(title)}`); - // eslint-disable-next-line no-console - console.log(` ${farver.gray("─".repeat(title.length + 2))}`); - }, - - emptyLine: () => { - // eslint-disable-next-line no-console - console.log(); - }, - - item: (message: string, ...args: unknown[]) => { - // eslint-disable-next-line no-console - console.log(` ${message}`, ...args); - }, - - step: (message: string) => { - // eslint-disable-next-line no-console - console.log(` ${farver.blue("→")} ${message}`); - }, - - success: (message: string) => { - // eslint-disable-next-line no-console - console.log(` ${farver.green("✓")} ${message}`); - }, - - clearScreen: () => { - const repeatCount = process.stdout.rows - 2; - const blank = repeatCount > 0 ? "\n".repeat(repeatCount) : ""; - // eslint-disable-next-line no-console - console.log(blank); - readline.cursorTo(process.stdout, 0, 0); - readline.clearScreenDown(process.stdout); - }, -}; - -export async function run( - bin: string, - args: string[], - opts: Partial = {}, -): Promise { - return exec(bin, args, { - throwOnError: true, - ...opts, - nodeOptions: { - stdio: "inherit", - ...opts.nodeOptions, - }, - }); -} - -export async function dryRun( - bin: string, - args: string[], - opts?: Partial, -): Promise { - return logger.verbose( - farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), - opts || "", - ); -} - -export const runIfNotDry = isDryRun ? dryRun : run; - -export function exitWithError(message: string, hint?: string): never { - logger.error(farver.bold(message)); - if (hint) { - console.error(farver.gray(` ${hint}`)); - } - - process.exit(1); -} - -if (isDryRun || isVerbose || isForce) { - logger.verbose(farver.inverse(farver.yellow(" Running with special flags "))); - logger.verbose({ isDryRun, isVerbose, isForce }); - logger.verbose(); -} diff --git a/src/verify.ts b/src/verify.ts deleted file mode 100644 index 0c2c3f3..0000000 --- a/src/verify.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { SharedOptions } from "#shared/types"; -import type { VersionOverrides } from "#versioning/version"; -import { join, relative } from "node:path"; -import { checkoutBranch, getCurrentBranch, isWorkingDirectoryClean, readFileFromGit } from "#core/git"; -import { createGitHubClient } from "#core/github"; -import { - discoverWorkspacePackages, -} from "#core/workspace"; -import { normalizeReleaseOptions } from "#shared/options"; -import { exitWithError, logger, ucdjsReleaseOverridesPath } from "#shared/utils"; -import { getGlobalCommitsPerPackage, getWorkspacePackageGroupedCommits } from "#versioning/commits"; -import { - calculateAndPrepareVersionUpdates, - -} from "#versioning/version"; -import { gt } from "semver"; - -export interface VerifyOptions extends SharedOptions { - branch?: { - release?: string; - default?: string; - }; - safeguards?: boolean; -} - -export async function verify(options: VerifyOptions): Promise { - const { - workspaceRoot, - ...normalizedOptions - } = await normalizeReleaseOptions(options); - - if (normalizedOptions.safeguards && !(await isWorkingDirectoryClean(workspaceRoot))) { - exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding."); - } - - const githubClient = createGitHubClient({ - owner: normalizedOptions.owner, - repo: normalizedOptions.repo, - githubToken: normalizedOptions.githubToken, - }); - - const releaseBranch = normalizedOptions.branch.release; - const defaultBranch = normalizedOptions.branch.default; - - const releasePr = await githubClient.getExistingPullRequest(releaseBranch); - - if (!releasePr || !releasePr.head) { - logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`); - return; - } - - logger.info(`Found release PR #${releasePr.number}. Verifying against default branch "${defaultBranch}"...`); - - const originalBranch = await getCurrentBranch(workspaceRoot); - if (originalBranch !== defaultBranch) { - await checkoutBranch(defaultBranch, workspaceRoot); - } - - // Read overrides file from the release branch - const overridesPath = ucdjsReleaseOverridesPath; - let existingOverrides: VersionOverrides = {}; - try { - const overridesContent = await readFileFromGit(workspaceRoot, releasePr.head.sha, overridesPath); - if (overridesContent) { - existingOverrides = JSON.parse(overridesContent); - logger.info("Found existing version overrides file on release branch."); - } - } catch { - logger.info("No version overrides file found on release branch. Continuing..."); - } - - const mainPackages = await discoverWorkspacePackages(workspaceRoot, options); - const mainCommits = await getWorkspacePackageGroupedCommits(workspaceRoot, mainPackages); - - const globalCommitsPerPackage = await getGlobalCommitsPerPackage( - workspaceRoot, - mainCommits, - mainPackages, - normalizedOptions.globalCommitMode, - ); - - const { allUpdates: expectedUpdates } = await calculateAndPrepareVersionUpdates({ - workspacePackages: mainPackages, - packageCommits: mainCommits, - workspaceRoot, - showPrompt: false, - globalCommitsPerPackage, - overrides: existingOverrides, - }); - - const expectedVersionMap = new Map( - expectedUpdates.map((u) => [u.package.name, u.newVersion]), - ); - - // Read package.json versions from the release branch without checking it out - const prVersionMap = new Map(); - for (const pkg of mainPackages) { - const pkgJsonPath = relative(workspaceRoot, join(pkg.path, "package.json")); - const pkgJsonContent = await readFileFromGit(workspaceRoot, releasePr.head.sha, pkgJsonPath); - if (pkgJsonContent) { - const pkgJson = JSON.parse(pkgJsonContent); - prVersionMap.set(pkg.name, pkgJson.version); - } - } - - if (originalBranch !== defaultBranch) { - await checkoutBranch(originalBranch, workspaceRoot); - } - - let isOutOfSync = false; - for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) { - const prVersion = prVersionMap.get(pkgName); - if (!prVersion) { - logger.warn(`Package "${pkgName}" found in default branch but not in release branch. Skipping.`); - continue; - } - - if (gt(expectedVersion, prVersion)) { - logger.error(`Package "${pkgName}" is out of sync. Expected version >= ${expectedVersion}, but PR has ${prVersion}.`); - isOutOfSync = true; - } else { - logger.success(`Package "${pkgName}" is up to date (PR version: ${prVersion}, Expected: ${expectedVersion})`); - } - } - - const statusContext = "ucdjs/release-verify"; - - if (isOutOfSync) { - await githubClient.setCommitStatus({ - sha: releasePr.head.sha, - state: "failure", - context: statusContext, - description: "Release PR is out of sync with the default branch. Please re-run the release process.", - }); - logger.error("Verification failed. Commit status set to 'failure'."); - } else { - await githubClient.setCommitStatus({ - sha: releasePr.head.sha, - state: "success", - context: statusContext, - description: "Release PR is up to date.", - targetUrl: `https://github.com/${normalizedOptions.owner}/${normalizedOptions.repo}/pull/${releasePr.number}`, - }); - logger.success("Verification successful. Commit status set to 'success'."); - } -} diff --git a/src/versioning/commits.ts b/src/versioning/commits.ts deleted file mode 100644 index 7ff4932..0000000 --- a/src/versioning/commits.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { WorkspacePackage } from "#core/workspace"; -import type { BumpKind } from "#shared/types"; -import type { GitCommit } from "commit-parser"; -import { getGroupedFilesByCommitSha, getMostRecentPackageTag } from "#core/git"; -import { logger } from "#shared/utils"; -import { getCommits } from "commit-parser"; -import farver from "farver"; - -export function determineHighestBump(commits: GitCommit[]): BumpKind { - if (commits.length === 0) { - return "none"; - } - - let highestBump: BumpKind = "none"; - - for (const commit of commits) { - const bump = determineBumpType(commit); - // logger.verbose(`Commit ${commit.shortHash} results in a ${bump} bump`); - - // Priority: major > minor > patch > none - if (bump === "major") { - return "major"; // Early exit - can't get higher - } - - if (bump === "minor") { - highestBump = "minor"; - } else if (bump === "patch" && highestBump === "none") { - highestBump = "patch"; - } - } - - return highestBump; -} - -/** - * Get commits grouped by workspace package. - * For each package, retrieves all commits since its last release tag that affect that package. - * - * @param {string} workspaceRoot - The root directory of the workspace - * @param {WorkspacePackage[]} packages - Array of workspace packages to analyze - * @returns {Promise>} A map of package names to their commits since their last release - */ -export async function getWorkspacePackageGroupedCommits( - workspaceRoot: string, - packages: WorkspacePackage[], -): Promise> { - const changedPackages = new Map(); - - const promises = packages.map(async (pkg) => { - // Get the latest tag that corresponds to the workspace package - // This will ensure that we only get commits, since the last release of this package. - const lastTag = await getMostRecentPackageTag(workspaceRoot, pkg.name); - - // Get all commits since the last tag, that affect this package - const allCommits = await getCommits({ - from: lastTag, - to: "HEAD", - cwd: workspaceRoot, - folder: pkg.path, - }); - - logger.verbose( - `Found ${farver.cyan(allCommits.length)} commits for package ${farver.bold( - pkg.name, - )} since tag ${farver.cyan(lastTag ?? "N/A")}`, - ); - - return { - pkgName: pkg.name, - commits: allCommits, - }; - }); - - const results = await Promise.all(promises); - - for (const { pkgName, commits } of results) { - changedPackages.set(pkgName, commits); - } - - return changedPackages; -} - -/** - * Check if a file path touches any package folder. - * @param file - The file path to check - * @param packagePaths - Set of normalized package paths - * @param workspaceRoot - The workspace root for path normalization - * @returns true if the file is inside a package folder - */ -function fileMatchesPackageFolder( - file: string, - packagePaths: Set, - workspaceRoot: string, -): boolean { - // Normalize the file path (remove leading ./) - const normalizedFile = file.startsWith("./") ? file.slice(2) : file; - - for (const pkgPath of packagePaths) { - // Normalize package path (remove workspace root prefix if present) - const normalizedPkgPath = pkgPath.startsWith(workspaceRoot) - ? pkgPath.slice(workspaceRoot.length + 1) - : pkgPath; - - // Check if file is inside this package folder - if ( - normalizedFile.startsWith(`${normalizedPkgPath}/`) - || normalizedFile === normalizedPkgPath - ) { - return true; - } - } - - return false; -} - -/** - * Check if a commit is a "global" commit (doesn't touch any package folder). - * @param workspaceRoot - The workspace root - * @param files - Array of files changed in the commit - * @param packagePaths - Set of normalized package paths - * @returns true if this is a global commit - */ -function isGlobalCommit( - workspaceRoot: string, - files: string[] | undefined, - packagePaths: Set, -): boolean { - if (!files || files.length === 0) { - // If we can't determine files, consider it non-global to be safe - return false; - } - - // A commit is global if NONE of its files touch any package folder - return !files.some((file) => fileMatchesPackageFolder(file, packagePaths, workspaceRoot)); -} - -const DEPENDENCY_FILES = [ - "package.json", - "pnpm-lock.yaml", - "pnpm-workspace.yaml", - "yarn.lock", - "package-lock.json", -] as string[]; - -/** - * Find the oldest and newest commits across all packages. - * @param packageCommits - Map of package commits - * @returns Object with oldest and newest commit SHAs, or null if no commits - */ -function findCommitRange(packageCommits: Map): { oldest: string; newest: string } | null { - let oldestCommit: string | null = null; - let newestCommit: string | null = null; - - for (const commits of packageCommits.values()) { - if (commits.length === 0) continue; - - // Commits are ordered newest to oldest - const firstCommit = commits[0]!.shortHash; - const lastCommit = commits[commits.length - 1]!.shortHash; - - if (!newestCommit) { - newestCommit = firstCommit; - } - oldestCommit = lastCommit; // Will be the last package's oldest - } - - if (!oldestCommit || !newestCommit) return null; - return { oldest: oldestCommit, newest: newestCommit }; -} - -/** - * Get global commits for each package based on their individual commit timelines. - * This solves the problem where packages with different release histories need different global commits. - * - * A "global commit" is a commit that doesn't touch any package folder but may affect all packages - * (e.g., root package.json, CI config, README). - * - * Performance: Makes ONE batched git call to get files for all commits across all packages. - * - * @param workspaceRoot - The root directory of the workspace - * @param packageCommits - Map of package name to their commits (from getWorkspacePackageCommits) - * @param allPackages - All workspace packages (used to identify package folders) - * @param mode - Filter mode: false (disabled), "all" (all global commits), or "dependencies" (only dependency-related) - * @returns Map of package name to their global commits - */ -export async function getGlobalCommitsPerPackage( - workspaceRoot: string, - packageCommits: Map, - allPackages: WorkspacePackage[], - mode?: false | "dependencies" | "all", -): Promise> { - const result = new Map(); - - if (!mode) { - logger.verbose("Global commits mode disabled"); - return result; - } - - logger.verbose(`Computing global commits per-package (mode: ${farver.cyan(mode)})`); - - const commitRange = findCommitRange(packageCommits); - if (!commitRange) { - logger.verbose("No commits found across packages"); - return result; - } - - logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`); - - const commitFilesMap = await getGroupedFilesByCommitSha(workspaceRoot, commitRange.oldest, commitRange.newest); - if (!commitFilesMap) { - logger.warn("Failed to get commit file list, returning empty global commits"); - return result; - } - - logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`); - - const packagePaths = new Set(allPackages.map((p) => p.path)); - - for (const [pkgName, commits] of packageCommits) { - const globalCommitsAffectingPackage: GitCommit[] = []; - - logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`); - - for (const commit of commits) { - const files = commitFilesMap.get(commit.shortHash); - if (!files) continue; - - if (isGlobalCommit(workspaceRoot, files, packagePaths)) { - globalCommitsAffectingPackage.push(commit); - } - } - - logger.verbose("Package global commits found", `${farver.bold(pkgName)}: ${farver.cyan(globalCommitsAffectingPackage.length)} global commits`); - - if (mode === "all") { - result.set(pkgName, globalCommitsAffectingPackage); - continue; - } - - // mode === "dependencies" - const dependencyCommits: GitCommit[] = []; - - for (const commit of globalCommitsAffectingPackage) { - const files = commitFilesMap.get(commit.shortHash); - if (!files) continue; - - const affectsDeps = files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file)); - - if (affectsDeps) { - logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`); - dependencyCommits.push(commit); - } - } - - logger.verbose("Global commits affect dependencies", `${farver.bold(pkgName)}: ${farver.cyan(dependencyCommits.length)} global commits affect dependencies`); - result.set(pkgName, dependencyCommits); - } - - return result; -} - -function determineBumpType(commit: GitCommit): BumpKind { - // Breaking change always results in major bump - if (commit.isBreaking) { - return "major"; - } - - // Non-conventional commits don't trigger bumps - if (!commit.isConventional || !commit.type) { - return "none"; - } - - // Map conventional commit types to bump types - switch (commit.type) { - case "feat": - return "minor"; - - case "fix": - case "perf": - return "patch"; - - // These don't trigger version bumps - case "docs": - case "style": - case "refactor": - case "test": - case "build": - case "ci": - case "chore": - case "revert": - return "none"; - - default: - // Unknown types don't trigger bumps - return "none"; - } -} diff --git a/src/versioning/package.ts b/src/versioning/package.ts deleted file mode 100644 index 6cc2234..0000000 --- a/src/versioning/package.ts +++ /dev/null @@ -1,198 +0,0 @@ -import type { WorkspacePackage } from "#core/workspace"; -import type { - PackageRelease, - PackageUpdateOrder, -} from "#shared/types"; -import { logger } from "#shared/utils"; -import { createVersionUpdate } from "#versioning/version"; - -interface PackageDependencyGraph { - packages: Map; - dependents: Map>; -} - -/** - * Build a dependency graph from workspace packages - * - * Creates a bidirectional graph that maps: - * - packages: Map of package name → WorkspacePackage - * - dependents: Map of package name → Set of packages that depend on it - * - * @param packages - All workspace packages - * @returns Dependency graph with packages and dependents maps - */ -export function buildPackageDependencyGraph( - packages: WorkspacePackage[], -): PackageDependencyGraph { - const packagesMap = new Map(); - const dependents = new Map>(); - - for (const pkg of packages) { - packagesMap.set(pkg.name, pkg); - dependents.set(pkg.name, new Set()); - } - - for (const pkg of packages) { - const allDeps = [ - ...pkg.workspaceDependencies, - ...pkg.workspaceDevDependencies, - ]; - - for (const dep of allDeps) { - const depSet = dependents.get(dep); - if (depSet) { - depSet.add(pkg.name); - } - } - } - - return { - packages: packagesMap, - dependents, - }; -} - -/** - * Get all packages affected by changes (including transitive dependents) - * - * Uses graph traversal to find all packages that need updates: - * - Packages with direct changes - * - All packages that depend on changed packages (transitively) - * - * @param graph - Dependency graph - * @param changedPackages - Set of package names with direct changes - * @returns Set of all package names that need updates - */ -export function getAllAffectedPackages( - graph: PackageDependencyGraph, - changedPackages: Set, -): Set { - const affected = new Set(); - - function visitDependents(pkgName: string) { - if (affected.has(pkgName)) return; - affected.add(pkgName); - - const dependents = graph.dependents.get(pkgName); - if (dependents) { - for (const dependent of dependents) { - visitDependents(dependent); - } - } - } - - // Start traversal from each changed package - for (const pkg of changedPackages) { - visitDependents(pkg); - } - - return affected; -} - -/** - * Calculate the order in which packages should be published - * - * Performs topological sorting to ensure dependencies are published before dependents. - * Assigns a "level" to each package based on its depth in the dependency tree. - * - * This is used by the publish command to publish packages in the correct order. - * - * @param graph - Dependency graph - * @param packagesToPublish - Set of package names to publish - * @returns Array of packages in publish order with their dependency level - */ -export function getPackagePublishOrder( - graph: PackageDependencyGraph, - packagesToPublish: Set, -): PackageUpdateOrder[] { - const result: PackageUpdateOrder[] = []; - const visited = new Set(); - const toUpdate = new Set(packagesToPublish); - - const packagesToProcess = new Set(packagesToPublish); - for (const pkg of packagesToPublish) { - const deps = graph.dependents.get(pkg); - if (deps) { - for (const dep of deps) { - packagesToProcess.add(dep); - toUpdate.add(dep); - } - } - } - - function visit(pkgName: string, level: number) { - if (visited.has(pkgName)) return; - visited.add(pkgName); - - const pkg = graph.packages.get(pkgName); - if (!pkg) return; - - const allDeps = [ - ...pkg.workspaceDependencies, - ...pkg.workspaceDevDependencies, - ]; - - let maxDepLevel = level; - for (const dep of allDeps) { - if (toUpdate.has(dep)) { - visit(dep, level); - const depResult = result.find((r) => r.package.name === dep); - if (depResult && depResult.level >= maxDepLevel) { - maxDepLevel = depResult.level + 1; - } - } - } - - result.push({ package: pkg, level: maxDepLevel }); - } - - for (const pkg of toUpdate) { - visit(pkg, 0); - } - - result.sort((a, b) => a.level - b.level); - - return result; -} - -/** - * Create version updates for all packages affected by dependency changes - * - * When a package is updated, all packages that depend on it should also be updated. - * This function calculates which additional packages need patch bumps due to dependency changes. - * - * @param graph - Dependency graph - * @param workspacePackages - All workspace packages - * @param directUpdates - Packages with direct code changes - * @returns All updates including dependent packages that need patch bumps - */ -export function createDependentUpdates( - graph: PackageDependencyGraph, - workspacePackages: WorkspacePackage[], - directUpdates: PackageRelease[], -): PackageRelease[] { - const allUpdates = [...directUpdates]; - const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u])); - const changedPackages = new Set(directUpdates.map((u) => u.package.name)); - - // Get all packages affected by changes (including transitive dependents) - const affectedPackages = getAllAffectedPackages(graph, changedPackages); - - // Create updates for packages that don't have direct updates - for (const pkgName of affectedPackages) { - logger.verbose(`Processing affected package: ${pkgName}`); - // Skip if already has a direct update - if (directUpdateMap.has(pkgName)) { - logger.verbose(`Skipping ${pkgName}, already has a direct update`); - continue; - } - - const pkg = workspacePackages.find((p) => p.name === pkgName); - if (!pkg) continue; - - // This package needs a patch bump because its dependencies changed - allUpdates.push(createVersionUpdate(pkg, "patch", false)); - } - - return allUpdates; -} diff --git a/src/versioning/version.ts b/src/versioning/version.ts deleted file mode 100644 index 207221d..0000000 --- a/src/versioning/version.ts +++ /dev/null @@ -1,426 +0,0 @@ -import type { WorkspacePackage } from "#core/workspace"; -import type { BumpKind, PackageJson, PackageRelease } from "#shared/types"; -import type { GitCommit } from "commit-parser"; -import { readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { selectVersionPrompt } from "#core/prompts"; -import { isCI, logger } from "#shared/utils"; -import { determineHighestBump } from "#versioning/commits"; -import { buildPackageDependencyGraph, createDependentUpdates } from "#versioning/package"; -import farver from "farver"; - -export function isValidSemver(version: string): boolean { - // Basic semver validation: X.Y.Z with optional pre-release/build metadata - const semverRegex = /^\d+\.\d+\.\d+(?:[-+].+)?$/; - return semverRegex.test(version); -} - -export function getNextVersion(currentVersion: string, bump: BumpKind): string { - if (bump === "none") { - logger.verbose(`No version bump needed, keeping version ${currentVersion}`); - return currentVersion; - } - - if (!isValidSemver(currentVersion)) { - throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`); - } - - // eslint-disable-next-line regexp/no-super-linear-backtracking - const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/); - if (!match) { - throw new Error(`Invalid semver version: ${currentVersion}`); - } - - const [, major, minor, patch] = match; - let newMajor = Number.parseInt(major!, 10); - let newMinor = Number.parseInt(minor!, 10); - let newPatch = Number.parseInt(patch!, 10); - - switch (bump) { - case "major": - newMajor += 1; - newMinor = 0; - newPatch = 0; - break; - - case "minor": - newMinor += 1; - newPatch = 0; - break; - - case "patch": - newPatch += 1; - break; - } - - return `${newMajor}.${newMinor}.${newPatch}`; -} - -export function createVersionUpdate( - pkg: WorkspacePackage, - bump: BumpKind, - hasDirectChanges: boolean, -): PackageRelease { - const newVersion = getNextVersion(pkg.version, bump); - - return { - package: pkg, - currentVersion: pkg.version, - newVersion, - bumpType: bump, - hasDirectChanges, - }; -} - -function _calculateBumpType(oldVersion: string, newVersion: string): BumpKind { - if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) { - throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`); - } - - const oldParts = oldVersion.split(".").map(Number); - const newParts = newVersion.split(".").map(Number); - - if (newParts[0]! > oldParts[0]!) return "major"; - if (newParts[1]! > oldParts[1]!) return "minor"; - if (newParts[2]! > oldParts[2]!) return "patch"; - - return "none"; -} - -const messageColorMap: Record string> = { - feat: farver.green, - feature: farver.green, - - refactor: farver.cyan, - style: farver.cyan, - - docs: farver.blue, - doc: farver.blue, - types: farver.blue, - type: farver.blue, - - chore: farver.gray, - ci: farver.gray, - build: farver.gray, - deps: farver.gray, - dev: farver.gray, - - fix: farver.yellow, - test: farver.yellow, - - perf: farver.magenta, - - revert: farver.red, - breaking: farver.red, -}; - -function formatCommitsForDisplay(commits: GitCommit[]): string { - if (commits.length === 0) { - return farver.dim("No commits found"); - } - - const maxCommitsToShow = 10; - const commitsToShow = commits.slice(0, maxCommitsToShow); - const hasMore = commits.length > maxCommitsToShow; - - const typeLength = commits.map(({ type }) => type.length).reduce((a, b) => Math.max(a, b), 0); - const scopeLength = commits.map(({ scope }) => scope?.length).reduce((a, b) => Math.max(a || 0, b || 0), 0) || 0; - - const formattedCommits = commitsToShow.map((commit) => { - let color = messageColorMap[commit.type] || ((c: string) => c); - if (commit.isBreaking) { - color = (s) => farver.inverse.red(s); - } - - const paddedType = commit.type.padStart(typeLength + 1, " "); - const paddedScope = !commit.scope - ? " ".repeat(scopeLength ? scopeLength + 2 : 0) - : farver.dim("(") + commit.scope + farver.dim(")") + " ".repeat(scopeLength - commit.scope.length); - - return [ - farver.dim(commit.shortHash), - " ", - color === farver.gray ? color(paddedType) : farver.bold(color(paddedType)), - " ", - paddedScope, - farver.dim(":"), - " ", - color === farver.gray ? color(commit.description) : commit.description, - ].join(""); - }).join("\n"); - - if (hasMore) { - return `${formattedCommits}\n ${farver.dim(`... and ${commits.length - maxCommitsToShow} more commits`)}`; - } - - return formattedCommits; -} - -export interface VersionOverride { - type: BumpKind; - version: string; -} - -export type VersionOverrides = Record; - -interface CalculateVersionUpdatesOptions { - workspacePackages: WorkspacePackage[]; - packageCommits: Map; - workspaceRoot: string; - showPrompt?: boolean; - globalCommitsPerPackage: Map; - overrides?: VersionOverrides; -} - -async function calculateVersionUpdates({ - workspacePackages, - packageCommits, - workspaceRoot, - showPrompt, - globalCommitsPerPackage, - overrides: initialOverrides = {}, -}: CalculateVersionUpdatesOptions): Promise<{ - updates: PackageRelease[]; - overrides: VersionOverrides; -}> { - const versionUpdates: PackageRelease[] = []; - const processedPackages = new Set(); - const newOverrides: VersionOverrides = { ...initialOverrides }; - - const bumpRanks: Record = { major: 3, minor: 2, patch: 1, none: 0 }; - - logger.verbose(`Starting version inference for ${packageCommits.size} packages with commits`); - - // First pass: process packages with commits - for (const [pkgName, pkgCommits] of packageCommits) { - const pkg = workspacePackages.find((p) => p.name === pkgName); - if (!pkg) { - logger.error(`Package ${pkgName} not found in workspace packages, skipping`); - continue; - } - - processedPackages.add(pkgName); - - const globalCommits = globalCommitsPerPackage.get(pkgName) || []; - const allCommitsForPackage = [...pkgCommits, ...globalCommits]; - - const determinedBump = determineHighestBump(allCommitsForPackage); - const override = newOverrides[pkgName]; - const effectiveBump = override?.type || determinedBump; - - if (effectiveBump === "none") { - continue; - } - - let newVersion = override?.version || getNextVersion(pkg.version, effectiveBump); - let finalBumpType: BumpKind = effectiveBump; - - if (!isCI && showPrompt) { - logger.clearScreen(); - logger.section(`📝 Commits for ${farver.cyan(pkg.name)}`); - const commitDisplay = formatCommitsForDisplay(allCommitsForPackage); - const commitLines = commitDisplay.split("\n"); - commitLines.forEach((line) => logger.item(line)); - logger.emptyLine(); - - const selectedVersion = await selectVersionPrompt( - workspaceRoot, - pkg, - pkg.version, - newVersion, - ); - - if (selectedVersion === null) continue; - - const userBump = _calculateBumpType(pkg.version, selectedVersion); - finalBumpType = userBump; - - if (bumpRanks[userBump] < bumpRanks[determinedBump]) { - newOverrides[pkgName] = { type: userBump, version: selectedVersion }; - logger.info(`Version override recorded for ${pkgName}: ${determinedBump} → ${userBump}`); - } else if (newOverrides[pkgName] && bumpRanks[userBump] >= bumpRanks[determinedBump]) { - // If the user manually selects a version that's NOT a downgrade, - // remove any existing override for that package. - delete newOverrides[pkgName]; - logger.info(`Version override removed for ${pkgName}.`); - } - - newVersion = selectedVersion; - } - - versionUpdates.push({ - package: pkg, - currentVersion: pkg.version, - newVersion, - bumpType: finalBumpType, - hasDirectChanges: allCommitsForPackage.length > 0, - }); - } - - // Second pass for manual bumps (if not in verify mode) - if (!isCI && showPrompt) { - for (const pkg of workspacePackages) { - if (processedPackages.has(pkg.name)) continue; - - logger.clearScreen(); - logger.section(`📦 Package: ${pkg.name}`); - logger.item("No direct commits found"); - - const newVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, pkg.version); - if (newVersion === null) break; - - if (newVersion !== pkg.version) { - const bumpType = _calculateBumpType(pkg.version, newVersion); - versionUpdates.push({ - package: pkg, - currentVersion: pkg.version, - newVersion, - bumpType, - hasDirectChanges: false, - }); - // We don't record an override here as there was no automatic bump to override. - } - } - } - - return { updates: versionUpdates, overrides: newOverrides }; -} - -/** - * Calculate version updates and prepare dependent updates - * Returns both the updates and a function to apply them - */ -export async function calculateAndPrepareVersionUpdates({ - workspacePackages, - packageCommits, - workspaceRoot, - showPrompt, - globalCommitsPerPackage, - overrides, -}: CalculateVersionUpdatesOptions): Promise<{ - allUpdates: PackageRelease[]; - applyUpdates: () => Promise; - overrides: VersionOverrides; -}> { - // Calculate direct version updates - const { updates: directUpdates, overrides: newOverrides } = await calculateVersionUpdates({ - workspacePackages, - packageCommits, - workspaceRoot, - showPrompt, - globalCommitsPerPackage, - overrides, - }); - - // Build dependency graph and calculate dependent updates - const graph = buildPackageDependencyGraph(workspacePackages); - const allUpdates = createDependentUpdates(graph, workspacePackages, directUpdates); - - // Create apply function that updates all package.json files - const applyUpdates = async () => { - await Promise.all( - allUpdates.map(async (update: PackageRelease) => { - const depUpdates = getDependencyUpdates(update.package, allUpdates); - await updatePackageJson( - update.package, - update.newVersion, - depUpdates, - ); - }), - ); - }; - - return { - allUpdates, - applyUpdates, - overrides: newOverrides, - }; -} - -async function updatePackageJson( - pkg: WorkspacePackage, - newVersion: string, - dependencyUpdates: Map, -): Promise { - const packageJsonPath = join(pkg.path, "package.json"); - - // Read current package.json - const content = await readFile(packageJsonPath, "utf-8"); - const packageJson: PackageJson = JSON.parse(content); - - // Update version - packageJson.version = newVersion; - - function updateDependency( - deps: Record | undefined, - depName: string, - depVersion: string, - isPeerDependency = false, - ): void { - if (!deps) return; - - const oldVersion = deps[depName]; - if (!oldVersion) return; - - if (oldVersion === "workspace:*") { - // Don't update workspace protocol dependencies - // PNPM will handle this automatically - logger.verbose(` - Skipping workspace:* dependency: ${depName}`); - return; - } - - if (isPeerDependency) { - // For peer dependencies, use a looser range to avoid version conflicts - // Match the major version to maintain compatibility - const majorVersion = depVersion.split(".")[0]; - deps[depName] = `>=${depVersion} <${Number(majorVersion) + 1}.0.0`; - } else { - deps[depName] = `^${depVersion}`; - } - - logger.verbose(` - Updated dependency ${depName}: ${oldVersion} → ${deps[depName]}`); - } - - // Update workspace dependencies - for (const [depName, depVersion] of dependencyUpdates) { - updateDependency(packageJson.dependencies, depName, depVersion); - updateDependency(packageJson.devDependencies, depName, depVersion); - updateDependency(packageJson.peerDependencies, depName, depVersion, true); - } - - // Write back with formatting - const updated = `${JSON.stringify(packageJson, null, 2)}\n`; - await writeFile(packageJsonPath, updated, "utf-8"); - logger.verbose(` - Successfully wrote updated package.json`); -} - -/** - * Get all dependency updates needed for a package - */ -function getDependencyUpdates( - pkg: WorkspacePackage, - allUpdates: PackageRelease[], -): Map { - const updates = new Map(); - - // Check all workspace dependencies - const allDeps = [ - ...pkg.workspaceDependencies, - ...pkg.workspaceDevDependencies, - ]; - - for (const dep of allDeps) { - // Find if this dependency is being updated - const update = allUpdates.find((u) => u.package.name === dep); - if (update) { - logger.verbose(` - Dependency ${dep} will be updated: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`); - updates.set(dep, update.newVersion); - } - } - - if (updates.size === 0) { - logger.verbose(` - No dependency updates needed`); - } - - return updates; -} diff --git a/test/_shared.ts b/test/_shared.ts deleted file mode 100644 index 758aee8..0000000 --- a/test/_shared.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { GitHubClient } from "#core/github"; -import type { WorkspacePackage } from "#core/workspace"; -import type { NormalizedReleaseOptions } from "#shared/options"; -import type { GitCommit } from "commit-parser"; -import { DEFAULT_COMMIT_GROUPS } from "#shared/options"; - -export function createCommit(overrides: Partial = {}): GitCommit { - const message = overrides.message ?? overrides.description ?? "feat: add feature"; - const description = overrides.description ?? message.split("\n")[0]!; - - return { - hash: overrides.hash ?? "abc1234567890", - shortHash: overrides.shortHash ?? "abc1234", - message, - description, - type: overrides.type ?? "feat", - scope: overrides.scope, - isConventional: overrides.isConventional ?? true, - isBreaking: overrides.isBreaking ?? false, - body: overrides.body, - references: overrides.references ?? [], - authors: overrides.authors ?? [ - { name: "Test Author", email: "author@example.com" }, - ], - ...overrides, - } as GitCommit; -} - -export function createGitHubClientStub(overrides: Partial = {}): GitHubClient { - const stub: Partial = { - resolveAuthorInfo: async (info) => info, - ...overrides, - }; - - return stub as GitHubClient; -} - -export function createNormalizedReleaseOptions( - overrides: Partial = {}, -): NormalizedReleaseOptions { - const base: NormalizedReleaseOptions = { - packages: true, - prompts: { - packages: true, - versions: true, - }, - workspaceRoot: overrides.workspaceRoot ?? process.cwd(), - githubToken: "test-token", - owner: overrides.owner ?? "ucdjs", - repo: overrides.repo ?? "test-repo", - groups: overrides.groups ?? DEFAULT_COMMIT_GROUPS, - branch: { - release: "release/next", - default: "main", - }, - safeguards: true, - globalCommitMode: "dependencies", - pullRequest: { - title: "chore: release", - body: "Release body", - }, - changelog: { - enabled: true, - template: "", - }, - }; - - return { - ...base, - ...overrides, - branch: { - ...base.branch, - ...overrides.branch, - }, - prompts: { - ...base.prompts, - ...overrides.prompts, - }, - pullRequest: { - ...base.pullRequest, - ...overrides.pullRequest, - }, - changelog: { - ...base.changelog, - ...overrides.changelog, - }, - }; -} - -export function createWorkspacePackage( - path: string, - overrides: Partial = {}, -): WorkspacePackage { - const name = overrides.name ?? "@ucdjs/test"; - const version = overrides.version ?? "0.0.0"; - - return { - name, - version, - path, - packageJson: overrides.packageJson ?? { name, version }, - workspaceDependencies: overrides.workspaceDependencies ?? [], - workspaceDevDependencies: overrides.workspaceDevDependencies ?? [], - ...overrides, - }; -} - -export function createChangelogTestContext(workspaceRoot: string, overrides: { - normalizedOptions?: Partial; - workspacePackage?: Partial; - githubClient?: Partial; -} = {}) { - const normalizedOptions = createNormalizedReleaseOptions({ - workspaceRoot, - ...overrides.normalizedOptions, - }); - - const workspacePackage = createWorkspacePackage(workspaceRoot, overrides.workspacePackage); - const githubClient = createGitHubClientStub(overrides.githubClient); - - return { - normalizedOptions, - workspacePackage, - githubClient, - }; -} diff --git a/test/core/changelog.authors.test.ts b/test/core/changelog.authors.test.ts deleted file mode 100644 index 1f0fdb3..0000000 --- a/test/core/changelog.authors.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { GitHubClient } from "#core/github"; -import { generateChangelogEntry } from "#core/changelog"; -import { DEFAULT_COMMIT_GROUPS } from "#shared/options"; -import { describe, expect, it, vi } from "vitest"; -import { createCommit } from "../_shared"; - -describe("generateChangelogEntry author rendering", () => { - it("includes resolved GitHub handles for commit authors", async () => { - const commits = [ - createCommit({ - references: [ - { type: "pull-request", value: "#123" }, - ], - }), - ]; - - const githubClient = { - resolveAuthorInfo: vi.fn(async (info) => { - if (!info.login) { - info.login = info.email.split("@")[0]!; - } - return info; - }), - } as unknown as GitHubClient; - - const entry = await generateChangelogEntry({ - packageName: "@ucdjs/test", - version: "1.0.1", - previousVersion: "1.0.0", - date: "2025-11-18", - commits, - owner: "ucdjs", - repo: "release-scripts", - groups: DEFAULT_COMMIT_GROUPS, - githubClient, - }); - - expect(entry).toContain("(by [@author](https://github.com/author))"); - expect(githubClient.resolveAuthorInfo).toHaveBeenCalledTimes(1); - }); -}); diff --git a/test/core/changelog.test.ts b/test/core/changelog.test.ts deleted file mode 100644 index 0734385..0000000 --- a/test/core/changelog.test.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { generateChangelogEntry, parseChangelog, updateChangelog } from "#core/changelog"; -import { DEFAULT_COMMIT_GROUPS } from "#shared/options"; -import { dedent } from "@luxass/utils"; -import * as tinyexec from "tinyexec"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { testdir } from "vitest-testdirs"; -import { - createChangelogTestContext, - createCommit, - createGitHubClientStub, -} from "../_shared"; - -vi.mock("tinyexec"); -const mockExec = vi.mocked(tinyexec.x); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("generateChangelogEntry", () => { - const baseEntryOptions = { - packageName: "@ucdjs/test", - owner: "ucdjs", - repo: "test-repo", - } as const; - - it("should generate a changelog entry with features", async () => { - const commits = [ - createCommit({ - type: "feat", - message: "feat: add new feature\n\nFixes #123", - hash: "abc1234567890", - shortHash: "abc1234", - references: [{ type: "issue", value: "#123" }], - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.2.0", - previousVersion: "0.1.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(` - "## [0.2.0](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.2.0) (2025-01-16) - - - ### Features - * feat: add new feature ([Issue #123](https://github.com/ucdjs/test-repo/issues/123)) ([abc1234](https://github.com/ucdjs/test-repo/commit/abc1234567890)) (by Test Author)" - `); - }); - - it("should generate a changelog entry with bug fixes", async () => { - const commits = [ - createCommit({ - type: "fix", - message: "fix: fix critical bug", - hash: "def5678901234", - shortHash: "def5678", - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.1.1", - previousVersion: "0.1.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(` - "## [0.1.1](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.1.1) (2025-01-16) - - - ### Bug Fixes - * fix: fix critical bug ([def5678](https://github.com/ucdjs/test-repo/commit/def5678901234)) (by Test Author)" - `); - }); - - it("should handle multiple commit types", async () => { - const commits = [ - createCommit({ - type: "feat", - message: "feat: add feature A", - hash: "aaa1111111111", - shortHash: "aaa1111", - }), - createCommit({ - type: "fix", - message: "fix: fix bug B\n\nCloses #456", - hash: "bbb2222222222", - shortHash: "bbb2222", - references: [{ type: "issue", value: "#456" }], - }), - createCommit({ - type: "chore", - message: "chore: update dependencies", - hash: "ccc3333333333", - shortHash: "ccc3333", - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.3.0", - previousVersion: "0.2.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(` - "## [0.3.0](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.2.0...@ucdjs/test@0.3.0) (2025-01-16) - - - ### Features - * feat: add feature A ([aaa1111](https://github.com/ucdjs/test-repo/commit/aaa1111111111)) (by Test Author) - - ### Bug Fixes - * fix: fix bug B ([Issue #456](https://github.com/ucdjs/test-repo/issues/456)) ([bbb2222](https://github.com/ucdjs/test-repo/commit/bbb2222222222)) (by Test Author)" - `); - }); - - it("should handle first release without previous version", async () => { - const commits = [ - createCommit({ - type: "feat", - message: "feat: initial release", - hash: "initial123", - shortHash: "initial", - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.1.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(` - "## 0.1.0 (2025-01-16) - - - ### Features - * feat: initial release ([initial](https://github.com/ucdjs/test-repo/commit/initial123)) (by Test Author)" - `); - }); - - it("should group perf commits with bug fixes", async () => { - const commits = [ - createCommit({ - type: "perf", - message: "perf: improve performance", - hash: "perf123456789", - shortHash: "perf123", - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.1.1", - previousVersion: "0.1.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(` - "## [0.1.1](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.1.1) (2025-01-16) - - - ### Bug Fixes - * perf: improve performance ([perf123](https://github.com/ucdjs/test-repo/commit/perf123456789)) (by Test Author)" - `); - }); - - it("should handle non-conventional commits", async () => { - const commits = [ - createCommit({ - message: "some random commit", - hash: "random12345678", - shortHash: "random1", - isConventional: false, - }), - ]; - - const entry = await generateChangelogEntry({ - ...baseEntryOptions, - version: "0.1.1", - previousVersion: "0.1.0", - date: "2025-01-16", - commits, - groups: DEFAULT_COMMIT_GROUPS, - githubClient: createGitHubClientStub(), - }); - - expect(entry).toMatchInlineSnapshot(`"## [0.1.1](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.1.1) (2025-01-16)"`); - }); -}); - -describe("parseChangelog", () => { - it("should parse changelog with package name", () => { - const content = dedent` - # @ucdjs/test - - ## 0.1.0 (2025-01-16) - - ### Features - - * initial release - `; - - const parsed = parseChangelog(content); - - expect(parsed.packageName).toBe("@ucdjs/test"); - expect(parsed.headerLineEnd).toBe(0); - expect(parsed.versions).toHaveLength(1); - expect(parsed.versions[0]!.version).toBe("0.1.0"); - }); - - it("should parse Vite-style changelog entries", () => { - const content = dedent` - # @ucdjs/test - - ## [0.2.0](https://github.com/ucdjs/test/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.2.0) (2025-01-16) - - ### Features - - * new feature - - ## [0.1.0](https://github.com/ucdjs/test/compare/@ucdjs/test@0.0.1...@ucdjs/test@0.1.0) (2025-01-15) - - ### Bug Fixes - - * fix bug - `; - - const parsed = parseChangelog(content); - - expect(parsed.versions).toHaveLength(2); - expect(parsed.versions[0]!.version).toBe("0.2.0"); - expect(parsed.versions[1]!.version).toBe("0.1.0"); - }); - - it("should parse Changesets-style changelog entries", () => { - const content = dedent` - # @ucdjs/test - - ## 0.1.0 - - ### Minor Changes - - - [#172](https://github.com/ucdjs/test/pull/172) feat: add initial package - `; - - const parsed = parseChangelog(content); - - expect(parsed.versions).toHaveLength(1); - expect(parsed.versions[0]!.version).toBe("0.1.0"); - expect(parsed.versions[0]!.content).toContain("Minor Changes"); - }); - - it("should parse mixed Vite and Changesets entries", () => { - const content = dedent` - # @ucdjs/test - - ## [0.2.0](https://github.com/ucdjs/test/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.2.0) (2025-01-16) - - ### Features - - * new feature - - ## 0.1.0 - - ### Minor Changes - - - [#172](https://github.com/ucdjs/test/pull/172) feat: initial release - `; - - const parsed = parseChangelog(content); - - expect(parsed.versions).toHaveLength(2); - expect(parsed.versions[0]!.version).toBe("0.2.0"); - expect(parsed.versions[1]!.version).toBe("0.1.0"); - }); - - it("should handle changelog without package name", () => { - const content = dedent` - ## 0.1.0 (2025-01-16) - - ### Features - - * initial release - `; - - const parsed = parseChangelog(content); - - expect(parsed.packageName).toBeNull(); - expect(parsed.versions).toHaveLength(1); - }); - - it("should handle empty changelog", () => { - const content = "# @ucdjs/test\n"; - - const parsed = parseChangelog(content); - - expect(parsed.packageName).toBe("@ucdjs/test"); - expect(parsed.versions).toHaveLength(0); - }); - - it("should handle changelog with tags", () => { - const content = dedent` - # @ucdjs/test - - ## [0.1.0](https://github.com/ucdjs/test/compare/v0.0.1...v0.1.0) (2025-01-16) - - ### Bug Fixes - - * fix something - `; - - const parsed = parseChangelog(content); - - expect(parsed.versions).toHaveLength(1); - expect(parsed.versions[0]!.version).toBe("0.1.0"); - }); -}); - -describe("updateChangelog", () => { - it("should create a new changelog file", async () => { - const testdirPath = await testdir({}); - const { normalizedOptions, workspacePackage, githubClient } = createChangelogTestContext(testdirPath); - - mockExec.mockRejectedValue(new Error("fatal: path 'CHANGELOG.md' does not exist")); - - const commits = [ - createCommit({ - type: "feat", - message: "feat: add new feature", - hash: "abc123", - shortHash: "abc123", - }), - ]; - - await updateChangelog({ - normalizedOptions, - workspacePackage, - version: "0.1.0", - commits, - date: "2025-01-16", - githubClient, - }); - - const content = await readFile(join(testdirPath, "CHANGELOG.md"), "utf-8"); - - expect(content).toMatchInlineSnapshot(` - "# @ucdjs/test - - ## 0.1.0 (2025-01-16) - - - ### Features - * feat: add new feature ([abc123](https://github.com/ucdjs/test-repo/commit/abc123)) (by Test Author) - " - `); - }); - - it("should insert new version above existing entries", async () => { - const testdirPath = await testdir({}); - const context = createChangelogTestContext(testdirPath); - - const commits = [ - createCommit({ - type: "feat", - message: "feat: add feature B", - hash: "def456", - shortHash: "def456", - }), - ]; - - mockExec.mockRejectedValueOnce(new Error("fatal: path 'CHANGELOG.md' does not exist")); - - await updateChangelog({ - normalizedOptions: context.normalizedOptions, - workspacePackage: context.workspacePackage, - version: "0.1.0", - commits: [ - createCommit({ - type: "feat", - message: "feat: initial release", - hash: "abc123", - shortHash: "abc123", - }), - ], - date: "2025-01-15", - githubClient: context.githubClient, - }); - - const existingChangelog = await readFile(join(testdirPath, "CHANGELOG.md"), "utf-8"); - - mockExec.mockResolvedValueOnce({ stdout: existingChangelog, stderr: "", exitCode: 0 }); - - await updateChangelog({ - normalizedOptions: context.normalizedOptions, - workspacePackage: context.workspacePackage, - version: "0.2.0", - previousVersion: "0.1.0", - commits, - date: "2025-01-16", - githubClient: context.githubClient, - }); - - const content = await readFile(join(testdirPath, "CHANGELOG.md"), "utf-8"); - - expect(content).toMatchInlineSnapshot(` - "# @ucdjs/test - - ## [0.2.0](https://github.com/ucdjs/test-repo/compare/@ucdjs/test@0.1.0...@ucdjs/test@0.2.0) (2025-01-16) - - - ### Features - * feat: add feature B ([def456](https://github.com/ucdjs/test-repo/commit/def456)) (by Test Author) - - - ## 0.1.0 (2025-01-15) - - - ### Features - * feat: initial release ([abc123](https://github.com/ucdjs/test-repo/commit/abc123)) (by Test Author) - " - `); - }); - - it("should replace existing version entry (PR update)", async () => { - const testdirPath = await testdir({}); - const context = createChangelogTestContext(testdirPath); - - mockExec.mockRejectedValueOnce(new Error("fatal: path 'CHANGELOG.md' does not exist")); - - await updateChangelog({ - normalizedOptions: context.normalizedOptions, - workspacePackage: context.workspacePackage, - version: "0.2.0", - commits: [ - createCommit({ - type: "feat", - message: "feat: add feature A", - hash: "abc123", - shortHash: "abc123", - }), - ], - date: "2025-01-16", - githubClient: context.githubClient, - }); - - mockExec.mockRejectedValueOnce(new Error("fatal: path 'CHANGELOG.md' does not exist")); - - await updateChangelog({ - normalizedOptions: context.normalizedOptions, - workspacePackage: context.workspacePackage, - version: "0.2.0", - commits: [ - createCommit({ - type: "feat", - message: "feat: add feature A", - hash: "abc123", - shortHash: "abc123", - }), - createCommit({ - type: "feat", - message: "feat: add feature B", - hash: "def456", - shortHash: "def456", - }), - ], - date: "2025-01-16", - githubClient: context.githubClient, - }); - - const content = await readFile(join(testdirPath, "CHANGELOG.md"), "utf-8"); - const parsed = parseChangelog(content); - - expect(parsed.versions).toHaveLength(1); - expect(parsed.versions[0]!.version).toBe("0.2.0"); - expect(content).toContain("add feature A"); - expect(content).toContain("add feature B"); - }); -}); diff --git a/test/core/git.test.ts b/test/core/git.test.ts deleted file mode 100644 index 38f4f40..0000000 --- a/test/core/git.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { - createBranch, - doesBranchExist, - getAvailableBranches, - getCurrentBranch, - getDefaultBranch, - getMostRecentPackageTag, - isWorkingDirectoryClean, -} from "#core/git"; -import { logger } from "#shared/utils"; -import * as tinyexec from "tinyexec"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("tinyexec"); -const mockExec = vi.mocked(tinyexec.exec); - -beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(logger, "error").mockImplementation(() => {}); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("git utilities", () => { - describe("isWorkingDirectoryClean", () => { - it("should return true if working directory is clean", async () => { - mockExec.mockResolvedValue({ - stdout: "", - stderr: "", - exitCode: 0, - }); - - const result = await isWorkingDirectoryClean("/workspace"); - expect(mockExec).toHaveBeenCalledWith( - "git", - ["status", "--porcelain"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - - expect(result).toBe(true); - }); - - it("should return false if working directory has uncommitted changes", async () => { - mockExec.mockResolvedValue({ - stdout: " M src/index.ts\n", - stderr: "", - exitCode: 0, - }); - - const result = await isWorkingDirectoryClean("/workspace"); - expect(result).toBe(false); - }); - - it("should return false and log error when git command fails", async () => { - const gitError = new Error("fatal: not a git repository"); - mockExec.mockRejectedValue(gitError); - - const result = await isWorkingDirectoryClean("/workspace"); - - expect(logger.error).toHaveBeenCalledWith( - "Error checking git status:", - gitError, - ); - expect(result).toBe(false); - }); - }); - - describe("branch utilities", () => { - describe("doesBranchExist", () => { - it("should return true if branch exists", async () => { - mockExec.mockResolvedValue({ - stdout: "branch-sha-123456", - stderr: "", - exitCode: 0, - }); - - const result = await doesBranchExist("feature-branch", "/workspace"); - expect(mockExec).toHaveBeenCalledWith( - "git", - ["rev-parse", "--verify", "feature-branch"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - - expect(result).toBe(true); - }); - - it("should return false if branch does not exist", async () => { - mockExec.mockRejectedValue(new Error("fatal: Needed a single revision")); - - const result = await doesBranchExist("nonexistent-branch", "/workspace"); - expect(result).toBe(false); - }); - }); - - describe("getDefaultBranch", () => { - it("should return the default branch name", async () => { - mockExec.mockResolvedValue({ - stdout: "refs/remotes/origin/main\n", - stderr: "", - exitCode: 0, - }); - - const result = await getDefaultBranch("/workspace"); - - expect(mockExec).toHaveBeenCalledWith( - "git", - ["symbolic-ref", "refs/remotes/origin/HEAD"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - stdio: "pipe", - }), - }), - ); - - expect(result).toBe("main"); - }); - - it("should return different branch name", async () => { - mockExec.mockResolvedValue({ - stdout: "refs/remotes/origin/develop\n", - stderr: "", - exitCode: 0, - }); - - const result = await getDefaultBranch("/workspace"); - - expect(result).toBe("develop"); - }); - - it("should return 'main' if default branch cannot be determined", async () => { - mockExec.mockRejectedValue(new Error("Some git error")); - - const result = await getDefaultBranch("/workspace"); - - expect(result).toBe("main"); - }); - - it("should return 'main' if remote show output is unexpected", async () => { - mockExec.mockResolvedValue({ - stdout: "Some unexpected output\n", - stderr: "", - exitCode: 0, - }); - - const result = await getDefaultBranch("/workspace"); - - expect(result).toBe("main"); - }); - }); - - describe("getCurrentBranch", () => { - it("should return the current branch name", async () => { - mockExec.mockResolvedValue({ - stdout: "feature-branch\n", - stderr: "", - exitCode: 0, - }); - - const result = await getCurrentBranch("/workspace"); - - expect(mockExec).toHaveBeenCalledWith( - "git", - ["rev-parse", "--abbrev-ref", "HEAD"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - - expect(result).toBe("feature-branch"); - }); - - it("should handle errors and throw", async () => { - mockExec.mockRejectedValue(new Error("Some git error")); - - await expect(getCurrentBranch("/workspace")).rejects.toThrow( - "Some git error", - ); - }); - }); - - describe("getAvailableBranches", () => { - it("should return a list of available branches", async () => { - mockExec.mockResolvedValue({ - stdout: " main\n* feature-branch\ndevelop\n", - stderr: "", - exitCode: 0, - }); - - const result = await getAvailableBranches("/workspace"); - - expect(mockExec).toHaveBeenCalledWith( - "git", - ["branch", "--list"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - - expect(result).toEqual(["main", "feature-branch", "develop"]); - }); - - it("should handle errors and throw", async () => { - mockExec.mockRejectedValue(new Error("Some git error")); - - await expect(getAvailableBranches("/workspace")).rejects.toThrow( - "Some git error", - ); - }); - }); - - describe("createBranch", () => { - it("should create a new branch from the specified base branch", async () => { - mockExec.mockResolvedValue({ - stdout: "", - stderr: "", - exitCode: 0, - }); - - await createBranch("new-feature", "main", "/workspace"); - - expect(mockExec).toHaveBeenCalledWith( - "git", - ["checkout", "-b", "new-feature", "main"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - }); - - it.todo("should handle errors and throw", async () => { - mockExec.mockRejectedValue(new Error("Some git error")); - - await expect( - createBranch("new-feature", "main", "/workspace"), - ).rejects.toThrow("Some git error"); - }); - }); - }); - - describe("package tags", () => { - it("should return the last tag for a package", async () => { - const mockExec = vi.mocked(tinyexec.exec); - mockExec.mockResolvedValue({ - stdout: "other-package@1.0.0\nmy-package@1.2.0\nmy-package@1.1.0\n", - stderr: "", - exitCode: 0, - } as any); - - const result = await getMostRecentPackageTag("/workspace", "my-package"); - - expect(mockExec).toHaveBeenCalledWith( - "git", - ["tag", "--list", "my-package@*"], - expect.objectContaining({ - nodeOptions: expect.objectContaining({ - cwd: "/workspace", - stdio: "pipe", - }), - }), - ); - expect(result).toBe("my-package@1.1.0"); - }); - - it("should return undefined if no tag exists for package", async () => { - const mockExec = vi.mocked(tinyexec.exec); - mockExec.mockResolvedValue({ - stdout: "", - stderr: "", - exitCode: 0, - } as any); - - const result = await getMostRecentPackageTag("/workspace", "my-package"); - - expect(result).toBeUndefined(); - }); - - it("should return undefined if no tags exist", async () => { - const mockExec = vi.mocked(tinyexec.exec); - mockExec.mockResolvedValue({ - stdout: "", - stderr: "", - exitCode: 0, - } as any); - - const result = await getMostRecentPackageTag("/workspace", "my-package"); - - expect(result).toBeUndefined(); - }); - }); -}); diff --git a/test/versioning/commits.test.ts b/test/versioning/commits.test.ts deleted file mode 100644 index 04018c4..0000000 --- a/test/versioning/commits.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { determineHighestBump } from "#versioning/commits"; -import { - describe, - expect, - it, -} from "vitest"; -import { createCommit } from "../_shared"; - -describe("determineHighestBump", () => { - it("should return 'none' for empty commit list", () => { - const result = determineHighestBump([]); - expect(result).toBe("none"); - }); - - it("should return 'patch' if only patch commits are present", () => { - const result = determineHighestBump([ - createCommit({ - message: "fix: bug fix", - type: "fix", - isConventional: true, - }), - createCommit({ - message: "chore: update dependencies", - type: "fix", - isConventional: true, - }), - ]); - - expect(result).toBe("patch"); - }); - - it("should return 'minor' if minor and patch commits are present", () => { - const result = determineHighestBump([ - createCommit({ - message: "feat: new feature", - type: "feat", - isConventional: true, - }), - createCommit({ - message: "fix: bug fix", - type: "fix", - isConventional: true, - }), - ]); - - expect(result).toBe("minor"); - }); - - it("should return 'major' if a breaking change commit is present", () => { - const result = determineHighestBump([ - createCommit({ - message: "feat: new feature\n\nBREAKING CHANGE: changes API", - type: "feat", - isConventional: true, - isBreaking: true, - }), - createCommit({ - message: "fix: bug fix", - type: "fix", - isConventional: true, - }), - ]); - - expect(result).toBe("major"); - }); - - it("should ignore non-conventional commits", () => { - const result = determineHighestBump([ - createCommit({ - message: "Some random commit message", - isConventional: false, - type: "", - }), - createCommit({ - message: "fix: bug fix", - type: "fix", - isConventional: true, - }), - ]); - - expect(result).toBe("patch"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index a0a878d..a1c8589 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,12 @@ "noEmit": true, "esModuleInterop": true, "verbatimModuleSyntax": true, - "skipLibCheck": true + "skipLibCheck": true, + "plugins": [ + { + "name": "@effect/language-service" + } + ] }, "include": ["src"], "exclude": ["node_modules", "dist"] From c8234dea76609fb20c4ef816fc842a128aaed315 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 5 Dec 2025 05:51:57 +0100 Subject: [PATCH 02/35] refactor: restructure release scripts and enhance configuration handling - Removed the `HOW_IT_WORKS.md` documentation file. - Introduced `ConfigService` to manage configuration options more effectively. - Updated `createReleaseScripts` to utilize normalized options and improve clarity. - Enhanced `GitService` and `GitHubService` to integrate with the new configuration structure. - Added `options.ts` to define and normalize options for the release process. - Created a new `changelog.ts` file for future changelog management. --- HOW_IT_WORKS.md | 266 --------------------------------- src/index.ts | 156 +++++++++++++------ src/services/config.service.ts | 13 ++ src/services/git.service.ts | 8 +- src/services/github.service.ts | 19 +-- src/utils/changelog.ts | 0 src/utils/options.ts | 178 ++++++++++++++++++++++ 7 files changed, 318 insertions(+), 322 deletions(-) delete mode 100644 HOW_IT_WORKS.md create mode 100644 src/services/config.service.ts create mode 100644 src/utils/changelog.ts create mode 100644 src/utils/options.ts diff --git a/HOW_IT_WORKS.md b/HOW_IT_WORKS.md deleted file mode 100644 index aae9ce3..0000000 --- a/HOW_IT_WORKS.md +++ /dev/null @@ -1,266 +0,0 @@ -# How It Works - -This document explains the release workflow used by `@ucdjs/release-scripts`. - -## Overview - -The release process is split into two phases: - -1. **Prepare Release** - Creates/updates a release PR with version bumps and changelogs -2. **Publish** - Publishes packages to npm after the PR is merged (TODO) - -This is similar to how [Changesets](https://github.com/changesets/changesets) works, but uses **conventional commits** instead of changeset files. - -## Complete Workflow - -```mermaid -flowchart TD - A[Developers commit with conventional commits] --> B[Trigger: Manual or Scheduled] - B --> C[prepareRelease runs] - C --> D{Release PR exists?} - D -->|Yes| E[Update existing PR] - D -->|No| F[Create new PR] - E --> G[Human reviews PR] - F --> G - G --> H[Merge PR to main] - H --> I[CI: Publish workflow triggered] - I --> J[publish runs] - J --> K[Detect changed packages] - K --> L[Build packages in dependency order] - L --> M[Publish to npm in dependency order] - M --> N[Create & push git tags] - - style C fill:#90EE90 - style J fill:#FFB6C1 - style A fill:#E8E8E8 - style G fill:#FFE4B5 - style H fill:#FFE4B5 -``` - -**Key Points:** -- 🟢 Green: `prepareRelease` phase (creates PR) -- 🔴 Pink: `publish` phase (publishes packages) - **TODO** -- 🟡 Yellow: Human interaction required - -## Phase 1: Prepare Release - -### High-Level Flow - -```mermaid -flowchart LR - A[Analyze commits] --> B[Determine version bumps] - B --> C[Update package.json files] - C --> D[Update workspace dependencies] - D --> E[Generate CHANGELOGs] - E --> F[Create/Update PR] -``` - -### Step-by-Step Process - -#### 1. Analyze Conventional Commits - -The script analyzes commits since the last release for each package: - -``` -feat: add new utility function → Minor bump -fix: resolve parsing bug → Patch bump -feat!: redesign authentication → Major bump -docs: update README → No bump -``` - -#### 2. Determine Version Bumps - -Based on commit analysis, calculate the new version for each changed package: - -``` -@ucdjs/utils: 1.0.0 → 1.1.0 (had 'feat' commits) -@ucdjs/core: 2.3.1 → 2.3.2 (had 'fix' commits) -@ucdjs/plugin: 1.5.0 → 2.0.0 (had breaking changes) -``` - -#### 3. Handle Workspace Dependencies - -When a package is updated, **all packages that depend on it are also updated**: - -```mermaid -graph TD - A[@ucdjs/utils
v1.0.0 → v1.1.0] --> B[@ucdjs/core
v2.0.0 → v2.0.1] - A --> C[@ucdjs/plugin
v1.5.0 → v1.5.1] - B --> D[@ucdjs/cli
v3.0.0 → v3.0.1] - - style A fill:#90EE90 - style B fill:#FFB6C1 - style C fill:#FFB6C1 - style D fill:#FFB6C1 -``` - -**Legend:** -- 🟢 **Green** = Package with actual code changes (from commits) -- 🔴 **Pink** = Dependent packages (updated because dependencies changed) - -**Example:** -1. `@ucdjs/utils` has new features → bump to `v1.1.0` -2. `@ucdjs/core` depends on `@ucdjs/utils` → update its dependency to `^1.1.0` → bump to `v2.0.1` -3. `@ucdjs/plugin` depends on `@ucdjs/utils` → update its dependency to `^1.1.0` → bump to `v1.5.1` -4. `@ucdjs/cli` depends on `@ucdjs/core` → update its dependency to `^2.0.1` → bump to `v3.0.1` - -#### 4. Update Files - -For each package, the script updates: - -**package.json:** -```diff -{ - "name": "@ucdjs/core", -- "version": "2.0.0", -+ "version": "2.0.1", - "dependencies": { -- "@ucdjs/utils": "^1.0.0" -+ "@ucdjs/utils": "^1.1.0" - } -} -``` - -**CHANGELOG.md:** -```markdown -# Changelog - -## 2.0.1 (2025-01-07) - -### Dependencies - -- Updated @ucdjs/utils to ^1.1.0 -``` - -#### 5. Create or Update Release PR - -The script checks if a release PR already exists: - -**If PR exists:** -- Update the branch with new version changes -- Update PR description with new package list - -**If PR doesn't exist:** -- Create a new branch (e.g., `release/vX.Y.Z`) -- Push all changes -- Create PR with title like "Release vX.Y.Z" - -### Release PR Contents - -```markdown -📦 Release vX.Y.Z - -## Packages - -- @ucdjs/utils: 1.0.0 → 1.1.0 -- @ucdjs/core: 2.0.0 → 2.0.1 (dependency update) -- @ucdjs/plugin: 1.5.0 → 1.5.1 (dependency update) -- @ucdjs/cli: 3.0.0 → 3.0.1 (dependency update) - -## Changes - -### @ucdjs/utils v1.1.0 - -#### Features -- add new utility function (#123) - -#### Fixes -- handle edge case in parser (#124) - -[See individual CHANGELOG.md files for full details] -``` - -## Phase 2: Publish (TODO) - -> **Status:** Not yet implemented - -After the release PR is merged to main, a GitHub Actions workflow automatically publishes the packages. - -### High-Level Flow - -```mermaid -flowchart LR - A[Detect version changes] --> B[Build dependency graph] - B --> C[Build packages] - C --> D[Publish to npm] - D --> E[Create git tags] - E --> F[Push tags] -``` - -### Step-by-Step Process - -#### 1. Detect Changed Packages - -When the PR is merged, the workflow detects which packages had version changes by comparing `package.json` files. - -#### 2. Build Dependency Graph - -Build a graph of all workspace packages and their dependencies to determine publish order. - -#### 3. Calculate Publish Order (Topological Sort) - -Packages must be published in **dependency order** to ensure dependents reference already-published versions. - -```mermaid -graph TD - subgraph "Level 0: Publish First" - A[@ucdjs/utils v1.1.0] - end - - subgraph "Level 1: Publish Second" - B[@ucdjs/core v2.0.1] - C[@ucdjs/plugin v1.5.1] - end - - subgraph "Level 2: Publish Last" - D[@ucdjs/cli v3.0.1] - end - - A -.->|depends on| B - A -.->|depends on| C - B -.->|depends on| D - - style A fill:#90EE90 - style B fill:#87CEEB - style C fill:#87CEEB - style D fill:#DDA0DD -``` - -**Why this order matters:** - -1. **Publish `@ucdjs/utils@1.1.0`** first ✅ -2. **Publish `@ucdjs/core@2.0.1`** (references `@ucdjs/utils@^1.1.0` which now exists) ✅ -3. **Publish `@ucdjs/plugin@1.5.1`** (references `@ucdjs/utils@^1.1.0` which now exists) ✅ -4. **Publish `@ucdjs/cli@3.0.1`** (references `@ucdjs/core@2.0.1` which now exists) ✅ - -**If we published in wrong order:** - -❌ Publish `@ucdjs/cli@3.0.1` first → references `@ucdjs/core@2.0.1` which doesn't exist yet → **npm install fails!** - -#### 4. Build & Publish Each Package - -For each package in dependency order: - -1. Run build script (if exists) -2. Run `npm publish` (or `pnpm publish`) -3. Wait for publish to complete -4. Move to next package - -#### 5. Create Git Tags - -After all packages are successfully published, create git tags: - -```bash -git tag @ucdjs/utils@1.1.0 -git tag @ucdjs/core@2.0.1 -git tag @ucdjs/plugin@1.5.1 -git tag @ucdjs/cli@3.0.1 -``` - -#### 6. Push Tags - -```bash -git push origin --tags -``` - -These tags can then trigger additional workflows (like creating GitHub releases). diff --git a/src/index.ts b/src/index.ts index 4d39be6..4a0351d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,42 +1,14 @@ import type { WorkspacePackage } from "./services/workspace.service.js"; -import process from "node:process"; +import type { NormalizedOptions, Options } from "./utils/options.js"; import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; import { Effect, Layer } from "effect"; +import { ConfigService } from "./services/config.service.js"; import { GitService } from "./services/git.service.js"; import { GitHubService } from "./services/github.service.js"; import { WorkspaceService } from "./services/workspace.service.js"; +import { normalizeOptions } from "./utils/options.js"; -export interface Options { - /** - * Repository identifier (e.g., "owner/repo") - */ - repo: `${string}/${string}`; - - /** - * Root directory of the workspace (defaults to process.cwd()) - */ - workspaceRoot?: string; - - /** - * Specific packages to prepare for release. - * - true: discover all packages - * - FindWorkspacePackagesOptions: discover with filters - * - string[]: specific package names - */ - packages?: true | unknown | string[]; - - /** - * GitHub token for authentication - */ - githubToken: string; - - branch?: { - release?: string; - default?: string; - }; - - safeguards?: boolean; -} +export type { Options } from "./utils/options.js"; export interface ReleaseScriptsAPI { verify: () => Promise; @@ -46,16 +18,16 @@ export interface ReleaseScriptsAPI { }; } -export async function createReleaseScripts(config: Options): Promise { - const { - workspaceRoot: cwd = process.cwd(), - } = config; +export async function createReleaseScripts(options: Options): Promise { + const config = normalizeOptions(options); + const cwd = config.workspaceRoot; const MainLayer = Layer.mergeAll( GitService.Default, WorkspaceService.Default, GitHubService.Default, ).pipe( + Layer.provide(ConfigService.layer(config)), Layer.provide(NodeCommandExecutor.layer), Layer.provide(NodeFileSystem.layer), ); @@ -91,21 +63,120 @@ export async function createReleaseScripts(config: Options): Promise !("private" in pkg) || !pkg.private); + + yield* Effect.log(`✓ Found ${packages.length} packages (${publicPackages.length} public)`); - console.log(`✅ Verification successful on branch ${originalBranch}.`); + if (publicPackages.length > 0) { + yield* Effect.log("📋 Public packages in release:"); + + for (const pkg of publicPackages) { + const version = pkg.version || "0.0.0"; + + yield* Effect.log(` • ${pkg.name}@${version}`); + } + } + + // === Version Sync Verification === + yield* Effect.log("\n🔄 Verifying version synchronization..."); + + // For now, assume packages are in sync - add actual version comparison logic here + const isOutOfSync = false; + + // TODO: Add version comparison logic: + // 1. Calculate expected versions based on commits since last release + // 2. Compare with current package.json versions in the PR + // 3. Check if versions need to be bumped + + yield* Effect.log("✓ Package versions appear to be in sync"); + + // === Set Commit Status === + const statusContext = "ucdjs/release-verify"; + const commitHash = yield* git.getLastCommitHash; + + if (isOutOfSync) { + yield* github.createCommitStatus(commitHash, { + state: "failure", + context: statusContext, + description: "Release PR is out of sync with expected versions", + }); + yield* Effect.log("❌ Verification failed - set commit status to 'failure'"); + return yield* Effect.fail(new Error("Release verification failed")); + } else { + yield* github.createCommitStatus(commitHash, { + state: "success", + context: statusContext, + description: "Release PR verification passed", + target_url: releasePr.html_url, + }); + yield* Effect.log("✅ Verification passed - set commit status to 'success'"); + } + + yield* Effect.log("\n✅ Comprehensive verification completed successfully"); + + // Helper function for basic verification when no PR exists + function* basicVerification() { + yield* Effect.log("\n📦 Basic workspace verification..."); + + const remoteUrl = yield* git.getRemoteUrl; + if (remoteUrl) { + yield* Effect.log(`✓ Remote configured: ${remoteUrl}`); + } else { + yield* Effect.log("⚠️ No remote repository configured"); + } + + const branches = yield* git.listBranches; + yield* Effect.log(`✓ Available branches: ${branches.join(", ")}`); + + yield* Effect.log(`✓ Workspace with ${packages.length} packages`); + yield* Effect.log("\n✅ Basic verification completed"); + } }); return runProgram(program); @@ -123,6 +194,7 @@ export async function createReleaseScripts(config: Options): Promise { const program = Effect.gen(function* () { const workspace = yield* WorkspaceService; + const pkg = yield* workspace.findPackageByName(packageName); return pkg || null; }); diff --git a/src/services/config.service.ts b/src/services/config.service.ts new file mode 100644 index 0000000..d3679cc --- /dev/null +++ b/src/services/config.service.ts @@ -0,0 +1,13 @@ +import type { NormalizedOptions } from "../utils/options.js"; +import { Context, Effect, Layer } from "effect"; + +export class ConfigService extends Context.Tag("@ucdjs/release-scripts/ConfigService")< + ConfigService, + NormalizedOptions +>() { + static layer(config: NormalizedOptions) { + return Layer.effect(ConfigService, Effect.succeed( + config, + )); + } +} diff --git a/src/services/git.service.ts b/src/services/git.service.ts index e04d92e..4ff5bc3 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -1,14 +1,18 @@ import { Command, CommandExecutor } from "@effect/platform"; import { NodeCommandExecutor } from "@effect/platform-node"; import { Effect } from "effect"; -import { GitCommandError, GitError } from "../errors"; +import { GitCommandError, GitError } from "../errors.js"; +import { ConfigService } from "./config.service.js"; export class GitService extends Effect.Service()("@ucdjs/release-scripts/GitService", { effect: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; + const config = yield* ConfigService; const execGitCommand = (args: readonly string[]): Effect.Effect => - executor.string(Command.make("git", ...args)).pipe( + executor.string(Command.make("git", ...args).pipe( + Command.workingDirectory(config.workspaceRoot), + )).pipe( Effect.mapError((error: any) => new GitCommandError({ command: `git ${args.join(" ")}`, diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 20950f4..2782456 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import process from "node:process"; import { Effect, Schema } from "effect"; +import { ConfigService } from "./config.service.js"; import { GitService } from "./git.service.js"; // Schema definitions for GitHub API types @@ -59,6 +59,7 @@ export type RepositoryInfo = Schema.Schema.Type; export class GitHubService extends Effect.Service()("@ucdjs/release-scripts/GitHubService", { effect: Effect.gen(function* () { const git = yield* GitService; + const config = yield* ConfigService; const getRepositoryInfo = (): Effect.Effect => Effect.gen(function* () { @@ -81,19 +82,13 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea schema: Schema.Schema, options: RequestInit = {}, ): Effect.Effect => Effect.gen(function* () { - const token = process.env.GITHUB_TOKEN; - if (!token) { - return yield* Effect.fail(new Error("GITHUB_TOKEN environment variable is required")); - } - - const repoInfo = yield* getRepositoryInfo(); - const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/${endpoint}`; + const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`; const response = yield* Effect.tryPromise(() => fetch(url, { ...options, headers: { - "Authorization": `token ${token}`, + "Authorization": `token ${config.githubToken}`, "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json", ...options.headers, @@ -118,7 +113,7 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea // Parse and validate the response using the schema return yield* Schema.decodeUnknown(schema)(data).pipe( - Effect.mapError((error) => new Error(`Schema validation failed: ${error}`)) + Effect.mapError((error) => new Error(`Schema validation failed: ${error}`)), ); }); @@ -158,7 +153,7 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea // Validate input const validatedStatus = yield* Schema.decodeUnknown(CommitStatusSchema)(status); - const token = process.env.GITHUB_TOKEN; + const token = config.githubToken; if (!token) { return yield* Effect.fail(new Error("GITHUB_TOKEN environment variable is required")); } @@ -195,7 +190,7 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea `pulls?head=${repoInfo.owner}:${currentBranch}&state=open`, Schema.Array(PullRequestSchema), ).pipe( - Effect.catchAll(() => Effect.succeed([])) + Effect.catchAll(() => Effect.succeed([])), ); return pulls.length > 0 ? pulls[0]! : null; diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/options.ts b/src/utils/options.ts new file mode 100644 index 0000000..64a4c18 --- /dev/null +++ b/src/utils/options.ts @@ -0,0 +1,178 @@ +import process from "node:process"; + +export interface Options { + /** + * Repository identifier (e.g., "owner/repo") + */ + repo: `${string}/${string}`; + + /** + * Root directory of the workspace (defaults to process.cwd()) + */ + workspaceRoot?: string; + + /** + * Specific packages to prepare for release. + * - true: discover all packages + * - FindWorkspacePackagesOptions: discover with filters + * - string[]: specific package names + */ + packages?: true | { + exclude?: string[]; + include?: string[]; + excludePrivate?: boolean; + } | string[]; + + /** + * GitHub token for authentication + */ + githubToken: string; + + /** + * Branch configuration for release process + */ + branch?: { + release?: string; + default?: string; + }; + + /** + * Enable safety checks (default: true) + */ + safeguards?: boolean; + + /** + * How to handle global commits (commits that affect multiple packages) + */ + globalCommitMode?: "dependencies" | "all" | "none"; + + /** + * Pull request configuration + */ + pullRequest?: { + title?: string; + body?: string; + }; + + /** + * Changelog configuration + */ + changelog?: { + enabled?: boolean; + template?: string; + }; + + /** + * Prompt configuration + */ + prompts?: { + packages?: boolean; + versions?: boolean; + }; + + /** + * Commit groups for changelog categorization + */ + groups?: Array<{ + name: string; + title: string; + types: string[]; + }>; +} + +type DeepRequired = Required<{ + [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired +}>; + +export type NormalizedOptions = DeepRequired> & { + owner: string; + repo: string; +}; + +const DEFAULT_COMMIT_GROUPS = [ + { name: "features", title: "Features", types: ["feat"] }, + { name: "fixes", title: "Bug Fixes", types: ["fix", "perf"] }, + { name: "refactor", title: "Refactoring", types: ["refactor"] }, + { name: "docs", title: "Documentation", types: ["docs"] }, +]; + +const DEFAULT_PR_BODY_TEMPLATE = `## Summary + +This PR contains the following changes: + +- Updated package versions +- Updated changelogs + +## Packages + +The following packages will be released: + +{{packages}}`; + +const DEFAULT_CHANGELOG_TEMPLATE = `# Changelog + +{{releases}}`; + +export function normalizeOptions(options: Options): NormalizedOptions { + const { + workspaceRoot = process.cwd(), + githubToken = "", + repo: fullRepo, + packages = true, + branch = {}, + safeguards = true, + globalCommitMode = "dependencies", + pullRequest = {}, + changelog = {}, + prompts = {}, + groups = DEFAULT_COMMIT_GROUPS, + } = options; + + if (!githubToken.trim()) { + throw new Error("GitHub token is required. Set GITHUB_TOKEN environment variable or pass it in options."); + } + + if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) { + throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world')."); + } + + const [owner, repo] = fullRepo.split("/"); + if (!owner || !repo) { + throw new Error(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`); + } + + const normalizedPackages = typeof packages === "object" && !Array.isArray(packages) + ? { + exclude: packages.exclude ?? [], + include: packages.include ?? [], + excludePrivate: packages.excludePrivate ?? false, + } + : packages; + + return { + workspaceRoot, + githubToken, + owner, + repo, + packages: normalizedPackages, + branch: { + release: branch.release ?? "release/next", + default: branch.default ?? "main", + }, + safeguards, + globalCommitMode, + pullRequest: { + title: pullRequest.title ?? "chore: release new version", + body: pullRequest.body ?? DEFAULT_PR_BODY_TEMPLATE, + }, + changelog: { + enabled: changelog.enabled ?? true, + template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE, + }, + prompts: { + packages: prompts.packages ?? true, + versions: prompts.versions ?? true, + }, + groups, + }; +} From 93c213beb4197be14907c5074d2ade8f5fe9140e Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 6 Dec 2025 06:40:24 +0100 Subject: [PATCH 03/35] feat: implement Git and GitHub service enhancements - Introduced `GitHubError` for better error handling in GitHub API interactions. - Refactored `GitService` to utilize `ConfigOptions` instead of `ConfigService`. - Added dry run mode to options, allowing for non-destructive testing of commands. - Enhanced `GitService` with new methods for branch management and command execution. - Created tests for `GitService` to ensure proper functionality and error handling. --- playground.ts | 27 ++++ src/errors.ts | 7 +- src/index.ts | 12 +- src/services/config.service.ts | 6 +- src/services/git.service.ts | 182 ++++++++++----------- src/services/github.service.ts | 257 +++++++----------------------- src/utils/options.ts | 6 + test/index.test.ts | 15 ++ test/services/git.service.test.ts | 126 +++++++++++++++ 9 files changed, 340 insertions(+), 298 deletions(-) create mode 100644 playground.ts create mode 100644 test/index.test.ts create mode 100644 test/services/git.service.test.ts diff --git a/playground.ts b/playground.ts new file mode 100644 index 0000000..7480bf0 --- /dev/null +++ b/playground.ts @@ -0,0 +1,27 @@ +import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; +import { Effect } from "effect"; +import { ConfigOptions } from "./src/services/config.service.ts"; +import { GitService } from "./src/services/git.service.ts"; + +const program = Effect.gen(function* () { + const git = yield* GitService; + + yield* git.commit.stage(["."]); + yield* git.commit.write("refactor: change git & github services"); + yield* git.commit.push("effect-rewrite"); + + yield* git.branches.checkout("main"); + + return void 0; +}); + +const runnable = program.pipe( + Effect.provide(GitService.Default), + Effect.provide(NodeCommandExecutor.layer), + Effect.provide(NodeFileSystem.layer), + Effect.provide(ConfigOptions.layer({ + workspaceRoot: ".", + })), +); + +Effect.runPromise(runnable); diff --git a/src/errors.ts b/src/errors.ts index 86ee333..2249339 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,7 +7,6 @@ export class GitError extends Data.TaggedError("GitError")<{ export class GitCommandError extends Data.TaggedError("GitCommandError")<{ readonly command: string; - readonly exitCode: number; readonly stderr: string; }> {} @@ -24,3 +23,9 @@ export class WorkspaceError extends Data.TaggedError("WorkspaceError")<{ operation?: string; cause?: unknown; }> { } + +export class GitHubError extends Data.TaggedError("GitHubError")<{ + message: string; + operation?: "getPullRequestByBranch" | "createPullRequest" | "updatePullRequest" | "setCommitStatus" | "request"; + cause?: unknown; +}> { } diff --git a/src/index.ts b/src/index.ts index 4a0351d..4316737 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import type { WorkspacePackage } from "./services/workspace.service.js"; import type { NormalizedOptions, Options } from "./utils/options.js"; import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; import { Effect, Layer } from "effect"; -import { ConfigService } from "./services/config.service.js"; +import { ConfigOptions } from "./services/config.service.js"; import { GitService } from "./services/git.service.js"; import { GitHubService } from "./services/github.service.js"; import { WorkspaceService } from "./services/workspace.service.js"; @@ -27,7 +27,7 @@ export async function createReleaseScripts(options: Options): Promise() { static layer(config: NormalizedOptions) { - return Layer.effect(ConfigService, Effect.succeed( + return Layer.effect(ConfigOptions, Effect.succeed( config, )); } diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 4ff5bc3..1d96cea 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -1,121 +1,121 @@ import { Command, CommandExecutor } from "@effect/platform"; import { NodeCommandExecutor } from "@effect/platform-node"; -import { Effect } from "effect"; -import { GitCommandError, GitError } from "../errors.js"; -import { ConfigService } from "./config.service.js"; +import { Effect, Layer } from "effect"; +import { GitCommandError } from "../errors"; +import { ConfigOptions } from "./config.service"; export class GitService extends Effect.Service()("@ucdjs/release-scripts/GitService", { effect: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; - const config = yield* ConfigService; + const config = yield* ConfigOptions; - const execGitCommand = (args: readonly string[]): Effect.Effect => + const execGitCommand = (args: readonly string[]) => executor.string(Command.make("git", ...args).pipe( Command.workingDirectory(config.workspaceRoot), )).pipe( - Effect.mapError((error: any) => - new GitCommandError({ + Effect.mapError((err) => { + return new GitCommandError({ command: `git ${args.join(" ")}`, - exitCode: error.exitCode || 1, - stderr: error.stderr || error.message || "Unknown error", - }), - ), + stderr: err.message, + }); + }), ); - const getCurrentBranch: Effect.Effect = Effect.gen(function* () { - const output = yield* execGitCommand(["rev-parse", "--abbrev-ref", "HEAD"]); - const branch = output.trim(); - - if (branch === "HEAD") { - return yield* Effect.fail( - new GitError({ message: "Repository is in detached HEAD state" }), - ); - } - - return branch; + // This should only be used by functions that need to respect dry-run mode + // e.g. createBranch, deleteBranch. + // Functions that just modify git behavior (checkoutBranch, etc.) should use execGitCommand directly. + // Since it doesn't really change external state. + const execGitCommandIfNotDry = config.dryRun + ? (args: readonly string[]) => + Effect.succeed( + `Dry run mode: skipping git command "git ${args.join(" ")}"`, + ) + : execGitCommand; + + const isWithinRepository = Effect.gen(function* () { + const result = yield* execGitCommand(["rev-parse", "--is-inside-work-tree"]).pipe( + Effect.catchAll(() => Effect.succeed("false")), + ); + return result.trim() === "true"; }); - const listBranches: Effect.Effect = Effect.gen(function* () { - const output = yield* execGitCommand(["branch", "--format=%(refname:short)"]); + const listBranches = Effect.gen(function* () { + const output = yield* execGitCommand(["branch", "--list"]); return output .trim() .split("\n") - .filter((branch: string) => branch.length > 0) - .map((branch: string) => branch.trim()); - }); - - const isRepository: Effect.Effect = execGitCommand(["rev-parse", "--git-dir"]).pipe( - Effect.map(() => true), - Effect.catchAll(() => Effect.succeed(false)), - ); - - const getRemoteUrl: Effect.Effect = execGitCommand(["config", "--get", "remote.origin.url"]).pipe( - Effect.map((output) => { - const url = output.trim(); - return url.length > 0 ? url : undefined; - }), - Effect.catchAll(() => Effect.succeed(undefined)), - ); - - const hasChanges: Effect.Effect = Effect.gen(function* () { - const output = yield* execGitCommand(["status", "--porcelain"]); - return output.trim().length > 0; + .filter((line) => line.length > 0) + .map((line) => line.replace(/^\* /, "").trim()) + .map((line) => line.trim()); }); - const hasStagedChanges: Effect.Effect = Effect.gen(function* () { - const output = yield* execGitCommand(["diff", "--cached", "--name-only"]); - return output.trim().length > 0; + const isWorkingDirectoryClean = Effect.gen(function* () { + const status = yield* execGitCommand(["status", "--porcelain"]); + return status.trim().length === 0; }); - const getLastCommitHash: Effect.Effect = Effect.gen(function* () { - const output = yield* execGitCommand(["rev-parse", "HEAD"]); - return output.trim(); - }); - - const branchExists = (branchName: string): Effect.Effect => - execGitCommand(["rev-parse", "--verify", `refs/heads/${branchName}`]).pipe( - Effect.map(() => true), - Effect.catchAll(() => Effect.succeed(false)), + function doesBranchExist(branch: string) { + return listBranches.pipe( + Effect.map((branches) => branches.includes(branch)), ); - - const addFiles = (files: readonly string[]): Effect.Effect => - execGitCommand(["add", ...files]).pipe(Effect.asVoid); - - const commit = (message: string): Effect.Effect => - execGitCommand(["commit", "-m", message]).pipe(Effect.asVoid); - - const createTag = (tagName: string, message?: string): Effect.Effect => { - const args = message - ? ["tag", "-a", tagName, "-m", message] - : ["tag", tagName]; - return execGitCommand(args).pipe(Effect.asVoid); - }; - - const push = (remote = "origin", branch?: string): Effect.Effect => { - const args = branch ? ["push", remote, branch] : ["push", remote]; - return execGitCommand(args).pipe(Effect.asVoid); - }; - - const pushTags = (remote = "origin"): Effect.Effect => - execGitCommand(["push", remote, "--tags"]).pipe(Effect.asVoid); + } + + function createBranch(branch: string, base: string = config.branch.default) { + return execGitCommandIfNotDry(["branch", branch, base]); + } + + function checkoutBranch(branch: string) { + return Effect.gen(function* () { + const result = yield* execGitCommand(["checkout", branch]); + + console.log(result); + }); + } + + function stageChanges(files: readonly string[]) { + return Effect.gen(function* () { + if (files.length === 0) { + return yield* Effect.fail(new Error("No files to stage.")); + } + + return yield* execGitCommand(["add", ...files]); + }); + } + + function writeCommit(message: string) { + return Effect.gen(function* () { + return yield* execGitCommandIfNotDry(["commit", "-m", message]); + }); + } + + function pushChanges(branch: string, remote: string = "origin") { + return Effect.gen(function* () { + const result = yield* execGitCommandIfNotDry(["push", remote, branch]); + return result; + }); + } return { - getCurrentBranch, - listBranches, - isRepository, - getRemoteUrl, - hasChanges, - hasStagedChanges, - getLastCommitHash, - branchExists, - addFiles, - commit, - createTag, - push, - pushTags, + branches: { + list: listBranches, + exists: doesBranchExist, + create: createBranch, + checkout: checkoutBranch, + }, + commit: { + stage: stageChanges, + write: writeCommit, + push: pushChanges, + }, + isWithinRepository, + isWorkingDirectoryClean, } as const; }), dependencies: [ NodeCommandExecutor.layer, ], -}) {} +}) { + static mockLayer(mockExecutor: CommandExecutor.CommandExecutor) { + return Layer.succeed(CommandExecutor.CommandExecutor, mockExecutor); + } +} diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 2782456..5e98f4e 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,6 +1,6 @@ -/* eslint-disable no-console */ import { Effect, Schema } from "effect"; -import { ConfigService } from "./config.service.js"; +import { GitHubError } from "../errors.js"; +import { ConfigOptions } from "./config.service.js"; import { GitService } from "./git.service.js"; // Schema definitions for GitHub API types @@ -49,7 +49,6 @@ export const RepositoryInfoSchema = Schema.Struct({ repo: Schema.String, }); -// Type inference from schemas export type PullRequest = Schema.Schema.Type; export type CreatePullRequestOptions = Schema.Schema.Type; export type UpdatePullRequestOptions = Schema.Schema.Type; @@ -58,207 +57,71 @@ export type RepositoryInfo = Schema.Schema.Type; export class GitHubService extends Effect.Service()("@ucdjs/release-scripts/GitHubService", { effect: Effect.gen(function* () { - const git = yield* GitService; - const config = yield* ConfigService; + const config = yield* ConfigOptions; - const getRepositoryInfo = (): Effect.Effect => - Effect.gen(function* () { - const remoteUrl = yield* git.getRemoteUrl; - if (!remoteUrl) { - return yield* Effect.fail(new Error("No git remote found")); - } - - const repoMatch = remoteUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)(?:\.git)?$/); - if (!repoMatch) { - return yield* Effect.fail(new Error("Could not parse GitHub repository from remote URL")); - } - - const [, owner, repo] = repoMatch; - return yield* Schema.decodeUnknown(RepositoryInfoSchema)({ owner, repo }); - }); - - const makeRequest = ( - endpoint: string, - schema: Schema.Schema, - options: RequestInit = {}, - ): Effect.Effect => Effect.gen(function* () { + function makeRequest(endpoint: string, schema: Schema.Schema, options: RequestInit = {}) { const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`; - - const response = yield* Effect.tryPromise(() => - fetch(url, { - ...options, - headers: { - "Authorization": `token ${config.githubToken}`, - "Accept": "application/vnd.github.v3+json", - "Content-Type": "application/json", - ...options.headers, - }, - }), - ).pipe( - Effect.mapError((error) => new Error(`Failed to fetch: ${error}`)), - ); - - if (!response.ok) { - const errorText = yield* Effect.tryPromise(() => response.text()).pipe( - Effect.mapError(() => new Error("Could not read error response")), - ); - return yield* Effect.fail( - new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`), - ); - } - - const data = yield* Effect.tryPromise(() => response.json()).pipe( - Effect.mapError((error) => new Error(`Failed to parse JSON: ${error}`)), - ); - - // Parse and validate the response using the schema - return yield* Schema.decodeUnknown(schema)(data).pipe( - Effect.mapError((error) => new Error(`Schema validation failed: ${error}`)), - ); - }); - - const getPullRequest = (prNumber: number): Effect.Effect => - makeRequest(`pulls/${prNumber}`, PullRequestSchema); - - const createPullRequest = (options: CreatePullRequestOptions): Effect.Effect => - Effect.gen(function* () { - // Validate input - const validatedOptions = yield* Schema.decodeUnknown(CreatePullRequestOptionsSchema)(options); - - return yield* makeRequest("pulls", PullRequestSchema, { - method: "POST", - body: JSON.stringify(validatedOptions), - }); - }); - - const updatePullRequest = ( - prNumber: number, - options: UpdatePullRequestOptions, - ): Effect.Effect => - Effect.gen(function* () { - // Validate input - const validatedOptions = yield* Schema.decodeUnknown(UpdatePullRequestOptionsSchema)(options); - - return yield* makeRequest(`pulls/${prNumber}`, PullRequestSchema, { - method: "PATCH", - body: JSON.stringify(validatedOptions), - }); - }); - - const createCommitStatus = ( - sha: string, - status: CommitStatus, - ): Effect.Effect => - Effect.gen(function* () { - // Validate input - const validatedStatus = yield* Schema.decodeUnknown(CommitStatusSchema)(status); - - const token = config.githubToken; - if (!token) { - return yield* Effect.fail(new Error("GITHUB_TOKEN environment variable is required")); - } - - const repoInfo = yield* getRepositoryInfo(); - const url = `https://api.github.com/repos/${repoInfo.owner}/${repoInfo.repo}/statuses/${sha}`; - - const response = yield* Effect.promise(() => - fetch(url, { - method: "POST", + return Effect.tryPromise({ + try: async () => { + const res = await fetch(url, { + ...options, headers: { - "Authorization": `token ${token}`, + "Authorization": `token ${config.githubToken}`, "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json", + "User-Agent": "ucdjs-release-scripts (https://github.com/ucdjs/release-scripts)", + ...options.headers, }, - body: JSON.stringify(validatedStatus), - }), - ); - - if (!response.ok) { - const errorText = yield* Effect.promise(() => response.text()); - return yield* Effect.fail( - new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorText}`), - ); - } - }); - - const getCurrentPullRequest = (): Effect.Effect => - Effect.gen(function* () { - const currentBranch = yield* git.getCurrentBranch; - const repoInfo = yield* getRepositoryInfo(); - - const pulls = yield* makeRequest( - `pulls?head=${repoInfo.owner}:${currentBranch}&state=open`, - Schema.Array(PullRequestSchema), - ).pipe( - Effect.catchAll(() => Effect.succeed([])), - ); - - return pulls.length > 0 ? pulls[0]! : null; - }); - - const createPullRequestFromCurrentBranch = ( - options: Omit, - ): Effect.Effect => - Effect.gen(function* () { - const currentBranch = yield* git.getCurrentBranch; - - return yield* createPullRequest({ - ...options, - head: currentBranch, - }); - }); - - const pushCurrentBranch = (remote = "origin"): Effect.Effect => - Effect.gen(function* () { - const currentBranch = yield* git.getCurrentBranch; - yield* git.push(remote, currentBranch); - }); - - const createPullRequestWorkflow = ( - options: Omit & { pushFirst?: boolean }, - ): Effect.Effect => - Effect.gen(function* () { - const { pushFirst = true, ...prOptions } = options; - - // Push current branch if requested - if (pushFirst) { - console.log("📤 Pushing current branch to remote..."); - yield* pushCurrentBranch(); - } - - // Check if PR already exists - const existingPr = yield* getCurrentPullRequest(); - if (existingPr) { - console.log(`✓ Pull request already exists: #${existingPr.number}`); - return existingPr; - } - - // Create new PR - console.log("📝 Creating pull request..."); - const pr = yield* createPullRequestFromCurrentBranch(prOptions); - console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`); - - return pr; - }); - - const setCommitStatusForCurrentBranch = (status: CommitStatus): Effect.Effect => - Effect.gen(function* () { - const commitHash = yield* git.getLastCommitHash; - yield* createCommitStatus(commitHash, status); - }); + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`GitHub API request failed with status ${res.status}: ${text}`); + } + + if (res.status === 204) { + return undefined; + } + + return res.json(); + }, + catch: (e) => new GitHubError({ message: String(e), operation: "request", cause: e }), + }).pipe( + Effect.flatMap((json) => + json === undefined + ? Effect.succeed(undefined as A) + : Schema.decodeUnknown(schema)(json).pipe( + Effect.mapError( + (e) => + new GitHubError({ + message: "Failed to decode GitHub response", + operation: "request", + cause: e, + }), + ), + ), + ), + ); + } + + function getPullRequestByBranch(branch: string) { + const head = branch.includes(":") ? branch : `${config.owner}:${branch}`; + const url = `/repos/${config.owner}/${config.repo}/pulls?state=open&head=${encodeURIComponent(head)}`; + return makeRequest(url, Schema.Array(PullRequestSchema)).pipe( + Effect.map((pulls) => (pulls.length > 0 ? pulls[0] : null)), + Effect.mapError((e) => new GitHubError({ + message: e.message, + operation: "getPullRequestByBranch", + cause: e.cause, + })), + ); + } return { - getPullRequest, - createPullRequest, - updatePullRequest, - createCommitStatus, - getCurrentPullRequest, - createPullRequestFromCurrentBranch, - pushCurrentBranch, - createPullRequestWorkflow, - setCommitStatusForCurrentBranch, - getRepositoryInfo, + getPullRequestByBranch, } as const; }), - dependencies: [GitService.Default], -}) {} + dependencies: [ + GitService.Default, + ], +}) { } diff --git a/src/utils/options.ts b/src/utils/options.ts index 64a4c18..9e70061 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -1,6 +1,11 @@ import process from "node:process"; export interface Options { + /** + * Enable dry run mode (no changes will be pushed or PRs created) + */ + dryRun?: boolean; + /** * Repository identifier (e.g., "owner/repo") */ @@ -150,6 +155,7 @@ export function normalizeOptions(options: Options): NormalizedOptions { : packages; return { + dryRun: options.dryRun ?? false, workspaceRoot, githubToken, owner, diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..688ce03 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from "@effect/vitest"; +import { Effect } from "effect"; + +// A simple divide function that returns an Effect, failing when dividing by zero +function divide(a: number, b: number) { + if (b === 0) return Effect.fail("Cannot divide by zero"); + return Effect.succeed(a / b); +} + +// Testing a successful division +it.effect("test success", () => + Effect.gen(function* () { + const result = yield* divide(4, 2); // Expect 4 divided by 2 to succeed + expect(result).toBe(2); // Assert that the result is 2 + })); diff --git a/test/services/git.service.test.ts b/test/services/git.service.test.ts new file mode 100644 index 0000000..5ddc164 --- /dev/null +++ b/test/services/git.service.test.ts @@ -0,0 +1,126 @@ +import type { CommandExecutor } from "@effect/platform"; +import { it } from "@effect/vitest"; +import { Effect, Layer } from "effect"; +import { GitCommandError } from "../../src/errors"; +import { ConfigOptions } from "../../src/services/config.service"; +import { GitService } from "../../src/services/git.service"; + +const mockConfig = { + workspaceRoot: "/test/workspace", + githubToken: "test-token", + owner: "test-owner", + repo: "test-repo", + packages: { + exclude: [], + include: [], + excludePrivate: false, + }, + prompts: { packages: true, versions: true }, + branch: { default: "main", release: "release/next" }, + safeguards: true, + globalCommitMode: "dependencies" as const, + pullRequest: { title: "chore: release", body: "" }, + changelog: { enabled: true, template: "" }, + groups: [], +}; + +function createMockExecutor(impl: Partial): CommandExecutor.CommandExecutor { + return { + string: () => Effect.succeed(""), + lines: () => Effect.succeed([]), + exitCode: () => Effect.succeed(0), + start: () => Effect.succeed({ exitCode: 0, stdout: "", stderr: "" }), + stream: () => Effect.succeed(""), + ...impl, + } as any; +} + +it("should parse branches from git output", () => + Effect.gen(function* () { + const mockBranches = "main\nfeature/test\nrelease/next"; + const mockExecutor = createMockExecutor({ + string: () => Effect.succeed(mockBranches), + }); + + const testLayer = Layer.merge( + ConfigOptions.layer(mockConfig), + GitService.mockLayer(mockExecutor), + ); + + const result = yield* GitService.pipe( + Effect.flatMap((gitService) => gitService.listBranches), + Effect.provide(testLayer), + ); + + if (result[0] !== "main" || result[1] !== "feature/test" || result[2] !== "release/next") { + throw new Error(`Expected ["main", "feature/test", "release/next"], got ${JSON.stringify(result)}`); + } + })); + +it("should handle empty branch list", () => + Effect.gen(function* () { + const mockExecutor = createMockExecutor({ + string: () => Effect.succeed(""), + }); + + const testLayer = Layer.merge( + ConfigOptions.layer(mockConfig), + GitService.mockLayer(mockExecutor), + ); + + const result = yield* GitService.pipe( + Effect.flatMap((gitService) => gitService.listBranches), + Effect.provide(testLayer), + ); + + if (result.length !== 0) { + throw new Error(`Expected empty array, got ${JSON.stringify(result)}`); + } + })); + +it("should handle git command errors", () => + Effect.gen(function* () { + const mockExecutor = createMockExecutor({ + string: () => + Effect.fail( + new Error("fatal: not a git repository"), + ) as Effect.Effect, + }); + + const testLayer = Layer.merge( + ConfigOptions.layer(mockConfig), + GitService.mockLayer(mockExecutor), + ); + + const result = yield* GitService.pipe( + Effect.flatMap((gitService) => gitService.listBranches), + Effect.provide(testLayer), + Effect.flip, + ); + + if (!(result instanceof GitCommandError)) { + throw new Error(`Expected GitCommandError, got ${(result as any).constructor?.name || typeof result}`); + } + })); + +it("should trim whitespace from branch names", () => + Effect.gen(function* () { + const mockBranches = " main \n feature/test \n release/next "; + const mockExecutor = createMockExecutor({ + string: () => Effect.succeed(mockBranches), + }); + + const testLayer = Layer.merge( + ConfigOptions.layer(mockConfig), + GitService.mockLayer(mockExecutor), + ); + + const result = yield* GitService.pipe( + Effect.flatMap((gitService) => gitService.listBranches), + Effect.provide(testLayer), + ); + + if (result[0] !== "main" || result[1] !== "feature/test" || result[2] !== "release/next") { + throw new Error(`Expected trimmed ["main", "feature/test", "release/next"], got ${JSON.stringify(result)}`); + } + })); From 1da693b49aba93984436bb51a06cd4fe796a200c Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 10:06:58 +0100 Subject: [PATCH 04/35] refactor: enhance Git and GitHub services with error handling and new features - Added error handling for branch checkout in `playground.ts`. - Introduced `ExternalCommitParserError` and `OverridesLoadError` for better error management in `errors.ts`. - Updated `GitService` to include new methods for commit parsing and file reading. - Refactored `ConfigOptions` to improve configuration handling in `options.ts`. - Implemented new utility functions in `helpers.ts` for managing version overrides and merging commits. - Enhanced `WorkspaceService` to support discovering workspace packages and reading package.json files. --- playground.ts | 5 +- src/errors.ts | 10 + src/index.ts | 167 +++++++--------- src/services/config.service.ts | 13 -- src/services/git.service.ts | 101 +++++++++- src/services/github.service.ts | 2 +- src/services/workspace.service.ts | 259 +++++++++++++++++------- src/utils/helpers.ts | 315 ++++++++++++++++++++++++++++++ src/utils/options.ts | 12 ++ 9 files changed, 690 insertions(+), 194 deletions(-) delete mode 100644 src/services/config.service.ts create mode 100644 src/utils/helpers.ts diff --git a/playground.ts b/playground.ts index 7480bf0..d0f1879 100644 --- a/playground.ts +++ b/playground.ts @@ -10,7 +10,10 @@ const program = Effect.gen(function* () { yield* git.commit.write("refactor: change git & github services"); yield* git.commit.push("effect-rewrite"); - yield* git.branches.checkout("main"); + yield* git.branches.checkout("main").pipe(Effect.catchAll((err) => { + console.error(`Error checking out main branch: ${err.message}`); + return Effect.fail(err); + })); return void 0; }); diff --git a/src/errors.ts b/src/errors.ts index 2249339..e9ac8bb 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,6 +10,11 @@ export class GitCommandError extends Data.TaggedError("GitCommandError")<{ readonly stderr: string; }> {} +export class ExternalCommitParserError extends Data.TaggedError("ExternalCommitParserError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + export class GitNotRepositoryError extends Data.TaggedError("GitNotRepositoryError")<{ readonly path: string; }> {} @@ -29,3 +34,8 @@ export class GitHubError extends Data.TaggedError("GitHubError")<{ operation?: "getPullRequestByBranch" | "createPullRequest" | "updatePullRequest" | "setCommitStatus" | "request"; cause?: unknown; }> { } + +export class OverridesLoadError extends Data.TaggedError("OverridesLoadError")<{ + message: string; + cause?: unknown; +}> {} diff --git a/src/index.ts b/src/index.ts index 4316737..425d4ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,20 @@ import type { WorkspacePackage } from "./services/workspace.service.js"; import type { NormalizedOptions, Options } from "./utils/options.js"; import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; -import { Effect, Layer } from "effect"; -import { ConfigOptions } from "./services/config.service.js"; +import { Console, Effect, Layer } from "effect"; import { GitService } from "./services/git.service.js"; import { GitHubService } from "./services/github.service.js"; +import { VersionUpdaterService } from "./services/version-updater.service.js"; import { WorkspaceService } from "./services/workspace.service.js"; -import { normalizeOptions } from "./utils/options.js"; +import { loadOverrides, mergeCommitsAffectingGloballyIntoPackage, mergePackageCommitsIntoPackages } from "./utils/helpers.js"; +import { ConfigOptions, normalizeOptions } from "./utils/options.js"; export type { Options } from "./utils/options.js"; export interface ReleaseScriptsAPI { verify: () => Promise; + prepare: () => Promise; + publish: () => Promise; packages: { list: () => Promise; get: (packageName: string) => Promise; @@ -26,6 +29,7 @@ export async function createReleaseScripts(options: Options): Promise(program: Effect.Effect): Promise
=> Effect.runPromise(Effect.provide(program, MainLayer) as Effect.Effect); - const initProgram = Effect.gen(function* () { + const safeguardProgram = Effect.gen(function* () { const git = yield* GitService; const isWithinRepository = yield* git.isWithinRepository; @@ -51,7 +55,7 @@ export async function createReleaseScripts(options: Options): Promise { console.error(`❌ Initialization failed: ${err.message}`); return Effect.exit(Effect.fail(err)); @@ -65,128 +69,93 @@ export async function createReleaseScripts(options: Options): Promise !("private" in pkg) || !pkg.private); + const packages = yield* workspace.discoverWorkspacePackages.pipe( + Effect.flatMap(mergePackageCommitsIntoPackages), + Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), + ); - yield* Effect.log(`✓ Found ${packages.length} packages (${publicPackages.length} public)`); + console.log("Discovered packages with commits and global commits:", packages); - if (publicPackages.length > 0) { - yield* Effect.log("📋 Public packages in release:"); + // STEP 4: Calculate the updates + // STEP 5: Read package.jsons from release branch (without checkout) + // STEP 6: Detect if Release PR is out of sync + // STEP 7: Set Commit Status + }); - for (const pkg of publicPackages) { - const version = pkg.version || "0.0.0"; + return runProgram(program); + }, + async prepare(): Promise { + const program = Effect.gen(function* () { + const git = yield* GitService; + const github = yield* GitHubService; + const workspace = yield* WorkspaceService; - yield* Effect.log(` • ${pkg.name}@${version}`); - } - } + yield* safeguardProgram; - // === Version Sync Verification === - yield* Effect.log("\n🔄 Verifying version synchronization..."); - - // For now, assume packages are in sync - add actual version comparison logic here - const isOutOfSync = false; - - // TODO: Add version comparison logic: - // 1. Calculate expected versions based on commits since last release - // 2. Compare with current package.json versions in the PR - // 3. Check if versions need to be bumped - - yield* Effect.log("✓ Package versions appear to be in sync"); - - // === Set Commit Status === - const statusContext = "ucdjs/release-verify"; - const commitHash = yield* git.getLastCommitHash; - - if (isOutOfSync) { - yield* github.createCommitStatus(commitHash, { - state: "failure", - context: statusContext, - description: "Release PR is out of sync with expected versions", - }); - yield* Effect.log("❌ Verification failed - set commit status to 'failure'"); - return yield* Effect.fail(new Error("Release verification failed")); - } else { - yield* github.createCommitStatus(commitHash, { - state: "success", - context: statusContext, - description: "Release PR verification passed", - target_url: releasePr.html_url, - }); - yield* Effect.log("✅ Verification passed - set commit status to 'success'"); + const releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release); + if (!releasePullRequest || !releasePullRequest.head) { + return yield* Effect.fail(new Error(`Release pull request for branch "${config.branch.release}" does not exist.`)); } - yield* Effect.log("\n✅ Comprehensive verification completed successfully"); + yield* Console.log(`✅ Release pull request #${releasePullRequest.number} exists.`); - // Helper function for basic verification when no PR exists - function* basicVerification() { - yield* Effect.log("\n📦 Basic workspace verification..."); + const currentBranch = yield* git.branches.get; + if (currentBranch !== config.branch.default) { + yield* git.branches.checkout(config.branch.default); + yield* Console.log(`✅ Checked out to default branch "${config.branch.default}".`); + } - const remoteUrl = yield* git.getRemoteUrl; - if (remoteUrl) { - yield* Effect.log(`✓ Remote configured: ${remoteUrl}`); - } else { - yield* Effect.log("⚠️ No remote repository configured"); - } + const overrides = yield* loadOverrides({ + sha: releasePullRequest.head.sha, + overridesPath: ".github/ucdjs-release.overrides.json", + }); - const branches = yield* git.listBranches; - yield* Effect.log(`✓ Available branches: ${branches.join(", ")}`); + console.log("Loaded overrides:", overrides); - yield* Effect.log(`✓ Workspace with ${packages.length} packages`); - yield* Effect.log("\n✅ Basic verification completed"); - } + const packages = yield* workspace.discoverWorkspacePackages.pipe( + Effect.flatMap(mergePackageCommitsIntoPackages), + Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), + ); + + console.log("Discovered packages with commits and global commits:", packages); }); return runProgram(program); }, + async publish(): Promise { + const program = Effect.gen(function* () { + return yield* Effect.fail(new Error("Not implemented yet.")); + }); + return runProgram(program); + }, packages: { async list(): Promise { const program = Effect.gen(function* () { const workspace = yield* WorkspaceService; - return yield* workspace.listPackages; + return yield* workspace.discoverWorkspacePackages; }); return runProgram(program); diff --git a/src/services/config.service.ts b/src/services/config.service.ts deleted file mode 100644 index a856b85..0000000 --- a/src/services/config.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NormalizedOptions } from "../utils/options.js"; -import { Context, Effect, Layer } from "effect"; - -export class ConfigOptions extends Context.Tag("@ucdjs/release-scripts/ConfigOptions")< - ConfigOptions, - NormalizedOptions ->() { - static layer(config: NormalizedOptions) { - return Layer.effect(ConfigOptions, Effect.succeed( - config, - )); - } -} diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 1d96cea..e822963 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -1,8 +1,9 @@ import { Command, CommandExecutor } from "@effect/platform"; import { NodeCommandExecutor } from "@effect/platform-node"; +import * as CommitParser from "commit-parser"; import { Effect, Layer } from "effect"; -import { GitCommandError } from "../errors"; -import { ConfigOptions } from "./config.service"; +import { ExternalCommitParserError, GitCommandError } from "../errors"; +import { ConfigOptions } from "../utils/options"; export class GitService extends Effect.Service()("@ucdjs/release-scripts/GitService", { effect: Effect.gen(function* () { @@ -64,12 +65,13 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr return execGitCommandIfNotDry(["branch", branch, base]); } - function checkoutBranch(branch: string) { - return Effect.gen(function* () { - const result = yield* execGitCommand(["checkout", branch]); + const getBranch = Effect.gen(function* () { + const output = yield* execGitCommand(["rev-parse", "--abbrev-ref", "HEAD"]); + return output.trim(); + }); - console.log(result); - }); + function checkoutBranch(branch: string) { + return execGitCommand(["checkout", branch]); } function stageChanges(files: readonly string[]) { @@ -95,18 +97,101 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr }); } + function readFileFromGit(filePath: string, ref: string = "HEAD") { + return execGitCommand(["show", `${ref}:${filePath}`]); + } + + function getMostRecentPackageTag(packageName: string) { + return execGitCommand(["tag", "--list", `${packageName}@*`]).pipe( + Effect.map((tags) => { + const tagList = tags + .trim() + .split("\n") + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); + + return tagList.reverse()[0] || null; + }), + ); + } + + function getCommits(options?: { + from?: string; + to?: string; + folder?: string; + }) { + return Effect.tryPromise({ + try: async () => CommitParser.getCommits({ + from: options?.from, + to: options?.to, + folder: options?.folder, + cwd: config.workspaceRoot, + }), + catch: (e) => new ExternalCommitParserError({ + message: `commit-parser getCommits`, + cause: e instanceof Error ? e.message : String(e), + }), + }); + } + + function filesChangesBetweenRefs(from: string, to: string) { + const commitsMap = new Map(); + + return execGitCommand(["log", "--name-only", "--format=%H", `${from}^..${to}`]).pipe( + Effect.map((output) => { + const lines = output.trim().split("\n").filter((line) => line.trim() !== ""); + + let currentSha: string | null = null; + const HASH_REGEX = /^[0-9a-f]{40}$/i; + + for (const line of lines) { + const trimmedLine = line.trim(); + + // Found a new commit hash + if (HASH_REGEX.test(trimmedLine)) { + currentSha = trimmedLine; + commitsMap.set(currentSha, []); + + continue; + } + + if (currentSha === null) { + // Malformed output: file path found before any commit hash + continue; + } + + // Found a file path, and we have a current hash to assign it to + // Note: In case of merge commits, an empty line might appear which is already filtered. + // If the line is NOT a hash, it must be a file path. + + // The file path is added to the array associated with the most recent hash. + commitsMap.get(currentSha)!.push(trimmedLine); + } + + return commitsMap; + }), + ); + } + return { branches: { list: listBranches, exists: doesBranchExist, create: createBranch, checkout: checkoutBranch, + get: getBranch, }, - commit: { + commits: { stage: stageChanges, write: writeCommit, push: pushChanges, + get: getCommits, + filesChangesBetweenRefs, + }, + tags: { + mostRecentForPackage: getMostRecentPackageTag, }, + readFileFromGit, isWithinRepository, isWorkingDirectoryClean, } as const; diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 5e98f4e..596c13c 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,6 +1,6 @@ import { Effect, Schema } from "effect"; import { GitHubError } from "../errors.js"; -import { ConfigOptions } from "./config.service.js"; +import { ConfigOptions } from "../utils/options.js"; import { GitService } from "./git.service.js"; // Schema definitions for GitHub API types diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index 40aa7d4..35c160d 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -1,42 +1,56 @@ +import type { FindWorkspacePackagesOptions } from "#shared/types"; import fs from "node:fs/promises"; import path from "node:path"; import { Command, CommandExecutor } from "@effect/platform"; -import { NodeTerminal } from "@effect/platform-node"; import { Effect, Schema } from "effect"; -import { WorkspaceError } from "../errors.js"; +import { WorkspaceError } from "../errors"; +import { ConfigOptions } from "../utils/options"; -const PackageJsonSchema = Schema.Struct({ - name: Schema.String, - dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), - devDependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), +export const DependencyObjectSchema = Schema.Record({ + key: Schema.String, + value: Schema.String, }); -const WorkspacePackageSchema = Schema.Struct({ +export const PackageJsonSchema = Schema.Struct({ name: Schema.String, + private: Schema.optional(Schema.Boolean), version: Schema.optional(Schema.String), - path: Schema.String, + dependencies: Schema.optional(DependencyObjectSchema), + devDependencies: Schema.optional(DependencyObjectSchema), + peerDependencies: Schema.optional(DependencyObjectSchema), }); -// pnpm list --json output can be an array of packages or an object with a packages property -const WorkspaceListSchema = Schema.Union( - Schema.Array(WorkspacePackageSchema), - Schema.transform( - Schema.Struct({ packages: Schema.Array(WorkspacePackageSchema) }), - Schema.Array(WorkspacePackageSchema), - { - decode: (obj) => obj.packages, - encode: (arr) => ({ packages: arr }), - }, - ), -); +export type PackageJson = Schema.Schema.Type; + +export const WorkspacePackageSchema = Schema.Struct({ + name: Schema.String, + version: Schema.String, + path: Schema.String, + packageJson: PackageJsonSchema, + workspaceDependencies: Schema.Array(Schema.String), + workspaceDevDependencies: Schema.Array(Schema.String), +}); export type WorkspacePackage = Schema.Schema.Type; +const WorkspaceListSchema = Schema.Array(Schema.Struct({ + name: Schema.String, + path: Schema.String, + version: Schema.String, + private: Schema.Boolean, + dependencies: Schema.optional(DependencyObjectSchema), + devDependencies: Schema.optional(DependencyObjectSchema), + peerDependencies: Schema.optional(DependencyObjectSchema), +})); + export class WorkspaceService extends Effect.Service()("@ucdjs/release-scripts/WorkspaceService", { effect: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; + const config = yield* ConfigOptions; - const listPackages = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json")).pipe( + const workspacePackageListOutput = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json").pipe( + Command.workingDirectory(config.workspaceRoot), + )).pipe( Effect.flatMap((stdout) => Effect.try({ try: () => JSON.parse(stdout), @@ -63,67 +77,168 @@ export class WorkspaceService extends Effect.Service()("@ucdjs Effect.cached, ); - const findPackageByName = (name: string) => - listPackages.pipe( - Effect.map((pkgs) => pkgs.find((p) => p.name === name)), + function readPackageJson(pkgPath: string) { + return Effect.tryPromise({ + try: async () => JSON.parse( + await fs.readFile(path.join(pkgPath, "package.json"), "utf8"), + ), + catch: (e) => new WorkspaceError({ + message: `Failed to read package.json for ${pkgPath}`, + cause: e, + operation: "readPackageJson", + }), + }).pipe( + Effect.flatMap((json) => Schema.decodeUnknown(PackageJsonSchema)(json).pipe( + Effect.mapError( + (e) => new WorkspaceError({ + message: `Invalid package.json for ${pkgPath}`, + cause: e, + operation: "readPackageJson", + }), + ), + )), + ); + } + + const discoverWorkspacePackages = Effect.gen(function* () { + let workspaceOptions: FindWorkspacePackagesOptions; + let explicitPackages: string[] | undefined; + + // Normalize package options and determine if packages were explicitly specified + if (config.packages == null || config.packages === true) { + workspaceOptions = { excludePrivate: false }; + } else if (Array.isArray(config.packages)) { + workspaceOptions = { excludePrivate: false, include: config.packages }; + explicitPackages = config.packages; + } else { + workspaceOptions = config.packages; + if (config.packages.include) { + explicitPackages = config.packages.include; + } + } + + const workspacePackages = yield* findWorkspacePackages( + workspaceOptions, ); - const readPackageJson = (pkgPath: string) => - Effect.tryPromise({ - try: async () => - JSON.parse( - await fs.readFile(path.join(pkgPath, "package.json"), "utf8"), - ), - catch: (e) => - new WorkspaceError({ - message: `Failed to read package.json for ${pkgPath}`, - cause: e, - operation: "readPackageJson", - }), - }).pipe( - Effect.flatMap((json) => - Schema.decodeUnknown(PackageJsonSchema)(json).pipe( - Effect.mapError( - (e) => - new WorkspaceError({ - message: `Invalid package.json for ${pkgPath}`, - cause: e, - operation: "readPackageJson", + // If specific packages were requested, validate they were all found + if (explicitPackages) { + const foundNames = new Set(workspacePackages.map((p) => p.name)); + const missing = explicitPackages.filter((p) => !foundNames.has(p)); + + if (missing.length > 0) { + return yield* Effect.fail( + new Error(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`), + ); + } + } + + // Show interactive prompt only if: + // 1. Not in CI + // 2. Prompt is enabled + // 3. No explicit packages were specified (user didn't pre-select specific packages) + // const isPackagePromptEnabled = config.prompts?.packages !== false; + // if (!isCI && isPackagePromptEnabled && !explicitPackages) { + // const selectedNames = await selectPackagePrompt(workspacePackages); + // workspacePackages = workspacePackages.filter((pkg) => + // selectedNames.includes(pkg.name), + // ); + // } + + return workspacePackages; + }); + + function findWorkspacePackages(options?: FindWorkspacePackagesOptions) { + return workspacePackageListOutput.pipe( + Effect.flatMap((rawProjects) => { + const allPackageNames = new Set(rawProjects.map((p) => p.name)); + const excludedPackages = new Set(); + + return Effect.all( + rawProjects.map((rawProject) => + readPackageJson(rawProject.path).pipe( + Effect.flatMap((packageJson) => { + if (!shouldIncludePackage(packageJson, options)) { + excludedPackages.add(rawProject.name); + return Effect.succeed(null); + } + + const pkg = { + name: rawProject.name, + version: rawProject.version, + path: rawProject.path, + packageJson, + workspaceDependencies: Object.keys(rawProject.dependencies || {}).filter((dep) => + allPackageNames.has(dep), + ), + workspaceDevDependencies: Object.keys(rawProject.devDependencies || {}).filter((dep) => + allPackageNames.has(dep), + ), + }; + + return Schema.decodeUnknown(WorkspacePackageSchema)(pkg).pipe( + Effect.mapError( + (e) => new WorkspaceError({ + message: `Invalid workspace package structure for ${rawProject.name}`, + cause: e, + operation: "findWorkspacePackages", + }), + ), + ); }), + Effect.catchAll(() => Effect.succeed(null)), + ), ), - ), - ), + ).pipe( + Effect.map((packages) => + packages.filter( + (pkg): pkg is WorkspacePackage => pkg !== null, + ), + ), + ); + }), ); + } - const getPackageGraph = listPackages.pipe( - Effect.flatMap((pkgs) => - Effect.forEach(pkgs, (p) => - p.path - ? readPackageJson(p.path).pipe( - Effect.map((pj) => ({ - name: p.name, - deps: [ - ...Object.keys(pj.dependencies ?? {}), - ...Object.keys(pj.devDependencies ?? {}), - ], - })), - ) - : Effect.succeed({ name: p.name, deps: [] })).pipe( - Effect.map((entries) => { - const graph: Record = {}; - for (const e of entries) graph[e.name] = e.deps; - return graph; - }), + function shouldIncludePackage(pkg: PackageJson, options?: FindWorkspacePackagesOptions): boolean { + if (!options) { + return true; + } + + // Check if private packages should be excluded + if (options.excludePrivate && pkg.private) { + return false; + } + + // Check include list (if specified, only these packages are included) + if (options.include && options.include.length > 0) { + if (!options.include.includes(pkg.name)) { + return false; + } + } + + // Check exclude list + if (options.exclude?.includes(pkg.name)) { + return false; + } + + return true; + } + + function findPackageByName(packageName: string) { + return discoverWorkspacePackages.pipe( + Effect.map((packages) => + packages.find((pkg) => pkg.name === packageName) || null, ), - ), - ); + ); + } return { - listPackages, - findPackageByName, readPackageJson, - getPackageGraph, + findWorkspacePackages, + discoverWorkspacePackages, + findPackageByName, } as const; }), dependencies: [], -}) {} +}) { } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..d74836a --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,315 @@ +import type * as CommitParser from "commit-parser"; +import type { WorkspacePackage } from "../services/workspace.service"; +import { Effect, Schema } from "effect"; +import { OverridesLoadError } from "../errors"; +import { GitService } from "../services/git.service"; +import { WorkspacePackageSchema } from "../services/workspace.service"; + +export interface VersionOverrides { + [packageName: string]: string; +} + +export interface LoadOverridesOptions { + sha: string; + overridesPath: string; +} + +export function loadOverrides(options: LoadOverridesOptions) { + return Effect.gen(function* () { + const git = yield* GitService; + + return yield* git.readFileFromGit(options.overridesPath, "utf8").pipe( + Effect.map((content) => ({ + content, + readError: null as unknown, + })), + Effect.catchAll((err) => + Effect.succeed({ + content: "", + readError: err, + }), + ), + Effect.flatMap(({ content, readError }) => { + if (!content) { + return Effect.succeed({} as VersionOverrides); + } + + return Effect.try({ + try: () => JSON.parse(content) as VersionOverrides, + catch: (err) => { + return new OverridesLoadError({ + message: "Failed to parse overrides file.", + cause: readError || err, + }); + }, + }).pipe( + Effect.catchAll(() => Effect.succeed({} as VersionOverrides)), + ); + }), + ); + }); +} + +const GitCommitSchema = Schema.Struct({ + isConventional: Schema.Boolean, + isBreaking: Schema.Boolean, + type: Schema.String, + scope: Schema.Union(Schema.String, Schema.Undefined), + description: Schema.String, + references: Schema.Array(Schema.Struct({ + type: Schema.Union(Schema.Literal("issue"), Schema.Literal("pull-request")), + value: Schema.String, + })), + authors: Schema.Array(Schema.Struct({ + name: Schema.String, + email: Schema.String, + profile: Schema.optional(Schema.String), + })), + hash: Schema.String, + shortHash: Schema.String, + body: Schema.String, + message: Schema.String, + date: Schema.String, +}); + +export const WorkspacePackageWithCommitsSchema = Schema.Struct({ + ...WorkspacePackageSchema.fields, + commits: Schema.Array(GitCommitSchema), + globalCommits: Schema.Array(GitCommitSchema).pipe( + Schema.propertySignature, + Schema.withConstructorDefault(() => []), + ), +}); + +export type WorkspacePackageWithCommits = Schema.Schema.Type; + +export function mergePackageCommitsIntoPackages( + packages: readonly WorkspacePackage[], +) { + return Effect.gen(function* () { + const git = yield* GitService; + + return yield* Effect.forEach(packages, (pkg) => + Effect.gen(function* () { + const lastTag = yield* git.tags.mostRecentForPackage(pkg.name); + + const commits = yield* git.commits.get({ + from: lastTag || undefined, + to: "HEAD", + folder: pkg.path, + }); + + const withCommits = { + ...pkg, + commits, + globalCommits: [], + }; + + return yield* Schema.decode(WorkspacePackageWithCommitsSchema)(withCommits).pipe( + Effect.mapError((e) => new Error(`Failed to decode package with commits for ${pkg.name}: ${e}`)), + ); + })); + }); +} + +/** + * Retrieves global commits that affect all packages in a monorepo. + * + * This function handles an important edge case in monorepo releases: + * When pkg-a is released, then a global change is made, and then pkg-b is released, + * we need to ensure that the global change is only attributed to pkg-a's release, + * not re-counted for pkg-b. + * + * Algorithm: + * 1. Find the overall commit range across all packages + * 2. Fetch all commits and file changes once for this range + * 3. For each package, filter commits based on its last tag cutoff + * 4. Apply mode-specific filtering for global commits + * + * Example scenario: + * - pkg-a: last released at commit A + * - global change at commit B (after A) + * - pkg-b: last released at commit C (after B) + * + * Result: + * - For pkg-a: includes commits from A to HEAD (including B) + * - For pkg-b: includes commits from C to HEAD (excluding B, since it was already in pkg-b's release range) + * + * @param packages - Array of workspace packages with their associated commits + * @param mode - Determines which global commits to include: + * - "none": No global commits (returns empty map) + * - "all": All commits that touch files outside any package directory + * - "dependencies": Only commits that touch dependency-related files (package.json, lock files, etc.) + * + * @returns A map of package names to their relevant global commits + */ +export function mergeCommitsAffectingGloballyIntoPackage( + packages: readonly WorkspacePackageWithCommits[], + mode: "none" | "all" | "dependencies", +) { + return Effect.gen(function* () { + const git = yield* GitService; + + // Early return for "none" mode + if (mode === "none") { + return packages; + } + + const [oldestCommitSha, newestCommitSha] = findCommitRange(packages); + if (oldestCommitSha == null || newestCommitSha == null) { + return packages; + } + + const allCommits = yield* git.commits.get({ + from: oldestCommitSha, + to: newestCommitSha, + folder: ".", + }); + + const affectedFilesPerCommit = yield* git.commits.filesChangesBetweenRefs( + oldestCommitSha, + newestCommitSha, + ); + + // Used for quick lookup of commit timestamps/cutoffs + const commitTimestamps = new Map( + allCommits.map((c) => [c.shortHash, new Date(c.date).getTime()]), + ); + + const packagePaths = new Set(packages.map((p) => p.path)); + const result = new Map(); + + for (const pkg of packages) { + // Get the package's last release tag timestamp + const lastTag = yield* git.tags.mostRecentForPackage(pkg.name); + const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag) ?? 0 : 0; + + const globalCommits: CommitParser.GitCommit[] = []; + + // Filter commits that occurred after this package's last release + for (const commit of allCommits) { + const commitTimestamp = commitTimestamps.get(commit.shortHash); + if (commitTimestamp == null || commitTimestamp <= cutoffTimestamp) { + continue; // Skip commits at or before the package's last release + } + + const files = affectedFilesPerCommit.get(commit.shortHash); + if (!files) continue; + + // Check if this commit is a global commit + if (isGlobalCommit(files, packagePaths)) { + // Apply mode-specific filtering + if (mode === "dependencies") { + if (files.some((file) => isDependencyFile(file))) { + globalCommits.push(commit); + } + } else { + // mode === "all" + globalCommits.push(commit); + } + } + } + + result.set(pkg.name, globalCommits); + } + + return Effect.succeed(packages.map((pkg) => ({ + ...pkg, + globalCommits: result.get(pkg.name) || [], + }))); + }); +} + +/** + * Determines if a commit is "global" (affects files outside any package directory). + * + * @param files - List of files changed in the commit + * @param packagePaths - Set of package directory paths + * @returns true if at least one file is outside all package directories + */ +function isGlobalCommit(files: readonly string[], packagePaths: Set): boolean { + return files.some((file) => { + const normalized = file.startsWith("./") ? file.slice(2) : file; + + // Check if file is under any package path + for (const pkgPath of packagePaths) { + if (normalized === pkgPath || normalized.startsWith(`${pkgPath}/`)) { + return false; // File is inside a package, not global + } + } + + return true; // File is outside all packages, therefore global + }); +} + +/** + * Files that are considered dependency-related in a monorepo. + */ +const DEPENDENCY_FILES = new Set([ + "package.json", + "pnpm-lock.yaml", + "yarn.lock", + "package-lock.json", + "pnpm-workspace.yaml", +]); + +/** + * Determines if a file is dependency-related. + * + * @param file - File path to check + * @returns true if the file is a dependency file (package.json, lock files, etc.) + */ +function isDependencyFile(file: string): boolean { + const normalized = file.startsWith("./") ? file.slice(2) : file; + + // Check if it's a root-level dependency file + if (DEPENDENCY_FILES.has(normalized)) return true; + + // Check if it ends with a dependency file name (e.g., "packages/foo/package.json") + return Array.from(DEPENDENCY_FILES).some((dep) => normalized.endsWith(`/${dep}`)); +} + +/** + * Finds the oldest and newest commits across all packages. + * + * This establishes the overall time range we need to analyze for global commits. + * + * @param packages - Array of packages with their commits + * @returns Tuple of [oldestCommitSha, newestCommitSha], or [null, null] if no commits found + */ +function findCommitRange(packages: readonly WorkspacePackageWithCommits[]): [oldestCommit: string | null, newestCommit: string | null] { + let oldestCommit: CommitParser.GitCommit | null = null; + let newestCommit: CommitParser.GitCommit | null = null; + + for (const pkg of packages) { + if (pkg.commits.length === 0) { + continue; + } + + const firstCommit = pkg.commits[0]; + if (!firstCommit) { + throw new Error(`No commits found for package ${pkg.name}`); + } + + const lastCommit = pkg.commits[pkg.commits.length - 1]; + if (!lastCommit) { + throw new Error(`No commits found for package ${pkg.name}`); + } + + // Update newest commit if this package has a more recent commit + if (newestCommit == null || new Date(lastCommit.date) > new Date(newestCommit.date)) { + newestCommit = lastCommit; + } + + // Update oldest commit if this package has an older commit + if (oldestCommit == null || new Date(firstCommit.date) < new Date(oldestCommit.date)) { + oldestCommit = firstCommit; + } + } + + if (oldestCommit == null || newestCommit == null) { + return [null, null]; + } + + return [oldestCommit.shortHash, newestCommit.shortHash]; +} diff --git a/src/utils/options.ts b/src/utils/options.ts index 9e70061..e8e621f 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -1,4 +1,16 @@ import process from "node:process"; +import { Context, Effect, Layer } from "effect"; + +export class ConfigOptions extends Context.Tag("@ucdjs/release-scripts/ConfigOptions")< + ConfigOptions, + NormalizedOptions +>() { + static layer(config: NormalizedOptions) { + return Layer.effect(ConfigOptions, Effect.succeed( + config, + )); + } +} export interface Options { /** From c7f9ee14f7be9d591492a5f7580be74a580b3478 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 10:25:12 +0100 Subject: [PATCH 05/35] feat: introduce new services for dependency graph and version calculation - Added `DependencyGraphService` to manage package dependencies and their update order. - Introduced `VersionCalculatorService` to calculate version bumps based on commit types. - Created `PackageUpdaterService` to apply version updates to package.json files. - Refactored imports in `index.ts` to utilize new service structure. - Removed unused `changelog.ts` file. - Updated `package.json` to reflect new service imports. --- package.json | 7 +- src/errors.ts | 6 + src/index.ts | 51 +++++++-- src/services/dependency-graph.service.ts | 93 ++++++++++++++++ src/services/package-updater.service.ts | 123 +++++++++++++++++++++ src/services/version-calculator.service.ts | 100 +++++++++++++++++ src/utils/changelog.ts | 0 src/utils/helpers.ts | 6 +- 8 files changed, 365 insertions(+), 21 deletions(-) create mode 100644 src/services/dependency-graph.service.ts create mode 100644 src/services/package-updater.service.ts create mode 100644 src/services/version-calculator.service.ts delete mode 100644 src/utils/changelog.ts diff --git a/package.json b/package.json index c714e15..28e2ce6 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,7 @@ "url": "git+https://github.com/ucdjs/release-scripts.git" }, "imports": { - "#core/*": "./src/core/*.ts", - "#versioning/*": "./src/versioning/*.ts", - "#shared/*": "./src/shared/*.ts", - "#release": "./src/release.ts", - "#publish": "./src/publish.ts", - "#verify": "./src/verify.ts" + "#services/*": "./src/services/*.service.ts" }, "exports": { ".": "./dist/index.mjs", diff --git a/src/errors.ts b/src/errors.ts index e9ac8bb..3f8c99c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -35,6 +35,12 @@ export class GitHubError extends Data.TaggedError("GitHubError")<{ cause?: unknown; }> { } +export class VersionCalculationError extends Data.TaggedError("VersionCalculationError")<{ + message: string; + packageName?: string; + cause?: unknown; +}> { } + export class OverridesLoadError extends Data.TaggedError("OverridesLoadError")<{ message: string; cause?: unknown; diff --git a/src/index.ts b/src/index.ts index 425d4ed..c3622cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,21 @@ -import type { WorkspacePackage } from "./services/workspace.service.js"; -import type { NormalizedOptions, Options } from "./utils/options.js"; +import type { WorkspacePackage } from "./services/workspace.service"; +import type { Options } from "./utils/options"; +import { DependencyGraphService } from "#services/dependency-graph"; +import { GitService } from "#services/git"; +import { GitHubService } from "#services/github"; +import { PackageUpdaterService } from "#services/package-updater"; +import { VersionCalculatorService } from "#services/version-calculator"; +import { WorkspaceService } from "#services/workspace"; import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; import { Console, Effect, Layer } from "effect"; -import { GitService } from "./services/git.service.js"; -import { GitHubService } from "./services/github.service.js"; -import { VersionUpdaterService } from "./services/version-updater.service.js"; -import { WorkspaceService } from "./services/workspace.service.js"; -import { loadOverrides, mergeCommitsAffectingGloballyIntoPackage, mergePackageCommitsIntoPackages } from "./utils/helpers.js"; -import { ConfigOptions, normalizeOptions } from "./utils/options.js"; +import { + loadOverrides, + mergeCommitsAffectingGloballyIntoPackage, + mergePackageCommitsIntoPackages, +} from "./utils/helpers"; +import { ConfigOptions, normalizeOptions } from "./utils/options"; -export type { Options } from "./utils/options.js"; +export type { Options } from "./utils/options"; export interface ReleaseScriptsAPI { verify: () => Promise; @@ -29,7 +35,9 @@ export async function createReleaseScripts(options: Options): Promise mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), ); - console.log("Discovered packages with commits and global commits:", packages); + yield* Console.log("Discovered packages with commits and global commits:", packages); + + const releases = yield* versionCalculator.calculateBumps(packages, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages); + + yield* Console.log("Calculated releases:", releases); + yield* Console.log("Release order:", ordered); // STEP 4: Calculate the updates // STEP 5: Read package.jsons from release branch (without checkout) @@ -110,6 +126,9 @@ export async function createReleaseScripts(options: Options): Promise()( + "@ucdjs/release-scripts/DependencyGraphService", + { + effect: Effect.gen(function* () { + function buildGraph(packages: readonly WorkspacePackage[]) { + const nameToPackage = new Map(); + const adjacency = new Map>(); + const inDegree = new Map(); + + for (const pkg of packages) { + nameToPackage.set(pkg.name, pkg); + adjacency.set(pkg.name, new Set()); + inDegree.set(pkg.name, 0); + } + + for (const pkg of packages) { + const deps = new Set([ + ...pkg.workspaceDependencies, + ...pkg.workspaceDevDependencies, + ]); + + for (const depName of deps) { + if (!nameToPackage.has(depName)) { + continue; + } + + adjacency.get(depName)?.add(pkg.name); + inDegree.set(pkg.name, (inDegree.get(pkg.name) ?? 0) + 1); + } + } + + return { nameToPackage, adjacency, inDegree } as const; + } + + function topologicalOrder(packages: readonly WorkspacePackage[]): Effect.Effect { + return Effect.gen(function* () { + const { nameToPackage, adjacency, inDegree } = buildGraph(packages); + + const queue: Array = []; + const levels = new Map(); + + for (const [name, degree] of inDegree) { + if (degree === 0) { + queue.push(name); + levels.set(name, 0); + } + } + + const ordered: PackageUpdateOrder[] = []; + + while (queue.length > 0) { + const current = queue.shift()!; + const currentLevel = levels.get(current) ?? 0; + + const pkg = nameToPackage.get(current); + if (pkg) { + ordered.push({ package: pkg, level: currentLevel }); + } + + for (const neighbor of adjacency.get(current) ?? []) { + const nextLevel = currentLevel + 1; + const existingLevel = levels.get(neighbor) ?? 0; + if (nextLevel > existingLevel) { + levels.set(neighbor, nextLevel); + } + + const newDegree = (inDegree.get(neighbor) ?? 0) - 1; + inDegree.set(neighbor, newDegree); + if (newDegree === 0) { + queue.push(neighbor); + } + } + } + + if (ordered.length !== packages.length) { + return yield* Effect.fail(new Error("Cycle detected in workspace dependencies")); + } + + return ordered; + }); + } + + return { + topologicalOrder, + } as const; + }), + dependencies: [], + }, +) {} diff --git a/src/services/package-updater.service.ts b/src/services/package-updater.service.ts new file mode 100644 index 0000000..9f90d16 --- /dev/null +++ b/src/services/package-updater.service.ts @@ -0,0 +1,123 @@ +import type { PackageRelease } from "../shared/types"; +import type { WorkspacePackage } from "./workspace.service"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Effect } from "effect"; +import { ConfigOptions } from "../utils/options"; + +function nextRange(oldRange: string, newVersion: string): string { + const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : ""; + const raw = workspacePrefix ? oldRange.slice("workspace:".length) : oldRange; + + if (raw === "*" || raw === "latest") { + return `${workspacePrefix}${raw}`; + } + + const prefix = raw.startsWith("^") || raw.startsWith("~") ? raw[0] : ""; + return `${workspacePrefix}${prefix}${newVersion}`; +} + +function updateDependencyRecord( + record: Record | undefined, + releaseMap: ReadonlyMap, +): { updated: boolean; next: Record | undefined } { + if (!record) return { updated: false, next: undefined }; + + let changed = false; + const next: Record = { ...record }; + + for (const [dep, currentRange] of Object.entries(record)) { + const bumped = releaseMap.get(dep); + if (!bumped) continue; + + const updatedRange = nextRange(currentRange, bumped); + if (updatedRange !== currentRange) { + next[dep] = updatedRange; + changed = true; + } + } + + return { updated: changed, next: changed ? next : record }; +} + +export class PackageUpdaterService extends Effect.Service()( + "@ucdjs/release-scripts/PackageUpdaterService", + { + effect: Effect.gen(function* () { + const config = yield* ConfigOptions; + + function writePackageJson(pkgPath: string, json: unknown) { + const fullPath = path.join(pkgPath, "package.json"); + const content = `${JSON.stringify(json, null, 2)}\n`; + + if (config.dryRun) { + return Effect.succeed(`Dry run: skip writing ${fullPath}`); + } + + return Effect.tryPromise({ + try: async () => { + await fs.writeFile(fullPath, content, "utf8"); + }, + catch: (e) => e as Error, + }); + } + + function applyReleases( + allPackages: readonly WorkspacePackage[], + releases: readonly PackageRelease[], + ) { + const releaseMap = new Map(); + for (const release of releases) { + releaseMap.set(release.package.name, release.newVersion); + } + + return Effect.all( + allPackages.map((pkg) => + Effect.gen(function* () { + const releaseVersion = releaseMap.get(pkg.name); + const nextJson = { ...pkg.packageJson } as Record; + + let updated = false; + + if (releaseVersion && pkg.packageJson.version !== releaseVersion) { + nextJson.version = releaseVersion; + updated = true; + } + + const depsResult = updateDependencyRecord(pkg.packageJson.dependencies, releaseMap); + if (depsResult.updated) { + nextJson.dependencies = depsResult.next; + updated = true; + } + + const devDepsResult = updateDependencyRecord(pkg.packageJson.devDependencies, releaseMap); + if (devDepsResult.updated) { + nextJson.devDependencies = devDepsResult.next; + updated = true; + } + + const peerDepsResult = updateDependencyRecord(pkg.packageJson.peerDependencies, releaseMap); + if (peerDepsResult.updated) { + nextJson.peerDependencies = peerDepsResult.next; + updated = true; + } + + if (!updated) { + return "skipped" as const; + } + + return yield* writePackageJson(pkg.path, nextJson).pipe( + Effect.map(() => "written" as const), + ); + }), + ), + ); + } + + return { + applyReleases, + } as const; + }), + dependencies: [], + }, +) {} diff --git a/src/services/version-calculator.service.ts b/src/services/version-calculator.service.ts new file mode 100644 index 0000000..2a0002c --- /dev/null +++ b/src/services/version-calculator.service.ts @@ -0,0 +1,100 @@ +import type { BumpKind, PackageRelease } from "../shared/types"; +import type { WorkspacePackageWithCommits } from "../utils/helpers"; +import { Effect } from "effect"; +import semver from "semver"; +import { VersionCalculationError } from "../errors"; + +const BUMP_PRIORITY: Record = { + none: 0, + patch: 1, + minor: 2, + major: 3, +}; + +function maxBump(current: BumpKind, incoming: BumpKind): BumpKind { + return BUMP_PRIORITY[incoming] > BUMP_PRIORITY[current] ? incoming : current; +} + +function bumpFromCommit(commit: { type?: string; isBreaking?: boolean }): BumpKind { + if (commit.isBreaking) return "major"; + if (commit.type === "feat") return "minor"; + if (commit.type === "fix" || commit.type === "perf") return "patch"; + return "none"; +} + +function determineBump(commits: ReadonlyArray<{ type?: string; isBreaking?: boolean }>): BumpKind { + return commits.reduce((acc, commit) => maxBump(acc, bumpFromCommit(commit)), "none"); +} + +export class VersionCalculatorService extends Effect.Service()( + "@ucdjs/release-scripts/VersionCalculatorService", + { + effect: Effect.gen(function* () { + function calculateBumps( + packages: readonly WorkspacePackageWithCommits[], + overrides: Readonly>, + ) { + return Effect.all( + packages.map((pkg) => + Effect.gen(function* () { + const allCommits = [...pkg.commits, ...pkg.globalCommits]; + const bumpType = determineBump(allCommits); + const hasDirectChanges = pkg.commits.length > 0; + + let nextVersion: string | null = null; + + const override = overrides[pkg.name]; + if (override) { + if (!semver.valid(override)) { + return yield* Effect.fail(new VersionCalculationError({ + message: `Invalid override version for ${pkg.name}: ${override}`, + packageName: pkg.name, + })); + } + nextVersion = override; + } + + if (nextVersion === null) { + if (bumpType === "none") { + nextVersion = pkg.version; + } else { + const bumped = semver.inc(pkg.version, bumpType); + if (!bumped) { + return yield* Effect.fail(new VersionCalculationError({ + message: `Failed to bump version for ${pkg.name} using bump type ${bumpType}`, + packageName: pkg.name, + })); + } + nextVersion = bumped; + } + } + + // TODO: Insert interactive version prompt here if prompts.versions is enabled. + + return { + package: { + name: pkg.name, + version: pkg.version, + path: pkg.path, + packageJson: pkg.packageJson, + workspaceDependencies: pkg.workspaceDependencies, + workspaceDevDependencies: pkg.workspaceDevDependencies, + }, + currentVersion: pkg.version, + newVersion: nextVersion, + bumpType, + hasDirectChanges, + } satisfies PackageRelease; + }), + ), + { concurrency: "unbounded" }, + ); + } + + return { + calculateBumps, + } as const; + }), + dependencies: [], + }, +) {} diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index d74836a..fb85a38 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,9 +1,9 @@ +import type { WorkspacePackage } from "#services/workspace"; import type * as CommitParser from "commit-parser"; -import type { WorkspacePackage } from "../services/workspace.service"; +import { GitService } from "#services/git"; +import { WorkspacePackageSchema } from "#services/workspace"; import { Effect, Schema } from "effect"; import { OverridesLoadError } from "../errors"; -import { GitService } from "../services/git.service"; -import { WorkspacePackageSchema } from "../services/workspace.service"; export interface VersionOverrides { [packageName: string]: string; From 68c4990cb1db8902ef82d3c24fcc20df92c30b64 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:07:48 +0100 Subject: [PATCH 06/35] feat: implement `ReleaseScriptsOptions` and refactor related services - Introduced `ReleaseScriptsOptions` for improved configuration handling. - Refactored `createReleaseScripts` to utilize `ReleaseScriptsOptionsInput`. - Updated `GitService`, `GitHubService`, and `PackageUpdaterService` to use the new options structure. - Added `assertWorkspaceReady` method in `GitService` for workspace validation. - Removed deprecated `ConfigOptions` and related tests. - Added tests for `normalizeReleaseScriptsOptions` to ensure correct behavior. --- src/index.ts | 128 ++++------------ src/options.ts | 115 ++++++++++++++ src/services/git.service.ts | 35 +++-- src/services/github.service.ts | 2 +- src/services/package-updater.service.ts | 2 +- src/services/workspace.service.ts | 2 +- src/shared/types.ts | 136 ---------------- src/utils/helpers.ts | 4 +- src/utils/options.ts | 196 ------------------------ src/verify.ts | 65 ++++++++ test/index.test.ts | 15 -- test/options.test.ts | 103 +++++++++++++ test/services/git.service.test.ts | 126 --------------- 13 files changed, 346 insertions(+), 583 deletions(-) create mode 100644 src/options.ts delete mode 100644 src/shared/types.ts delete mode 100644 src/utils/options.ts create mode 100644 src/verify.ts delete mode 100644 test/index.test.ts create mode 100644 test/options.test.ts delete mode 100644 test/services/git.service.test.ts diff --git a/src/index.ts b/src/index.ts index c3622cc..8bfadc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ +import type { ReleaseScriptsOptionsInput } from "./options"; import type { WorkspacePackage } from "./services/workspace.service"; -import type { Options } from "./utils/options"; import { DependencyGraphService } from "#services/dependency-graph"; import { GitService } from "#services/git"; import { GitHubService } from "#services/github"; @@ -8,16 +8,15 @@ import { VersionCalculatorService } from "#services/version-calculator"; import { WorkspaceService } from "#services/workspace"; import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; import { Console, Effect, Layer } from "effect"; +import { normalizeReleaseScriptsOptions, ReleaseScriptsOptions } from "./options"; import { loadOverrides, mergeCommitsAffectingGloballyIntoPackage, mergePackageCommitsIntoPackages, } from "./utils/helpers"; -import { ConfigOptions, normalizeOptions } from "./utils/options"; +import { constructVerifyProgram } from "./verify"; -export type { Options } from "./utils/options"; - -export interface ReleaseScriptsAPI { +export interface ReleaseScripts { verify: () => Promise; prepare: () => Promise; publish: () => Promise; @@ -27,55 +26,48 @@ export interface ReleaseScriptsAPI { }; } -export async function createReleaseScripts(options: Options): Promise { - const config = normalizeOptions(options); - const cwd = config.workspaceRoot; - - const MainLayer = Layer.mergeAll( - GitService.Default, - WorkspaceService.Default, - GitHubService.Default, - DependencyGraphService.Default, - PackageUpdaterService.Default, - VersionCalculatorService.Default, - ).pipe( - Layer.provide(ConfigOptions.layer(config)), +export async function createReleaseScripts(options: ReleaseScriptsOptionsInput): Promise { + const config = normalizeReleaseScriptsOptions(options); + + const AppLayer = Layer.succeed(ReleaseScriptsOptions, config).pipe( Layer.provide(NodeCommandExecutor.layer), Layer.provide(NodeFileSystem.layer), + Layer.provide(GitService.Default), + Layer.provide(GitHubService.Default), + Layer.provide(DependencyGraphService.Default), + Layer.provide(PackageUpdaterService.Default), + Layer.provide(VersionCalculatorService.Default), + Layer.provide(WorkspaceService.Default), ); - const runProgram = (program: Effect.Effect): Promise => - Effect.runPromise(Effect.provide(program, MainLayer) as Effect.Effect); + const runProgram = (program: Effect.Effect): Promise => { + const provided = program.pipe(Effect.provide(AppLayer)); + return Effect.runPromise(provided as Effect.Effect); + }; const safeguardProgram = Effect.gen(function* () { const git = yield* GitService; - - const isWithinRepository = yield* git.isWithinRepository; - if (!isWithinRepository) { - return yield* Effect.fail(new Error(`The directory ${cwd} is not a git repository.`)); - } - - const isWorkingDirectoryClean = yield* git.isWorkingDirectoryClean; - if (!isWorkingDirectoryClean) { - return yield* Effect.fail(new Error("The git repository has uncommitted changes.")); - } - - return yield* Effect.succeed(void 0); + return yield* git.workspace.assertWorkspaceReady; }); - await Effect.runPromise(Effect.provide(safeguardProgram, MainLayer).pipe( - Effect.catchAll((err) => { - console.error(`❌ Initialization failed: ${err.message}`); - return Effect.exit(Effect.fail(err)); - }), - )); + try { + await runProgram(safeguardProgram); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + await Effect.runPromise(Console.error(`❌ Initialization failed: ${message}`)); + throw err; + } return { async verify(): Promise { + return runProgram(constructVerifyProgram(config)); + }, + async prepare(): Promise { const program = Effect.gen(function* () { const git = yield* GitService; const github = yield* GitHubService; const dependencyGraph = yield* DependencyGraphService; + const packageUpdater = yield* PackageUpdaterService; const versionCalculator = yield* VersionCalculatorService; const workspace = yield* WorkspaceService; @@ -101,71 +93,19 @@ export async function createReleaseScripts(options: Options): Promise mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), - ); + )); yield* Console.log("Discovered packages with commits and global commits:", packages); - const releases = yield* versionCalculator.calculateBumps(packages, overrides); - const ordered = yield* dependencyGraph.topologicalOrder(packages); + const releases = yield* versionCalculator.calculateBumps(packages as any, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages as any); yield* Console.log("Calculated releases:", releases); yield* Console.log("Release order:", ordered); - // STEP 4: Calculate the updates - // STEP 5: Read package.jsons from release branch (without checkout) - // STEP 6: Detect if Release PR is out of sync - // STEP 7: Set Commit Status - }); - - return runProgram(program); - }, - async prepare(): Promise { - const program = Effect.gen(function* () { - const git = yield* GitService; - const github = yield* GitHubService; - const dependencyGraph = yield* DependencyGraphService; - const packageUpdater = yield* PackageUpdaterService; - const versionCalculator = yield* VersionCalculatorService; - const workspace = yield* WorkspaceService; - - yield* safeguardProgram; - - const releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release); - if (!releasePullRequest || !releasePullRequest.head) { - return yield* Effect.fail(new Error(`Release pull request for branch "${config.branch.release}" does not exist.`)); - } - - yield* Console.log(`✅ Release pull request #${releasePullRequest.number} exists.`); - - const currentBranch = yield* git.branches.get; - if (currentBranch !== config.branch.default) { - yield* git.branches.checkout(config.branch.default); - yield* Console.log(`✅ Checked out to default branch "${config.branch.default}".`); - } - - const overrides = yield* loadOverrides({ - sha: releasePullRequest.head.sha, - overridesPath: ".github/ucdjs-release.overrides.json", - }); - - console.log("Loaded overrides:", overrides); - - const packages = yield* workspace.discoverWorkspacePackages.pipe( - Effect.flatMap(mergePackageCommitsIntoPackages), - Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), - ); - - console.log("Discovered packages with commits and global commits:", packages); - - const releases = yield* versionCalculator.calculateBumps(packages, overrides); - const ordered = yield* dependencyGraph.topologicalOrder(packages); - - console.log("Calculated releases:", releases); - console.log("Release order:", ordered); - yield* packageUpdater.applyReleases(packages, releases); }); diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..7559d7c --- /dev/null +++ b/src/options.ts @@ -0,0 +1,115 @@ +import process from "node:process"; +import { Context } from "effect"; + +type DeepRequired = Required<{ + [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired +}>; + +export interface ReleaseScriptsOptionsInput { + dryRun?: boolean; + repo: `${string}/${string}`; + workspaceRoot?: string; + packages?: true | { + exclude?: string[]; + include?: string[]; + excludePrivate?: boolean; + } | string[]; + githubToken: string; + branch?: { + release?: string; + default?: string; + }; + globalCommitMode?: "dependencies" | "all" | "none"; + pullRequest?: { + title?: string; + body?: string; + }; + types?: Record; + changelog?: { + enabled?: boolean; + template?: string; + emojis?: boolean; + }; +} + +export type NormalizedReleaseScriptsOptions = DeepRequired> & { + owner: string; + repo: string; +}; + +const DEFAULT_PR_BODY_TEMPLATE = `## Summary\n\nThis PR contains the following changes:\n\n- Updated package versions\n- Updated changelogs\n\n## Packages\n\nThe following packages will be released:\n\n{{packages}}`; +const DEFAULT_CHANGELOG_TEMPLATE = `# Changelog\n\n{{releases}}`; + +export function normalizeReleaseScriptsOptions(options: ReleaseScriptsOptionsInput): NormalizedReleaseScriptsOptions { + const { + workspaceRoot = process.cwd(), + githubToken = "", + repo: fullRepo, + packages = true, + branch = {}, + globalCommitMode = "dependencies", + pullRequest = {}, + changelog = {}, + dryRun = false, + } = options; + + const token = githubToken.trim(); + if (!token) { + throw new Error("GitHub token is required. Pass it in via options."); + } + + if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) { + throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world')."); + } + + const [owner, repo] = fullRepo.split("/"); + if (!owner || !repo) { + throw new Error(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`); + } + + const normalizedPackages = typeof packages === "object" && !Array.isArray(packages) + ? { + exclude: packages.exclude ?? [], + include: packages.include ?? [], + excludePrivate: packages.excludePrivate ?? false, + } + : packages; + + return { + dryRun, + workspaceRoot, + githubToken: token, + owner, + repo, + packages: normalizedPackages, + branch: { + release: branch.release ?? "release/next", + default: branch.default ?? "main", + }, + globalCommitMode, + pullRequest: { + title: pullRequest.title ?? "chore: release new version", + body: pullRequest.body ?? DEFAULT_PR_BODY_TEMPLATE, + }, + changelog: { + enabled: changelog.enabled ?? true, + template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE, + emojis: changelog.emojis ?? true, + }, + types: { + feat: { title: "🚀 Features" }, + fix: { title: "🐞 Bug Fixes" }, + refactor: { title: "🔧 Code Refactoring" }, + perf: { title: "🏎 Performance" }, + docs: { title: "📚 Documentation" }, + style: { title: "🎨 Styles" }, + }, + }; +} + +export class ReleaseScriptsOptions extends Context.Tag("@ucdjs/release-scripts/ReleaseScriptsOptions")< + ReleaseScriptsOptions, + NormalizedReleaseScriptsOptions +>() { } diff --git a/src/services/git.service.ts b/src/services/git.service.ts index e822963..2546aae 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -3,12 +3,12 @@ import { NodeCommandExecutor } from "@effect/platform-node"; import * as CommitParser from "commit-parser"; import { Effect, Layer } from "effect"; import { ExternalCommitParserError, GitCommandError } from "../errors"; -import { ConfigOptions } from "../utils/options"; +import { ReleaseScriptsOptions } from "../options"; export class GitService extends Effect.Service()("@ucdjs/release-scripts/GitService", { effect: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; - const config = yield* ConfigOptions; + const config = yield* ReleaseScriptsOptions; const execGitCommand = (args: readonly string[]) => executor.string(Command.make("git", ...args).pipe( @@ -97,7 +97,7 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr }); } - function readFileFromGit(filePath: string, ref: string = "HEAD") { + function readFile(filePath: string, ref: string = "HEAD") { return execGitCommand(["show", `${ref}:${filePath}`]); } @@ -173,6 +173,20 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr ); } + const assertWorkspaceReady = Effect.gen(function* () { + const withinRepo = yield* isWithinRepository; + if (!withinRepo) { + return yield* Effect.fail(new Error("Not within a Git repository.")); + } + + const clean = yield* isWorkingDirectoryClean; + if (!clean) { + return yield* Effect.fail(new Error("Working directory is not clean.")); + } + + return true; + }); + return { branches: { list: listBranches, @@ -191,16 +205,15 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr tags: { mostRecentForPackage: getMostRecentPackageTag, }, - readFileFromGit, - isWithinRepository, - isWorkingDirectoryClean, + workspace: { + readFile, + isWithinRepository, + isWorkingDirectoryClean, + assertWorkspaceReady, + }, } as const; }), dependencies: [ NodeCommandExecutor.layer, ], -}) { - static mockLayer(mockExecutor: CommandExecutor.CommandExecutor) { - return Layer.succeed(CommandExecutor.CommandExecutor, mockExecutor); - } -} +}) {} diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 596c13c..57e5ab1 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,6 +1,6 @@ import { Effect, Schema } from "effect"; import { GitHubError } from "../errors.js"; -import { ConfigOptions } from "../utils/options.js"; +import { ConfigOptions } from "../options.js"; import { GitService } from "./git.service.js"; // Schema definitions for GitHub API types diff --git a/src/services/package-updater.service.ts b/src/services/package-updater.service.ts index 9f90d16..f1c1c24 100644 --- a/src/services/package-updater.service.ts +++ b/src/services/package-updater.service.ts @@ -3,7 +3,7 @@ import type { WorkspacePackage } from "./workspace.service"; import fs from "node:fs/promises"; import path from "node:path"; import { Effect } from "effect"; -import { ConfigOptions } from "../utils/options"; +import { ConfigOptions } from "../options"; function nextRange(oldRange: string, newVersion: string): string { const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : ""; diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index 35c160d..c9daa4c 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { Command, CommandExecutor } from "@effect/platform"; import { Effect, Schema } from "effect"; import { WorkspaceError } from "../errors"; -import { ConfigOptions } from "../utils/options"; +import { ConfigOptions } from "../options"; export const DependencyObjectSchema = Schema.Record({ key: Schema.String, diff --git a/src/shared/types.ts b/src/shared/types.ts deleted file mode 100644 index f16de73..0000000 --- a/src/shared/types.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { WorkspacePackage } from "#core/workspace"; - -export type BumpKind = "none" | "patch" | "minor" | "major"; -export type GlobalCommitMode = false | "dependencies" | "all"; - -export interface CommitGroup { - /** - * Unique identifier for the group - */ - name: string; - - /** - * Display title (e.g., "Features", "Bug Fixes") - */ - title: string; - - /** - * Conventional commit types to include in this group - */ - types: string[]; -} - -export interface SharedOptions { - /** - * Repository identifier (e.g., "owner/repo") - */ - repo: `${string}/${string}`; - - /** - * Root directory of the workspace (defaults to process.cwd()) - */ - workspaceRoot?: string; - - /** - * Specific packages to prepare for release. - * - true: discover all packages - * - FindWorkspacePackagesOptions: discover with filters - * - string[]: specific package names - */ - packages?: true | FindWorkspacePackagesOptions | string[]; - - /** - * GitHub token for authentication - */ - githubToken: string; - - /** - * Interactive prompt configuration - */ - prompts?: { - /** - * Enable package selection prompt (defaults to true when not in CI) - */ - packages?: boolean; - - /** - * Enable version override prompt (defaults to true when not in CI) - */ - versions?: boolean; - }; - - /** - * Commit grouping configuration - * Used for changelog generation and commit display - * @default DEFAULT_COMMIT_GROUPS - */ - groups?: CommitGroup[]; -} - -export interface PackageJson { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; - - private?: boolean; - - [key: string]: unknown; -} - -export interface PackageUpdateOrder { - package: WorkspacePackage; - level: number; -} - -export interface FindWorkspacePackagesOptions { - /** - * Package names to exclude - */ - exclude?: string[]; - - /** - * Only include these packages (if specified, all others are excluded) - */ - include?: string[]; - - /** - * Whether to exclude private packages (default: false) - */ - excludePrivate?: boolean; -} - -export interface PackageRelease { - /** - * The package being updated - */ - package: WorkspacePackage; - - /** - * Current version - */ - currentVersion: string; - - /** - * New version to release - */ - newVersion: string; - - /** - * Type of version bump - */ - bumpType: BumpKind; - - /** - * Whether this package has direct changes (vs being updated due to dependency changes) - */ - hasDirectChanges: boolean; -} - -export interface AuthorInfo { - commits: string[]; - login?: string; - email: string; - name: string; -} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index fb85a38..d04cb84 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -18,7 +18,7 @@ export function loadOverrides(options: LoadOverridesOptions) { return Effect.gen(function* () { const git = yield* GitService; - return yield* git.readFileFromGit(options.overridesPath, "utf8").pipe( + return yield* git.workspace.readFile(options.overridesPath, "utf8").pipe( Effect.map((content) => ({ content, readError: null as unknown, @@ -213,7 +213,7 @@ export function mergeCommitsAffectingGloballyIntoPackage( result.set(pkg.name, globalCommits); } - return Effect.succeed(packages.map((pkg) => ({ + return yield* Effect.succeed(packages.map((pkg) => ({ ...pkg, globalCommits: result.get(pkg.name) || [], }))); diff --git a/src/utils/options.ts b/src/utils/options.ts deleted file mode 100644 index e8e621f..0000000 --- a/src/utils/options.ts +++ /dev/null @@ -1,196 +0,0 @@ -import process from "node:process"; -import { Context, Effect, Layer } from "effect"; - -export class ConfigOptions extends Context.Tag("@ucdjs/release-scripts/ConfigOptions")< - ConfigOptions, - NormalizedOptions ->() { - static layer(config: NormalizedOptions) { - return Layer.effect(ConfigOptions, Effect.succeed( - config, - )); - } -} - -export interface Options { - /** - * Enable dry run mode (no changes will be pushed or PRs created) - */ - dryRun?: boolean; - - /** - * Repository identifier (e.g., "owner/repo") - */ - repo: `${string}/${string}`; - - /** - * Root directory of the workspace (defaults to process.cwd()) - */ - workspaceRoot?: string; - - /** - * Specific packages to prepare for release. - * - true: discover all packages - * - FindWorkspacePackagesOptions: discover with filters - * - string[]: specific package names - */ - packages?: true | { - exclude?: string[]; - include?: string[]; - excludePrivate?: boolean; - } | string[]; - - /** - * GitHub token for authentication - */ - githubToken: string; - - /** - * Branch configuration for release process - */ - branch?: { - release?: string; - default?: string; - }; - - /** - * Enable safety checks (default: true) - */ - safeguards?: boolean; - - /** - * How to handle global commits (commits that affect multiple packages) - */ - globalCommitMode?: "dependencies" | "all" | "none"; - - /** - * Pull request configuration - */ - pullRequest?: { - title?: string; - body?: string; - }; - - /** - * Changelog configuration - */ - changelog?: { - enabled?: boolean; - template?: string; - }; - - /** - * Prompt configuration - */ - prompts?: { - packages?: boolean; - versions?: boolean; - }; - - /** - * Commit groups for changelog categorization - */ - groups?: Array<{ - name: string; - title: string; - types: string[]; - }>; -} - -type DeepRequired = Required<{ - [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired -}>; - -export type NormalizedOptions = DeepRequired> & { - owner: string; - repo: string; -}; - -const DEFAULT_COMMIT_GROUPS = [ - { name: "features", title: "Features", types: ["feat"] }, - { name: "fixes", title: "Bug Fixes", types: ["fix", "perf"] }, - { name: "refactor", title: "Refactoring", types: ["refactor"] }, - { name: "docs", title: "Documentation", types: ["docs"] }, -]; - -const DEFAULT_PR_BODY_TEMPLATE = `## Summary - -This PR contains the following changes: - -- Updated package versions -- Updated changelogs - -## Packages - -The following packages will be released: - -{{packages}}`; - -const DEFAULT_CHANGELOG_TEMPLATE = `# Changelog - -{{releases}}`; - -export function normalizeOptions(options: Options): NormalizedOptions { - const { - workspaceRoot = process.cwd(), - githubToken = "", - repo: fullRepo, - packages = true, - branch = {}, - safeguards = true, - globalCommitMode = "dependencies", - pullRequest = {}, - changelog = {}, - prompts = {}, - groups = DEFAULT_COMMIT_GROUPS, - } = options; - - if (!githubToken.trim()) { - throw new Error("GitHub token is required. Set GITHUB_TOKEN environment variable or pass it in options."); - } - - if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) { - throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world')."); - } - - const [owner, repo] = fullRepo.split("/"); - if (!owner || !repo) { - throw new Error(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`); - } - - const normalizedPackages = typeof packages === "object" && !Array.isArray(packages) - ? { - exclude: packages.exclude ?? [], - include: packages.include ?? [], - excludePrivate: packages.excludePrivate ?? false, - } - : packages; - - return { - dryRun: options.dryRun ?? false, - workspaceRoot, - githubToken, - owner, - repo, - packages: normalizedPackages, - branch: { - release: branch.release ?? "release/next", - default: branch.default ?? "main", - }, - safeguards, - globalCommitMode, - pullRequest: { - title: pullRequest.title ?? "chore: release new version", - body: pullRequest.body ?? DEFAULT_PR_BODY_TEMPLATE, - }, - changelog: { - enabled: changelog.enabled ?? true, - template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE, - }, - prompts: { - packages: prompts.packages ?? true, - versions: prompts.versions ?? true, - }, - groups, - }; -} diff --git a/src/verify.ts b/src/verify.ts new file mode 100644 index 0000000..a0554e0 --- /dev/null +++ b/src/verify.ts @@ -0,0 +1,65 @@ +import type { NormalizedReleaseScriptsOptions } from "./options"; +import type { WorkspacePackage } from "./services/workspace.service"; +import { DependencyGraphService } from "#services/dependency-graph"; +import { GitService } from "#services/git"; +import { GitHubService } from "#services/github"; +import { VersionCalculatorService } from "#services/version-calculator"; +import { WorkspaceService } from "#services/workspace"; +import { Console, Effect } from "effect"; +import { + loadOverrides, + mergeCommitsAffectingGloballyIntoPackage, + mergePackageCommitsIntoPackages, +} from "./utils/helpers"; + +export function constructVerifyProgram( + config: NormalizedReleaseScriptsOptions, +) { + return Effect.gen(function* () { + const git = yield* GitService; + const github = yield* GitHubService; + const dependencyGraph = yield* DependencyGraphService; + const versionCalculator = yield* VersionCalculatorService; + const workspace = yield* WorkspaceService; + + yield* git.workspace.assertWorkspaceReady; + + const releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release); + if (!releasePullRequest || !releasePullRequest.head) { + return yield* Effect.fail(new Error(`Release pull request for branch "${config.branch.release}" does not exist.`)); + } + + yield* Console.log(`✅ Release pull request #${releasePullRequest.number} exists.`); + + const currentBranch = yield* git.branches.get; + if (currentBranch !== config.branch.default) { + yield* git.branches.checkout(config.branch.default); + yield* Console.log(`✅ Checked out to default branch "${config.branch.default}".`); + } + + const overrides = yield* loadOverrides({ + sha: releasePullRequest.head.sha, + overridesPath: ".github/ucdjs-release.overrides.json", + }); + + yield* Console.log("Loaded overrides:", overrides); + + const packages = (yield* workspace.discoverWorkspacePackages.pipe( + Effect.flatMap(mergePackageCommitsIntoPackages), + Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), + )) as readonly WorkspacePackage[]; + + yield* Console.log("Discovered packages with commits and global commits:", packages); + + const releases = yield* versionCalculator.calculateBumps(packages as any, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages as any); + + yield* Console.log("Calculated releases:", releases); + yield* Console.log("Release order:", ordered); + + // STEP 4: Calculate the updates + // STEP 5: Read package.jsons from release branch (without checkout) + // STEP 6: Detect if Release PR is out of sync + // STEP 7: Set Commit Status + }); +} diff --git a/test/index.test.ts b/test/index.test.ts deleted file mode 100644 index 688ce03..0000000 --- a/test/index.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, it } from "@effect/vitest"; -import { Effect } from "effect"; - -// A simple divide function that returns an Effect, failing when dividing by zero -function divide(a: number, b: number) { - if (b === 0) return Effect.fail("Cannot divide by zero"); - return Effect.succeed(a / b); -} - -// Testing a successful division -it.effect("test success", () => - Effect.gen(function* () { - const result = yield* divide(4, 2); // Expect 4 divided by 2 to succeed - expect(result).toBe(2); // Assert that the result is 2 - })); diff --git a/test/options.test.ts b/test/options.test.ts new file mode 100644 index 0000000..9da9baa --- /dev/null +++ b/test/options.test.ts @@ -0,0 +1,103 @@ +import type { ReleaseScriptsOptionsInput } from "../src/options"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect } from "effect"; +import { + normalizeReleaseScriptsOptions, + ReleaseScriptsOptions, +} from "../src/options"; + +describe("normalizeReleaseScriptsOptions - global", () => { + it("normalizes minimal valid input", () => { + const input: ReleaseScriptsOptionsInput = { + repo: "octocat/hello-world", + githubToken: "token123", + }; + + const result = normalizeReleaseScriptsOptions(input); + expect(result.owner).toBe("octocat"); + expect(result.repo).toBe("hello-world"); + expect(result.githubToken).toBe("token123"); + expect(result.dryRun).toBe(false); + expect(result.workspaceRoot).toBeDefined(); + expect(result.packages).toBe(true); + expect(result.branch.release).toBe("release/next"); + expect(result.branch.default).toBe("main"); + expect(result.globalCommitMode).toBe("dependencies"); + }); + + it("throws if repo is missing or invalid", () => { + expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "invalid/" as any })).toThrow(); + expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "/invalid" as any })).toThrow(); + }); + + it("throws if githubToken is missing", () => { + expect(() => normalizeReleaseScriptsOptions({ repo: "octocat/hello-world", githubToken: "" })).toThrow(); + }); + + it("normalizes packages object", () => { + const input: ReleaseScriptsOptionsInput = { + repo: "octocat/hello-world", + githubToken: "token", + packages: { exclude: ["foo"], include: ["bar"], excludePrivate: true }, + }; + const result = normalizeReleaseScriptsOptions(input); + expect(result.packages).toEqual({ exclude: ["foo"], include: ["bar"], excludePrivate: true }); + }); +}); + +describe("normalizeReleaseScriptsOptions - prepare", () => { + it("normalizes default values", () => { + const result = normalizeReleaseScriptsOptions({ repo: "octocat/hello-world", githubToken: "token" }); + expect(result.pullRequest.title).toBe("chore: release new version"); + expect(result.pullRequest.body).toContain("This PR contains the following changes"); + expect(result.changelog.enabled).toBe(true); + expect(result.changelog.template).toContain("# Changelog"); + }); + + it("overrides pullRequest and changelog", () => { + const input: ReleaseScriptsOptionsInput = { + repo: "octocat/hello-world", + githubToken: "token", + pullRequest: { title: "custom title", body: "custom body" }, + changelog: { enabled: false, template: "custom template" }, + }; + const result = normalizeReleaseScriptsOptions(input); + expect(result.pullRequest.title).toBe("custom title"); + expect(result.pullRequest.body).toBe("custom body"); + expect(result.changelog.enabled).toBe(false); + expect(result.changelog.template).toBe("custom template"); + }); +}); + +describe("Context.Tag dependency injection", () => { + it.effect("injects and accesses ReleaseScriptsOptions (global)", () => + Effect.gen(function* (_) { + const value = yield* _(ReleaseScriptsOptions); + + expect(value.owner).toBe("octocat"); + expect(value.repo).toBe("hello-world"); + expect(value.githubToken).toBe("token"); + }).pipe( + Effect.provideService(ReleaseScriptsOptions, normalizeReleaseScriptsOptions({ repo: "octocat/hello-world", githubToken: "token" })), + )); + + it.effect("injects and accesses ReleaseScriptsOptions (prepare)", () => + Effect.gen(function* (_) { + const value = yield* _(ReleaseScriptsOptions); + + expect(value.pullRequest.title).toBe("t"); + expect(value.pullRequest.body).toBe("b"); + expect(value.changelog.enabled).toBe(false); + expect(value.changelog.template).toBe("tpl"); + }).pipe( + Effect.provideService( + ReleaseScriptsOptions, + normalizeReleaseScriptsOptions({ + repo: "octocat/hello-world", + githubToken: "token", + pullRequest: { title: "t", body: "b" }, + changelog: { enabled: false, template: "tpl" }, + }), + ), + )); +}); diff --git a/test/services/git.service.test.ts b/test/services/git.service.test.ts deleted file mode 100644 index 5ddc164..0000000 --- a/test/services/git.service.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { CommandExecutor } from "@effect/platform"; -import { it } from "@effect/vitest"; -import { Effect, Layer } from "effect"; -import { GitCommandError } from "../../src/errors"; -import { ConfigOptions } from "../../src/services/config.service"; -import { GitService } from "../../src/services/git.service"; - -const mockConfig = { - workspaceRoot: "/test/workspace", - githubToken: "test-token", - owner: "test-owner", - repo: "test-repo", - packages: { - exclude: [], - include: [], - excludePrivate: false, - }, - prompts: { packages: true, versions: true }, - branch: { default: "main", release: "release/next" }, - safeguards: true, - globalCommitMode: "dependencies" as const, - pullRequest: { title: "chore: release", body: "" }, - changelog: { enabled: true, template: "" }, - groups: [], -}; - -function createMockExecutor(impl: Partial): CommandExecutor.CommandExecutor { - return { - string: () => Effect.succeed(""), - lines: () => Effect.succeed([]), - exitCode: () => Effect.succeed(0), - start: () => Effect.succeed({ exitCode: 0, stdout: "", stderr: "" }), - stream: () => Effect.succeed(""), - ...impl, - } as any; -} - -it("should parse branches from git output", () => - Effect.gen(function* () { - const mockBranches = "main\nfeature/test\nrelease/next"; - const mockExecutor = createMockExecutor({ - string: () => Effect.succeed(mockBranches), - }); - - const testLayer = Layer.merge( - ConfigOptions.layer(mockConfig), - GitService.mockLayer(mockExecutor), - ); - - const result = yield* GitService.pipe( - Effect.flatMap((gitService) => gitService.listBranches), - Effect.provide(testLayer), - ); - - if (result[0] !== "main" || result[1] !== "feature/test" || result[2] !== "release/next") { - throw new Error(`Expected ["main", "feature/test", "release/next"], got ${JSON.stringify(result)}`); - } - })); - -it("should handle empty branch list", () => - Effect.gen(function* () { - const mockExecutor = createMockExecutor({ - string: () => Effect.succeed(""), - }); - - const testLayer = Layer.merge( - ConfigOptions.layer(mockConfig), - GitService.mockLayer(mockExecutor), - ); - - const result = yield* GitService.pipe( - Effect.flatMap((gitService) => gitService.listBranches), - Effect.provide(testLayer), - ); - - if (result.length !== 0) { - throw new Error(`Expected empty array, got ${JSON.stringify(result)}`); - } - })); - -it("should handle git command errors", () => - Effect.gen(function* () { - const mockExecutor = createMockExecutor({ - string: () => - Effect.fail( - new Error("fatal: not a git repository"), - ) as Effect.Effect, - }); - - const testLayer = Layer.merge( - ConfigOptions.layer(mockConfig), - GitService.mockLayer(mockExecutor), - ); - - const result = yield* GitService.pipe( - Effect.flatMap((gitService) => gitService.listBranches), - Effect.provide(testLayer), - Effect.flip, - ); - - if (!(result instanceof GitCommandError)) { - throw new Error(`Expected GitCommandError, got ${(result as any).constructor?.name || typeof result}`); - } - })); - -it("should trim whitespace from branch names", () => - Effect.gen(function* () { - const mockBranches = " main \n feature/test \n release/next "; - const mockExecutor = createMockExecutor({ - string: () => Effect.succeed(mockBranches), - }); - - const testLayer = Layer.merge( - ConfigOptions.layer(mockConfig), - GitService.mockLayer(mockExecutor), - ); - - const result = yield* GitService.pipe( - Effect.flatMap((gitService) => gitService.listBranches), - Effect.provide(testLayer), - ); - - if (result[0] !== "main" || result[1] !== "feature/test" || result[2] !== "release/next") { - throw new Error(`Expected trimmed ["main", "feature/test", "release/next"], got ${JSON.stringify(result)}`); - } - })); From 9b61005175ce5a8d21e7f653ebf9c25690edc220 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:35:32 +0100 Subject: [PATCH 07/35] test: add unit tests for `isGlobalCommit`, `isDependencyFile`, and `findCommitRange` functions This commit introduces a comprehensive suite of unit tests for the functions `isGlobalCommit`, `isDependencyFile`, and `findCommitRange` in the `helpers` module. The tests cover various scenarios to ensure the correctness of these functions, including edge cases and expected behaviors with different input conditions. --- src/utils/helpers.test.ts | 283 ++++++++++++++++++++++++++++++++++++++ src/utils/helpers.ts | 10 +- 2 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 src/utils/helpers.test.ts diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts new file mode 100644 index 0000000..7ef3fbf --- /dev/null +++ b/src/utils/helpers.test.ts @@ -0,0 +1,283 @@ +import type { GitCommit } from "commit-parser"; +import type { WorkspacePackageWithCommits } from "./helpers"; +import { describe, expect, it } from "vitest"; +import { + findCommitRange, + isDependencyFile, + isGlobalCommit, +} from "./helpers"; + +function makeCommit(shortHash: string, date: string): GitCommit { + return { + isConventional: true, + isBreaking: false, + type: "feat", + scope: undefined, + description: "desc", + references: [], + authors: [{ name: "a", email: "a@example.com" }], + hash: shortHash.padEnd(40, "0"), + shortHash, + body: "", + message: `feat: ${shortHash}`, + date, + }; +} + +function makePackage( + name: string, + path: string, + commits: readonly GitCommit[], +): WorkspacePackageWithCommits { + return { + name, + path, + version: "1.0.0", + packageJson: { + name, + version: "1.0.0", + private: false, + }, + workspaceDependencies: [], + workspaceDevDependencies: [], + commits, + globalCommits: [], + }; +} + +describe("isGlobalCommit", () => { + it("returns true when file is at repository root", () => { + expect(isGlobalCommit(["README.md"], new Set(["packages/a"]))).toBe(true); + }); + + it("returns true when file is in a non-package directory", () => { + expect(isGlobalCommit(["scripts/build.js"], new Set(["packages/a", "packages/b"]))).toBe(true); + }); + + it("returns true when at least one file is outside all package paths", () => { + expect(isGlobalCommit(["packages/a/index.ts", "README.md"], new Set(["packages/a"]))).toBe(true); + }); + + it("returns false when all files are inside a single package", () => { + expect(isGlobalCommit(["packages/a/index.ts", "packages/a/lib/util.ts"], new Set(["packages/a"]))).toBe(false); + }); + + it("returns false when all files are inside different packages", () => { + expect(isGlobalCommit(["packages/a/index.ts", "packages/b/index.ts"], new Set(["packages/a", "packages/b"]))).toBe(false); + }); + + it("handles files with ./ prefix", () => { + expect(isGlobalCommit(["./README.md"], new Set(["packages/a"]))).toBe(true); + expect(isGlobalCommit(["./packages/a/index.ts"], new Set(["packages/a"]))).toBe(false); + }); + + it("handles empty file list", () => { + expect(isGlobalCommit([], new Set(["packages/a"]))).toBe(false); + }); + + it("handles empty package paths", () => { + expect(isGlobalCommit(["any/file.ts"], new Set())).toBe(true); + }); + + it("does not match partial package path prefixes", () => { + // "packages/ab" should not match "packages/a" + expect(isGlobalCommit(["packages/ab/index.ts"], new Set(["packages/a"]))).toBe(true); + }); + + it("matches exact package path", () => { + expect(isGlobalCommit(["packages/a"], new Set(["packages/a"]))).toBe(false); + }); +}); + +describe("isDependencyFile", () => { + describe("root-level files", () => { + it("matches package.json", () => { + expect(isDependencyFile("package.json")).toBe(true); + }); + + it("matches pnpm-lock.yaml", () => { + expect(isDependencyFile("pnpm-lock.yaml")).toBe(true); + }); + + it("matches yarn.lock", () => { + expect(isDependencyFile("yarn.lock")).toBe(true); + }); + + it("matches package-lock.json", () => { + expect(isDependencyFile("package-lock.json")).toBe(true); + }); + + it("matches pnpm-workspace.yaml", () => { + expect(isDependencyFile("pnpm-workspace.yaml")).toBe(true); + }); + }); + + describe("nested files", () => { + it("matches package.json in a nested directory", () => { + expect(isDependencyFile("packages/a/package.json")).toBe(true); + }); + + it("matches lock files in nested directories", () => { + expect(isDependencyFile("apps/web/pnpm-lock.yaml")).toBe(true); + }); + + it("matches deeply nested dependency files", () => { + expect(isDependencyFile("a/b/c/d/package.json")).toBe(true); + }); + }); + + describe("non-dependency files", () => { + it("rejects regular source files", () => { + expect(isDependencyFile("src/index.ts")).toBe(false); + }); + + it("rejects files with similar names", () => { + expect(isDependencyFile("package.json.bak")).toBe(false); + expect(isDependencyFile("my-package.json")).toBe(false); + }); + + it("rejects README and other docs", () => { + expect(isDependencyFile("README.md")).toBe(false); + expect(isDependencyFile("CHANGELOG.md")).toBe(false); + }); + + it("rejects config files that are not dependency-related", () => { + expect(isDependencyFile("tsconfig.json")).toBe(false); + expect(isDependencyFile("eslint.config.js")).toBe(false); + }); + }); + + describe("edge cases", () => { + it("handles ./ prefix", () => { + expect(isDependencyFile("./package.json")).toBe(true); + expect(isDependencyFile("./packages/a/package.json")).toBe(true); + }); + + it("rejects empty string", () => { + expect(isDependencyFile("")).toBe(false); + }); + }); +}); + +describe("findCommitRange", () => { + it("returns oldest and newest commit hashes across multiple packages", () => { + const commit1 = makeCommit("aaa", "2020-01-01T00:00:00Z"); + const commit2 = makeCommit("bbb", "2020-01-15T00:00:00Z"); + const commit3 = makeCommit("ccc", "2020-02-01T00:00:00Z"); + const commit4 = makeCommit("ddd", "2020-03-01T00:00:00Z"); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("pkg-a", "packages/a", [commit1, commit2]), + makePackage("pkg-b", "packages/b", [commit3, commit4]), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBe("aaa"); + expect(newest).toBe("ddd"); + }); + + it("handles single package with multiple commits", () => { + const commit1 = makeCommit("first", "2020-01-01T00:00:00Z"); + const commit2 = makeCommit("second", "2020-06-01T00:00:00Z"); + const commit3 = makeCommit("third", "2020-12-01T00:00:00Z"); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("solo", "packages/solo", [commit1, commit2, commit3]), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBe("first"); + expect(newest).toBe("third"); + }); + + it("handles single package with single commit", () => { + const commit = makeCommit("only", "2020-06-15T00:00:00Z"); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("solo", "packages/solo", [commit]), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBe("only"); + expect(newest).toBe("only"); + }); + + it("returns [null, null] when all packages have no commits", () => { + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("empty-a", "packages/a", []), + makePackage("empty-b", "packages/b", []), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBeNull(); + expect(newest).toBeNull(); + }); + + it("returns [null, null] for empty package array", () => { + const [oldest, newest] = findCommitRange([]); + expect(oldest).toBeNull(); + expect(newest).toBeNull(); + }); + + it("ignores packages with no commits when finding range", () => { + const commit1 = makeCommit("aaa", "2020-01-01T00:00:00Z"); + const commit2 = makeCommit("bbb", "2020-12-01T00:00:00Z"); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("empty", "packages/empty", []), + makePackage("has-commits", "packages/has", [commit1, commit2]), + makePackage("also-empty", "packages/also", []), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBe("aaa"); + expect(newest).toBe("bbb"); + }); + + it("correctly identifies oldest when packages are out of order", () => { + const olderCommit = makeCommit("older", "2019-01-01T00:00:00Z"); + const newerCommit = makeCommit("newer", "2021-01-01T00:00:00Z"); + const middleCommit = makeCommit("middle", "2020-01-01T00:00:00Z"); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("pkg-b", "packages/b", [newerCommit]), + makePackage("pkg-c", "packages/c", [middleCommit]), + makePackage("pkg-a", "packages/a", [olderCommit]), + ]; + + const [oldest, newest] = findCommitRange(packages); + expect(oldest).toBe("older"); + expect(newest).toBe("newer"); + }); + + it("handles commits with same timestamp", () => { + const sameTime = "2020-06-15T12:00:00Z"; + const commit1 = makeCommit("first", sameTime); + const commit2 = makeCommit("second", sameTime); + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("pkg-a", "packages/a", [commit1]), + makePackage("pkg-b", "packages/b", [commit2]), + ]; + + const [oldest, newest] = findCommitRange(packages); + // Both have same timestamp, so result depends on iteration order + expect(oldest).toBeDefined(); + expect(newest).toBeDefined(); + }); + + it("handles ISO date strings with timezone offsets", () => { + const commit1 = makeCommit("utc", "2020-01-01T00:00:00Z"); + const commit2 = makeCommit("offset", "2020-01-01T05:00:00+05:00"); // Same instant as commit1 + + const packages: readonly WorkspacePackageWithCommits[] = [ + makePackage("pkg-a", "packages/a", [commit1]), + makePackage("pkg-b", "packages/b", [commit2]), + ]; + + const [oldest, newest] = findCommitRange(packages); + // Both represent the same instant + expect(oldest).toBeDefined(); + expect(newest).toBeDefined(); + }); +}); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index d04cb84..e6fe5c7 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -227,7 +227,7 @@ export function mergeCommitsAffectingGloballyIntoPackage( * @param packagePaths - Set of package directory paths * @returns true if at least one file is outside all package directories */ -function isGlobalCommit(files: readonly string[], packagePaths: Set): boolean { +export function isGlobalCommit(files: readonly string[], packagePaths: Set): boolean { return files.some((file) => { const normalized = file.startsWith("./") ? file.slice(2) : file; @@ -259,7 +259,7 @@ const DEPENDENCY_FILES = new Set([ * @param file - File path to check * @returns true if the file is a dependency file (package.json, lock files, etc.) */ -function isDependencyFile(file: string): boolean { +export function isDependencyFile(file: string): boolean { const normalized = file.startsWith("./") ? file.slice(2) : file; // Check if it's a root-level dependency file @@ -277,9 +277,9 @@ function isDependencyFile(file: string): boolean { * @param packages - Array of packages with their commits * @returns Tuple of [oldestCommitSha, newestCommitSha], or [null, null] if no commits found */ -function findCommitRange(packages: readonly WorkspacePackageWithCommits[]): [oldestCommit: string | null, newestCommit: string | null] { - let oldestCommit: CommitParser.GitCommit | null = null; - let newestCommit: CommitParser.GitCommit | null = null; +export function findCommitRange(packages: readonly WorkspacePackageWithCommits[]): [oldestCommit: string | null, newestCommit: string | null] { + let oldestCommit: WorkspacePackageWithCommits["commits"][number] | null = null; + let newestCommit: WorkspacePackageWithCommits["commits"][number] | null = null; for (const pkg of packages) { if (pkg.commits.length === 0) { From 95c72c76c4353c4e6e86e2274ead2aec72fba94e Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:35:52 +0100 Subject: [PATCH 08/35] test: add unit tests for `isGlobalCommit`, `isDependencyFile`, and `findCommitRange` functions --- {src/utils => test}/helpers.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename {src/utils => test}/helpers.test.ts (98%) diff --git a/src/utils/helpers.test.ts b/test/helpers.test.ts similarity index 98% rename from src/utils/helpers.test.ts rename to test/helpers.test.ts index 7ef3fbf..b1e14c3 100644 --- a/src/utils/helpers.test.ts +++ b/test/helpers.test.ts @@ -1,11 +1,11 @@ import type { GitCommit } from "commit-parser"; -import type { WorkspacePackageWithCommits } from "./helpers"; +import type { WorkspacePackageWithCommits } from "../src/utils/helpers"; import { describe, expect, it } from "vitest"; import { findCommitRange, isDependencyFile, isGlobalCommit, -} from "./helpers"; +} from "../src/utils/helpers"; function makeCommit(shortHash: string, date: string): GitCommit { return { From d0119171a434ef658c073c92836f75e08f77967d Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:37:43 +0100 Subject: [PATCH 09/35] chore: remove unused `@effect/cli` and related dependencies from `pnpm-lock.yaml` This change cleans up the `pnpm-lock.yaml` file by removing the `@effect/cli` package and its associated dependencies, which are no longer needed. This helps to streamline the dependency graph and reduce potential conflicts in future updates. --- pnpm-lock.yaml | 64 -------------------------------------------------- 1 file changed, 64 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f65340b..11ec435 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@effect/cli': - specifier: 0.72.1 - version: 0.72.1(@effect/platform@0.93.3(effect@3.19.6))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) '@effect/platform': specifier: 0.93.3 version: 0.93.3(effect@3.19.6) @@ -111,14 +108,6 @@ packages: '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} - '@effect/cli@0.72.1': - resolution: {integrity: sha512-HGDMGD23TxFW9tCSX6g+M2u0robikMA0mP0SqeJMj7FWXTdcQ+cQsJE99bxi9iu+5YID7MIrVJMs8TUwXUV2sg==} - peerDependencies: - '@effect/platform': ^0.93.0 - '@effect/printer': ^0.47.0 - '@effect/printer-ansi': ^0.47.0 - effect: ^3.19.3 - '@effect/cluster@0.53.5': resolution: {integrity: sha512-eXPHIizdG5sOqxmkpWyEM6YoqMRakguxRped3lYcEopKj4N1K4nE9JANbKHXqzxPjnAvit+r7zDSuwHUw9nfAw==} peerDependencies: @@ -168,18 +157,6 @@ packages: peerDependencies: effect: ^3.19.4 - '@effect/printer-ansi@0.47.0': - resolution: {integrity: sha512-tDEQ9XJpXDNYoWMQJHFRMxKGmEOu6z32x3Kb8YLOV5nkauEKnKmWNs7NBp8iio/pqoJbaSwqDwUg9jXVquxfWQ==} - peerDependencies: - '@effect/typeclass': ^0.38.0 - effect: ^3.19.0 - - '@effect/printer@0.47.0': - resolution: {integrity: sha512-VgR8e+YWWhMEAh9qFOjwiZ3OXluAbcVLIOtvp2S5di1nSrPOZxj78g8LE77JSvyfp5y5bS2gmFW+G7xD5uU+2Q==} - peerDependencies: - '@effect/typeclass': ^0.38.0 - effect: ^3.19.0 - '@effect/rpc@0.72.2': resolution: {integrity: sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ==} peerDependencies: @@ -193,11 +170,6 @@ packages: '@effect/platform': ^0.93.0 effect: ^3.19.0 - '@effect/typeclass@0.38.0': - resolution: {integrity: sha512-lMUcJTRtG8KXhXoczapZDxbLK5os7M6rn0zkvOgncJW++A0UyelZfMVMKdT5R+fgpZcsAU/1diaqw3uqLJwGxA==} - peerDependencies: - effect: ^3.19.0 - '@effect/vitest@0.27.0': resolution: {integrity: sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ==} peerDependencies: @@ -1635,10 +1607,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -2197,9 +2165,6 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2467,16 +2432,6 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@effect/cli@0.72.1(@effect/platform@0.93.3(effect@3.19.6))(@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': - dependencies: - '@effect/platform': 0.93.3(effect@3.19.6) - '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) - '@effect/printer-ansi': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) - effect: 3.19.6 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.1 - '@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': dependencies: '@effect/platform': 0.93.3(effect@3.19.6) @@ -2530,17 +2485,6 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 - '@effect/printer-ansi@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6)': - dependencies: - '@effect/printer': 0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6) - '@effect/typeclass': 0.38.0(effect@3.19.6) - effect: 3.19.6 - - '@effect/printer@0.47.0(@effect/typeclass@0.38.0(effect@3.19.6))(effect@3.19.6)': - dependencies: - '@effect/typeclass': 0.38.0(effect@3.19.6) - effect: 3.19.6 - '@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': dependencies: '@effect/platform': 0.93.3(effect@3.19.6) @@ -2554,10 +2498,6 @@ snapshots: effect: 3.19.6 uuid: 11.1.0 - '@effect/typeclass@0.38.0(effect@3.19.6)': - dependencies: - effect: 3.19.6 - '@effect/vitest@0.27.0(effect@3.19.6)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': dependencies: effect: 3.19.6 @@ -3934,8 +3874,6 @@ snapshots: indent-string@5.0.0: {} - ini@4.1.3: {} - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -4657,8 +4595,6 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 - toml@3.0.0: {} - tree-kill@1.2.2: {} ts-api-utils@2.1.0(typescript@5.9.3): From c0b5318dc94e1d1b17feee5fbcd66ee4410ced3c Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:38:24 +0100 Subject: [PATCH 10/35] chore: remove empty code change entries This commit cleans up the code change entries by removing any empty `` tags. This helps maintain a tidy and organized structure in the change logs, ensuring that only relevant changes are documented. --- package.json | 18 +- pnpm-lock.yaml | 842 +++++++++++++++++++++---------------------------- 2 files changed, 369 insertions(+), 491 deletions(-) diff --git a/package.json b/package.json index 28e2ce6..ff7e326 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-beta.23", "description": "@ucdjs release scripts", "type": "module", - "packageManager": "pnpm@10.19.0", + "packageManager": "pnpm@10.24.0", "license": "MIT", "repository": { "type": "git", @@ -31,11 +31,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/platform": "0.93.3", - "@effect/platform-node": "0.101.1", + "@effect/platform": "0.93.6", + "@effect/platform-node": "0.103.0", "@luxass/utils": "2.7.2", "commit-parser": "1.3.0", - "effect": "3.19.6", + "effect": "3.19.9", "farver": "1.0.0-beta.1", "mri": "1.2.0", "prompts": "2.4.2", @@ -43,17 +43,17 @@ "tinyexec": "1.0.2" }, "devDependencies": { - "@effect/language-service": "^0.59.0", + "@effect/language-service": "^0.60.0", "@effect/vitest": "0.27.0", - "@luxass/eslint-config": "6.0.1", + "@luxass/eslint-config": "6.0.3", "@types/node": "22.18.12", "@types/prompts": "2.4.9", "@types/semver": "7.7.1", "eslint": "9.39.1", - "eta": "4.0.1", - "tsdown": "0.16.0", + "eta": "4.4.1", + "tsdown": "0.17.0", "typescript": "5.9.3", - "vitest": "4.0.4", + "vitest": "4.0.15", "vitest-testdirs": "4.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11ec435..a3c05bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@effect/platform': - specifier: 0.93.3 - version: 0.93.3(effect@3.19.6) + specifier: 0.93.6 + version: 0.93.6(effect@3.19.9) '@effect/platform-node': - specifier: 0.101.1 - version: 0.101.1(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) + specifier: 0.103.0 + version: 0.103.0(@effect/cluster@0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9) '@luxass/utils': specifier: 2.7.2 version: 2.7.2 @@ -21,8 +21,8 @@ importers: specifier: 1.3.0 version: 1.3.0 effect: - specifier: 3.19.6 - version: 3.19.6 + specifier: 3.19.9 + version: 3.19.9 farver: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1 @@ -40,14 +40,14 @@ importers: version: 1.0.2 devDependencies: '@effect/language-service': - specifier: ^0.59.0 - version: 0.59.0 + specifier: ^0.60.0 + version: 0.60.0 '@effect/vitest': specifier: 0.27.0 - version: 0.27.0(effect@3.19.6)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) + version: 0.27.0(effect@3.19.9)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) '@luxass/eslint-config': - specifier: 6.0.1 - version: 6.0.1(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) + specifier: 6.0.3 + version: 6.0.3(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) '@types/node': specifier: 22.18.12 version: 22.18.12 @@ -61,20 +61,20 @@ importers: specifier: 9.39.1 version: 9.39.1(jiti@2.6.1) eta: - specifier: 4.0.1 - version: 4.0.1 + specifier: 4.4.1 + version: 4.4.1 tsdown: - specifier: 0.16.0 - version: 0.16.0(typescript@5.9.3) + specifier: 0.17.0 + version: 0.17.0(synckit@0.11.11)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 vitest: - specifier: 4.0.4 - version: 4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) + specifier: 4.0.15 + version: 4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) vitest-testdirs: specifier: 4.3.0 - version: 4.3.0(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.3.0(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) packages: @@ -130,32 +130,32 @@ packages: lmdb: optional: true - '@effect/language-service@0.59.0': - resolution: {integrity: sha512-jFl5oqxPrWNoKL6u8m8i1xbJ9hT9YeuPY25tMiuB7YU1GjIHJHXwBbVspMsQ8JdDrP+vdXJj+yX/Tk6J2bbvuw==} + '@effect/language-service@0.60.0': + resolution: {integrity: sha512-elJDWHG5Naq3OkilPt9ZRn56JfSA3MhXUIlDx9RWJeScHm96kZ+HkZ3eFBxqROzXwD6Q2DTtFctFwOM0+QLZEA==} hasBin: true - '@effect/platform-node-shared@0.54.0': - resolution: {integrity: sha512-prTgG3CXqmrxB4Rg6utfwCTqjlGwjAEvK7R4g3HzVdFpfFRum+FQBpGHUcjyz7EejkDtBY2MWJC3Wr1QKDPjPw==} + '@effect/platform-node-shared@0.56.0': + resolution: {integrity: sha512-0RawLcUCLHVGs4ch1nY26P4xM+U6R03ZR02MgNHMsL0slh8YYlal5PnwD/852rJ59O9prQX3Kq8zs+cGVoLAJw==} peerDependencies: - '@effect/cluster': ^0.53.0 - '@effect/platform': ^0.93.3 + '@effect/cluster': ^0.55.0 + '@effect/platform': ^0.93.6 '@effect/rpc': ^0.72.2 - '@effect/sql': ^0.48.0 - effect: ^3.19.5 + '@effect/sql': ^0.48.6 + effect: ^3.19.8 - '@effect/platform-node@0.101.1': - resolution: {integrity: sha512-uShujtpWU0VbdhRKhoo6tXzTG1xT0bnj8u5Q1BHpanwKPmzOhf4n0XLlMl5PaihH5Cp7xHuQlwgZlqHzhqSHzw==} + '@effect/platform-node@0.103.0': + resolution: {integrity: sha512-N2JmOvHInHAC+JFdt+ME8/Pn9vdgBwYTTcqlSXkT+mBzq6fAKdwHkXHoFUMbk8bWtJGx70oezLLEetatjsveaA==} peerDependencies: - '@effect/cluster': ^0.53.4 - '@effect/platform': ^0.93.3 + '@effect/cluster': ^0.55.0 + '@effect/platform': ^0.93.6 '@effect/rpc': ^0.72.2 - '@effect/sql': ^0.48.0 - effect: ^3.19.6 + '@effect/sql': ^0.48.6 + effect: ^3.19.8 - '@effect/platform@0.93.3': - resolution: {integrity: sha512-s88zctkeXba24Mjy7MEFMuam1p5sXmsG7uQjPIDE6EiC+2IFUQd8976TtangiU0e8qu0SALpjIH1P1QyC7/1og==} + '@effect/platform@0.93.6': + resolution: {integrity: sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w==} peerDependencies: - effect: ^3.19.4 + effect: ^3.19.8 '@effect/rpc@0.72.2': resolution: {integrity: sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ==} @@ -184,11 +184,11 @@ packages: '@effect/rpc': ^0.72.2 effect: ^3.19.5 - '@emnapi/core@1.7.0': - resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.7.0': - resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -201,6 +201,10 @@ packages: resolution: {integrity: sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==} engines: {node: '>=20.11.0'} + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -390,14 +394,6 @@ packages: resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.2': - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.16.0': - resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -410,18 +406,14 @@ packages: resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/markdown@7.4.0': - resolution: {integrity: sha512-VQykmMjBb4tQoJOXVWXa+oQbQeCZlE7W3rAsOpmtpKLvJd75saZZ04PVVs7+zgMDJGghd4/gyFV6YlvdJFaeNQ==} + '@eslint/markdown@7.5.1': + resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.5': - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -455,8 +447,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@luxass/eslint-config@6.0.1': - resolution: {integrity: sha512-55XFOV1Yg5fUjNk17wmWXwZpQCeWmvZ1IkSoptjS6oYz3CziS2dvARNWl9+TvNvcJl4byxyS/zMHVTXDJ6YBaA==} + '@luxass/eslint-config@6.0.3': + resolution: {integrity: sha512-6VJOowLawKu4fWWJH8Y9trZxsmEFlCIKTzzeE/m9Xwpkxz8/FhPswWcmNVDiyHDek7nVbPfSf7jw8g8Sb4/oXA==} engines: {node: '>=22'} peerDependencies: '@eslint-react/eslint-plugin': ^2.0.1 @@ -523,8 +515,8 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -538,8 +530,12 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.96.0': - resolution: {integrity: sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==} + '@oxc-project/runtime@0.101.0': + resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.101.0': + resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -627,94 +623,88 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@quansync/fs@0.1.5': - resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@rolldown/binding-android-arm64@1.0.0-beta.46': - resolution: {integrity: sha512-1nfXUqZ227uKuLw9S12OQZU5z+h+cUOXLW5orntWVxHWvt20pt1PGUcVoIU8ssngKABu0vzHY268kAxuYX24BQ==} + '@rolldown/binding-android-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.46': - resolution: {integrity: sha512-w4IyumCQkpA3ezZ37COG3mMusFYxjEE8zqCfXZU/qb5k1JMD2kVl0fgJafIbGli27tgelYMweXkJGnlrxSGT9Q==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.46': - resolution: {integrity: sha512-9QqaRHPbdAnv306+7nzltq4CktJ49Z4W9ybHLWYxSeDSoOGL4l1QmxjDWoRHrqYEkNr+DWHqqoD4NNHgOk7lKw==} + '@rolldown/binding-darwin-x64@1.0.0-beta.53': + resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.46': - resolution: {integrity: sha512-Cuk5opdEMb+Evi7QcGArc4hWVoHSGz/qyUUWLTpFJWjylb8wH1u4f+HZE6gVGACuf4w/5P/VhAIamHyweAbBVQ==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.46': - resolution: {integrity: sha512-BPWDxEnxb4JNMXrSmPuc5ywI6cHOELofmT0e/WGkbL1MwKYRVvqTf+gMcGLF6zAV+OF5hLYMAEk8XKfao6xmDQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.46': - resolution: {integrity: sha512-CDQSVlryuRC955EwgbBK1h/6xQyttSxQG8+6/PeOfvUlfKGPMbBdcsOEHzGve5ED1Y7Ovh2UFjY/eT106aQqig==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.46': - resolution: {integrity: sha512-6IZHycZetmVaC9zwcl1aA9fPYPuxLa5apALjJRoJu/2BZdER3zBWxDnCzlEh4SUlo++cwdfV9ZQRK9JS8cLNuA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.46': - resolution: {integrity: sha512-R/kI8fMnsxXvWzcMv5A408hfvrwtAwD/HdQKIE1HKWmfxdSHB11Y3PVwlnt7RVo7I++6mWCIxxj5o3gut4ibEw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.46': - resolution: {integrity: sha512-vGUXKuHGUlG2XBwvN4A8KIegeaVVxN2ZxdGG9thycwRkzUvZ9ccKvqUVZM8cVRyNRWgVgsGCS18qLUefVplwKw==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.46': - resolution: {integrity: sha512-6SpDGH+0Dud3/RFDoC6fva6+Cm/0COnMRKR8kI4ssHWlCXPymlM59kYFCIBLZZqwURpNVVMPln4rWjxXuwD23w==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.46': - resolution: {integrity: sha512-peWDGp8YUAbTw5RJzr9AuPlTuf2adr+TBNIGF6ysMbobBKuQL41wYfGQlcerXJfLmjnQLf6DU2zTPBTfrS2Y8A==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.46': - resolution: {integrity: sha512-Ydbwg1JCnVbTAuDyKtu3dOuBLgZ6iZsy8p1jMPX/r7LMPnpXnS15GNcmMwa11nyl/M2VjGE1i/MORUTMt8mnRQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.46': - resolution: {integrity: sha512-XcPZG2uDxEn6G3takXQvi7xWgDiJqdC0N6mubL/giKD4I65zgQtbadwlIR8oDB/erOahZr5IX8cRBVcK3xcvpg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.46': - resolution: {integrity: sha512-VPC+F9S6nllv02aGG+gxHRgpOaOlYBPn94kDe9DCFSLOztf4uYIAkN+tLDlg5OcsOC8XNR5rP49zOfI0PfnHYw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.46': - resolution: {integrity: sha512-xMNwJo/pHkEP/mhNVnW+zUiJDle6/hxrwO0mfSJuEVRbBfgrJFuUSRoZx/nYUw5pCjrysl9OkNXCkAdih8GCnA==} + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} '@rollup/rollup-android-arm-eabi@4.52.5': resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} @@ -875,87 +865,54 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.46.2': - resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} + '@typescript-eslint/eslint-plugin@8.46.3': + resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.2 + '@typescript-eslint/parser': ^8.46.3 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.2': - resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==} + '@typescript-eslint/parser@8.46.3': + resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.2': - resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.3': resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.2': - resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.46.3': resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.2': - resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.46.3': resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.2': - resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==} + '@typescript-eslint/type-utils@8.46.3': + resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.2': - resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.46.3': resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.2': - resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.46.3': resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.2': - resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.3': resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -963,20 +920,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.2': - resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.46.3': resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/eslint-plugin@1.3.23': - resolution: {integrity: sha512-kp1vjoJTdVf8jWdzr/JpHIPfh3HMR6JBr2p7XuH4YNx0UXmV4XWdgzvCpAmH8yb39Gry31LULiuBcuhyc/OqkQ==} + '@vitest/eslint-plugin@1.4.1': + resolution: {integrity: sha512-eBMCLeUhKvQxH7nPihmLUJUWXxqKovVFEmxbGKqkY/aN6hTAXGiRid8traRUOvgr82NJFJL3KPpE19fElOR7bg==} engines: {node: '>=18'} peerDependencies: - eslint: '>= 8.57.0' - typescript: '>= 5.0.0' + eslint: '>=8.57.0' + typescript: '>=5.0.0' vitest: '*' peerDependenciesMeta: typescript: @@ -984,11 +937,11 @@ packages: vitest: optional: true - '@vitest/expect@4.0.4': - resolution: {integrity: sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==} + '@vitest/expect@4.0.15': + resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==} - '@vitest/mocker@4.0.4': - resolution: {integrity: sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==} + '@vitest/mocker@4.0.15': + resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0-0 @@ -998,20 +951,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.0.4': - resolution: {integrity: sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==} + '@vitest/pretty-format@4.0.15': + resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==} - '@vitest/runner@4.0.4': - resolution: {integrity: sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==} + '@vitest/runner@4.0.15': + resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==} - '@vitest/snapshot@4.0.4': - resolution: {integrity: sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==} + '@vitest/snapshot@4.0.15': + resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==} - '@vitest/spy@4.0.4': - resolution: {integrity: sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==} + '@vitest/spy@4.0.15': + resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==} - '@vitest/utils@4.0.4': - resolution: {integrity: sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==} + '@vitest/utils@4.0.15': + resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} @@ -1060,8 +1013,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-kit@2.1.3: - resolution: {integrity: sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} balanced-match@1.0.2: @@ -1071,8 +1024,8 @@ packages: resolution: {integrity: sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==} hasBin: true - birpc@2.7.0: - resolution: {integrity: sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==} + birpc@3.0.0: + resolution: {integrity: sha512-by+04pHuxpCEQcucAXqzopqfhyI8TLK5Qg5MST0cB6MP+JhHna9ollrtK9moVh27aq6Q6MEJgebD0cVm//yBkg==} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1110,8 +1063,8 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@6.2.0: - resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} chalk@4.1.2: @@ -1124,10 +1077,6 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -1186,9 +1135,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1209,21 +1155,17 @@ packages: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - diff@8.0.2: - resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} - engines: {node: '>=0.3.1'} - - dts-resolver@2.1.2: - resolution: {integrity: sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==} - engines: {node: '>=20.18.0'} + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} peerDependencies: oxc-resolver: '>=11.0.0' peerDependenciesMeta: oxc-resolver: optional: true - effect@3.19.6: - resolution: {integrity: sha512-Eh1E/CI+xCAcMSDC5DtyE29yWJINC0zwBbwHappQPorjKyS69rCA8qzpsHpfhKnPDYgxdg8zkknii8mZ+6YMQA==} + effect@3.19.9: + resolution: {integrity: sha512-taMXnfG/p+j7AmMOHHQaCHvjqwu9QBO3cxuZqL2dMG/yWcEMw0ZHruHe9B49OxtfKH/vKKDDKRhZ+1GJ2p5R5w==} electron-to-chromium@1.5.245: resolution: {integrity: sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==} @@ -1326,8 +1268,8 @@ packages: typescript: optional: true - eslint-plugin-jsdoc@61.1.5: - resolution: {integrity: sha512-UZ+7M6WVFBVRTxHZURxYP7M++M+ZEjxPGB/CScdrKAhzpf/LWS1HaNRHMOkISkOTTggMhwRwgKmVlTLQryXV2Q==} + eslint-plugin-jsdoc@61.1.12: + resolution: {integrity: sha512-CGJTnltz7ovwOW33xYhvA4fMuriPZpR5OnJf09SV28iU2IUpJwMd6P7zvUK8Sl56u5YzO+1F9m46wpSs2dufEw==} engines: {node: '>=20.11.0'} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -1367,11 +1309,11 @@ packages: peerDependencies: eslint: '>=6.0.0' - eslint-plugin-unicorn@61.0.2: - resolution: {integrity: sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==} + eslint-plugin-unicorn@62.0.0: + resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: - eslint: '>=9.29.0' + eslint: '>=9.38.0' eslint-plugin-unused-imports@4.3.0: resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==} @@ -1460,8 +1402,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eta@4.0.1: - resolution: {integrity: sha512-0h0oBEsF6qAJU7eu9ztvJoTo8D2PAq/4FvXVIQA1fek3WOTe6KPsVJycekG1+g1N6mfpblkheoGwaUhMtnlH4A==} + eta@4.4.1: + resolution: {integrity: sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==} engines: {node: '>=20'} expect-type@1.2.2: @@ -1564,8 +1506,8 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} globrex@0.1.2: @@ -1599,6 +1541,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-without-cache@0.2.2: + resolution: {integrity: sha512-4TTuRrZ0jBULXzac3EoX9ZviOs8Wn9iAbNhJEyLhTpAGF9eNmYSruaMMN/Tec/yqaO7H6yS2kALfQDJ5FxfatA==} + engines: {node: '>=20.19.0'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1650,11 +1596,6 @@ packages: resolution: {integrity: sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==} engines: {node: '>=20.0.0'} - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1901,6 +1842,9 @@ packages: object-deep-merge@2.0.0: resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1995,13 +1939,12 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2014,8 +1957,8 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true - regjsparser@0.12.0: - resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true reserved-identifiers@1.2.0: @@ -2033,13 +1976,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown-plugin-dts@0.17.3: - resolution: {integrity: sha512-8mGnNUVNrqEdTnrlcaDxs4sAZg0No6njO+FuhQd4L56nUbJO1tHxOoKDH3mmMJg7f/BhEj/1KjU5W9kZ9zM/kQ==} - engines: {node: '>=20.18.0'} + rolldown-plugin-dts@0.18.3: + resolution: {integrity: sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg==} + engines: {node: '>=20.19.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-beta.44 + rolldown: ^1.0.0-beta.51 typescript: ^5.0.0 vue-tsc: ~3.1.0 peerDependenciesMeta: @@ -2052,8 +1995,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.46: - resolution: {integrity: sha512-FYUbq0StVHOjkR/hEJ667Pup3ugeB9odBcbmxU5il9QfT9X2t/FPhkqFYQthbYxD2bKnQyO+2vHTgnmOHwZdeA==} + rolldown@1.0.0-beta.53: + resolution: {integrity: sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2138,9 +2081,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -2180,18 +2120,17 @@ packages: peerDependencies: typescript: '>=4.0.0' - tsdown@0.16.0: - resolution: {integrity: sha512-VCqqxT5FbjCmxmLNlOLHiNhu1MBtdvCsk43murvUFloQzQzr/C0FRauWtAw7lAPmS40rZlgocCoTNFqX72WSTg==} + tsdown@0.17.0: + resolution: {integrity: sha512-NPZRrlC51X9Bb55ZTDwrWges8Dm1niCvNA5AYw7aix6pfnDnB4WR0neG5RPq75xIodg3hqlQUzzyrX7n4dmnJg==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@vitejs/devtools': ^0.0.0-alpha.10 + '@vitejs/devtools': ^0.0.0-alpha.18 publint: ^0.3.0 typescript: ^5.0.0 unplugin-lightningcss: ^0.4.0 unplugin-unused: ^0.5.0 - unrun: ^0.2.1 peerDependenciesMeta: '@arethetypeswrong/core': optional: true @@ -2205,8 +2144,6 @@ packages: optional: true unplugin-unused: optional: true - unrun: - optional: true tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2223,8 +2160,8 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - unconfig@7.3.3: - resolution: {integrity: sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==} + unconfig-core@7.4.2: + resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -2245,6 +2182,16 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unrun@0.2.17: + resolution: {integrity: sha512-mumOyjZp1K1bsa1QwfRPw7+9TxyVHSgx6LHB2dBWI4m1hGDG9b2TK3fS3H8vCl/Gl9YTSxhZ9XuLbWv3QF8GEA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -2307,24 +2254,24 @@ packages: peerDependencies: vitest: '>=3.0.0 <4.0.0 || >=4.0.0 <5.0.0' - vitest@4.0.4: - resolution: {integrity: sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==} + vitest@4.0.15: + resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 + '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.4 - '@vitest/browser-preview': 4.0.4 - '@vitest/browser-webdriverio': 4.0.4 - '@vitest/ui': 4.0.4 + '@vitest/browser-playwright': 4.0.15 + '@vitest/browser-preview': 4.0.15 + '@vitest/browser-webdriverio': 4.0.15 + '@vitest/ui': 4.0.15 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true @@ -2432,45 +2379,45 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + '@effect/cluster@0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/platform': 0.93.3(effect@3.19.6) - '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/workflow': 0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) - effect: 3.19.6 + '@effect/platform': 0.93.6(effect@3.19.9) + '@effect/rpc': 0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/workflow': 0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9) + effect: 3.19.9 kubernetes-types: 1.30.0 - '@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + '@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/platform': 0.93.3(effect@3.19.6) - effect: 3.19.6 + '@effect/platform': 0.93.6(effect@3.19.9) + effect: 3.19.9 uuid: 11.1.0 - '@effect/language-service@0.59.0': {} + '@effect/language-service@0.60.0': {} - '@effect/platform-node-shared@0.54.0(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + '@effect/platform-node-shared@0.56.0(@effect/cluster@0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/cluster': 0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) - '@effect/platform': 0.93.3(effect@3.19.6) - '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) + '@effect/cluster': 0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9) + '@effect/platform': 0.93.6(effect@3.19.9) + '@effect/rpc': 0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) '@parcel/watcher': 2.5.1 - effect: 3.19.6 + effect: 3.19.9 multipasta: 0.2.7 ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.101.1(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + '@effect/platform-node@0.103.0(@effect/cluster@0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/cluster': 0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) - '@effect/platform': 0.93.3(effect@3.19.6) - '@effect/platform-node-shared': 0.54.0(@effect/cluster@0.53.5(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6) - '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - effect: 3.19.6 + '@effect/cluster': 0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9) + '@effect/platform': 0.93.6(effect@3.19.9) + '@effect/platform-node-shared': 0.56.0(@effect/cluster@0.53.5(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9) + '@effect/rpc': 0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/sql': 0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + effect: 3.19.9 mime: 3.0.0 undici: 7.16.0 ws: 8.18.3 @@ -2478,45 +2425,45 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform@0.93.3(effect@3.19.6)': + '@effect/platform@0.93.6(effect@3.19.9)': dependencies: - effect: 3.19.6 + effect: 3.19.9 find-my-way-ts: 0.1.6 msgpackr: 1.11.5 multipasta: 0.2.7 - '@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + '@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/platform': 0.93.3(effect@3.19.6) - effect: 3.19.6 + '@effect/platform': 0.93.6(effect@3.19.9) + effect: 3.19.9 msgpackr: 1.11.5 - '@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6)': + '@effect/sql@0.48.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/experimental': 0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/platform': 0.93.3(effect@3.19.6) - effect: 3.19.6 + '@effect/experimental': 0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/platform': 0.93.6(effect@3.19.9) + effect: 3.19.9 uuid: 11.1.0 - '@effect/vitest@0.27.0(effect@3.19.6)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + '@effect/vitest@0.27.0(effect@3.19.9)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': dependencies: - effect: 3.19.6 - vitest: 4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) + effect: 3.19.9 + vitest: 4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) - '@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(@effect/platform@0.93.3(effect@3.19.6))(@effect/rpc@0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6))(effect@3.19.6)': + '@effect/workflow@0.13.0(@effect/experimental@0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(@effect/platform@0.93.6(effect@3.19.9))(@effect/rpc@0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9))(effect@3.19.9)': dependencies: - '@effect/experimental': 0.57.4(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - '@effect/platform': 0.93.3(effect@3.19.6) - '@effect/rpc': 0.72.2(@effect/platform@0.93.3(effect@3.19.6))(effect@3.19.6) - effect: 3.19.6 + '@effect/experimental': 0.57.4(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + '@effect/platform': 0.93.6(effect@3.19.9) + '@effect/rpc': 0.72.2(@effect/platform@0.93.6(effect@3.19.9))(effect@3.19.9) + effect: 3.19.9 - '@emnapi/core@1.7.0': + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.0': + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true @@ -2542,6 +2489,8 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 6.10.0 + '@es-joy/resolve.exports@1.2.0': {} + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -2651,14 +2600,6 @@ snapshots: dependencies: '@eslint/core': 0.17.0 - '@eslint/core@0.15.2': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/core@0.16.0': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -2679,9 +2620,9 @@ snapshots: '@eslint/js@9.39.1': {} - '@eslint/markdown@7.4.0': + '@eslint/markdown@7.5.1': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 '@eslint/plugin-kit': 0.4.1 github-slugger: 2.0.0 mdast-util-from-markdown: 2.0.2 @@ -2695,11 +2636,6 @@ snapshots: '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.5': - dependencies: - '@eslint/core': 0.15.2 - levn: 0.4.1 - '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -2730,16 +2666,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@luxass/eslint-config@6.0.1(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + '@luxass/eslint-config@6.0.3(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 '@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.39.1(jiti@2.6.1)) - '@eslint/markdown': 7.4.0 + '@eslint/markdown': 7.5.1 '@stylistic/eslint-plugin': 5.5.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.3.23(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) + '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@vitest/eslint-plugin': 1.4.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) eslint: 9.39.1(jiti@2.6.1) eslint-config-flat-gitignore: 2.1.0(eslint@9.39.1(jiti@2.6.1)) eslint-flat-config-utils: 2.1.4 @@ -2747,19 +2683,19 @@ snapshots: eslint-plugin-antfu: 3.1.1(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-command: 3.3.1(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import-lite: 0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-jsdoc: 61.1.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-jsdoc: 61.1.12(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsonc: 2.21.0(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-n: 17.23.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-perfectionist: 4.15.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-pnpm: 1.3.0(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-regexp: 2.10.0(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-toml: 0.12.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-unicorn: 61.0.2(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) + eslint-plugin-unicorn: 62.0.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))) eslint-plugin-yml: 1.19.0(eslint@9.39.1(jiti@2.6.1)) eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.39.1(jiti@2.6.1)) - globals: 16.4.0 + globals: 16.5.0 jsonc-eslint-parser: 2.4.1 local-pkg: 1.1.2 parse-gitignore: 2.0.0 @@ -2795,10 +2731,10 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@napi-rs/wasm-runtime@1.0.7': + '@napi-rs/wasm-runtime@1.1.0': dependencies: - '@emnapi/core': 1.7.0 - '@emnapi/runtime': 1.7.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -2814,7 +2750,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@oxc-project/types@0.96.0': {} + '@oxc-project/runtime@0.101.0': {} + + '@oxc-project/types@0.101.0': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -2878,55 +2816,52 @@ snapshots: '@pkgr/core@0.2.9': {} - '@quansync/fs@0.1.5': + '@quansync/fs@1.0.0': dependencies: - quansync: 0.2.11 + quansync: 1.0.0 - '@rolldown/binding-android-arm64@1.0.0-beta.46': + '@rolldown/binding-android-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.46': + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.46': + '@rolldown/binding-darwin-x64@1.0.0-beta.53': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.46': + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.46': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.46': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.46': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.46': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.46': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.46': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.46': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.46': + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.46': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.46': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': optional: true - '@rolldown/pluginutils@1.0.0-beta.46': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.52.5': optional: true @@ -3047,14 +2982,14 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -3064,23 +2999,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3094,29 +3020,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.2': - dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 - '@typescript-eslint/scope-manager@8.46.3': dependencies: '@typescript-eslint/types': 8.46.3 '@typescript-eslint/visitor-keys': 8.46.3 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -3124,26 +3041,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.2': {} - '@typescript-eslint/types@8.46.3': {} - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 - debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) @@ -3160,17 +3059,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -3182,64 +3070,59 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.2': - dependencies: - '@typescript-eslint/types': 8.46.2 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.46.3': dependencies: '@typescript-eslint/types': 8.46.3 eslint-visitor-keys: 4.2.1 - '@vitest/eslint-plugin@1.3.23(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + '@vitest/eslint-plugin@1.4.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': dependencies: '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) + vitest: 4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.4': + '@vitest/expect@4.0.15': dependencies: '@standard-schema/spec': 1.0.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.4 - '@vitest/utils': 4.0.4 - chai: 6.2.0 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 + chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.4(vite@7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.15(vite@7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': dependencies: - '@vitest/spy': 4.0.4 + '@vitest/spy': 4.0.15 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) - '@vitest/pretty-format@4.0.4': + '@vitest/pretty-format@4.0.15': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.4': + '@vitest/runner@4.0.15': dependencies: - '@vitest/utils': 4.0.4 + '@vitest/utils': 4.0.15 pathe: 2.0.3 - '@vitest/snapshot@4.0.4': + '@vitest/snapshot@4.0.15': dependencies: - '@vitest/pretty-format': 4.0.4 + '@vitest/pretty-format': 4.0.15 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.4': {} + '@vitest/spy@4.0.15': {} - '@vitest/utils@4.0.4': + '@vitest/utils@4.0.15': dependencies: - '@vitest/pretty-format': 4.0.4 + '@vitest/pretty-format': 4.0.15 tinyrainbow: 3.0.3 '@vue/compiler-core@3.5.22': @@ -3299,7 +3182,7 @@ snapshots: assertion-error@2.0.1: {} - ast-kit@2.1.3: + ast-kit@2.2.0: dependencies: '@babel/parser': 7.28.5 pathe: 2.0.3 @@ -3308,7 +3191,7 @@ snapshots: baseline-browser-mapping@2.8.24: {} - birpc@2.7.0: {} + birpc@3.0.0: {} boolbase@1.0.0: {} @@ -3343,7 +3226,7 @@ snapshots: ccount@2.0.1: {} - chai@6.2.0: {} + chai@6.2.1: {} chalk@4.1.2: dependencies: @@ -3354,10 +3237,6 @@ snapshots: character-entities@2.0.2: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - ci-info@4.3.1: {} clean-regexp@1.0.0: @@ -3404,8 +3283,6 @@ snapshots: deep-is@0.1.4: {} - defu@6.1.4: {} - dequal@2.0.3: {} detect-libc@1.0.3: {} @@ -3419,11 +3296,9 @@ snapshots: diff-sequences@27.5.1: {} - diff@8.0.2: {} + dts-resolver@2.1.3: {} - dts-resolver@2.1.2: {} - - effect@3.19.6: + effect@3.19.9: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 @@ -3531,9 +3406,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 - eslint-plugin-jsdoc@61.1.5(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-jsdoc@61.1.12(eslint@9.39.1(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.76.0 + '@es-joy/resolve.exports': 1.2.0 are-docs-informative: 0.0.2 comment-parser: 1.4.1 debug: 4.4.3 @@ -3621,11 +3497,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@61.0.2(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@eslint/plugin-kit': 0.3.5 + '@eslint/plugin-kit': 0.4.1 change-case: 5.4.4 ci-info: 4.3.1 clean-regexp: 1.0.0 @@ -3633,23 +3509,23 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) esquery: 1.6.0 find-up-simple: 1.0.1 - globals: 16.4.0 + globals: 16.5.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 pluralize: 8.0.0 regexp-tree: 0.1.27 - regjsparser: 0.12.0 + regjsparser: 0.13.0 semver: 7.7.3 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))): + eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) eslint: 9.39.1(jiti@2.6.1) @@ -3661,7 +3537,7 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.5.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/parser': 8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-yml@1.19.0(eslint@9.39.1(jiti@2.6.1)): dependencies: @@ -3760,7 +3636,7 @@ snapshots: esutils@2.0.3: {} - eta@4.0.1: {} + eta@4.4.1: {} expect-type@1.2.2: {} @@ -3847,7 +3723,7 @@ snapshots: globals@15.15.0: {} - globals@16.4.0: {} + globals@16.5.0: {} globrex@0.1.2: {} @@ -3870,6 +3746,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-without-cache@0.2.2: {} + imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -3890,7 +3768,8 @@ snapshots: isexe@2.0.0: {} - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-yaml@4.1.0: dependencies: @@ -3902,8 +3781,6 @@ snapshots: jsdoc-type-pratt-parser@6.10.0: {} - jsesc@3.0.2: {} - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -4332,6 +4209,8 @@ snapshots: object-deep-merge@2.0.0: {} + obug@2.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4421,9 +4300,9 @@ snapshots: quansync@0.2.11: {} - queue-microtask@1.2.3: {} + quansync@1.0.0: {} - readdirp@4.1.2: {} + queue-microtask@1.2.3: {} refa@0.12.1: dependencies: @@ -4436,9 +4315,9 @@ snapshots: regexp-tree@0.1.27: {} - regjsparser@0.12.0: + regjsparser@0.13.0: dependencies: - jsesc: 3.0.2 + jsesc: 3.1.0 reserved-identifiers@1.2.0: {} @@ -4448,43 +4327,41 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.17.3(rolldown@1.0.0-beta.46)(typescript@5.9.3): + rolldown-plugin-dts@0.18.3(rolldown@1.0.0-beta.53)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 - ast-kit: 2.1.3 - birpc: 2.7.0 - debug: 4.4.3 - dts-resolver: 2.1.2 + ast-kit: 2.2.0 + birpc: 3.0.0 + dts-resolver: 2.1.3 get-tsconfig: 4.13.0 magic-string: 0.30.21 - rolldown: 1.0.0-beta.46 + obug: 2.1.1 + rolldown: 1.0.0-beta.53 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - - supports-color - rolldown@1.0.0-beta.46: + rolldown@1.0.0-beta.53: dependencies: - '@oxc-project/types': 0.96.0 - '@rolldown/pluginutils': 1.0.0-beta.46 + '@oxc-project/types': 0.101.0 + '@rolldown/pluginutils': 1.0.0-beta.53 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.46 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.46 - '@rolldown/binding-darwin-x64': 1.0.0-beta.46 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.46 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.46 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.46 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.46 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.46 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.46 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.46 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.46 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.46 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.46 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.46 + '@rolldown/binding-android-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-x64': 1.0.0-beta.53 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.53 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.53 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.53 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.53 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.53 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.53 rollup@4.52.5: dependencies: @@ -4571,8 +4448,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -4606,29 +4481,29 @@ snapshots: picomatch: 4.0.3 typescript: 5.9.3 - tsdown@0.16.0(typescript@5.9.3): + tsdown@0.17.0(synckit@0.11.11)(typescript@5.9.3): dependencies: ansis: 4.2.0 cac: 6.7.14 - chokidar: 4.0.3 - debug: 4.4.3 - diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.46 - rolldown-plugin-dts: 0.17.3(rolldown@1.0.0-beta.46)(typescript@5.9.3) + import-without-cache: 0.2.2 + obug: 2.1.1 + rolldown: 1.0.0-beta.53 + rolldown-plugin-dts: 0.18.3(rolldown@1.0.0-beta.53)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 tree-kill: 1.2.2 - unconfig: 7.3.3 + unconfig-core: 7.4.2 + unrun: 0.2.17(synckit@0.11.11) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' - oxc-resolver - - supports-color + - synckit - vue-tsc tslib@2.8.1: @@ -4642,12 +4517,10 @@ snapshots: ufo@1.6.1: {} - unconfig@7.3.3: + unconfig-core@7.4.2: dependencies: - '@quansync/fs': 0.1.5 - defu: 6.1.4 - jiti: 2.6.1 - quansync: 0.2.11 + '@quansync/fs': 1.0.0 + quansync: 1.0.0 undici-types@6.21.0: {} @@ -4672,6 +4545,13 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unrun@0.2.17(synckit@0.11.11): + dependencies: + '@oxc-project/runtime': 0.101.0 + rolldown: 1.0.0-beta.53 + optionalDependencies: + synckit: 0.11.11 + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: browserslist: 4.27.0 @@ -4700,35 +4580,34 @@ snapshots: jiti: 2.6.1 yaml: 2.8.1 - vitest-testdirs@4.3.0(vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)): + vitest-testdirs@4.3.0(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)): dependencies: testdirs: 4.0.0 - vitest: 4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) + vitest: 4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) - vitest@4.0.4(@types/debug@4.1.12)(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1): + vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1): dependencies: - '@vitest/expect': 4.0.4 - '@vitest/mocker': 4.0.4(vite@7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) - '@vitest/pretty-format': 4.0.4 - '@vitest/runner': 4.0.4 - '@vitest/snapshot': 4.0.4 - '@vitest/spy': 4.0.4 - '@vitest/utils': 4.0.4 - debug: 4.4.3 + '@vitest/expect': 4.0.15 + '@vitest/mocker': 4.0.15(vite@7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.15 + '@vitest/runner': 4.0.15 + '@vitest/snapshot': 4.0.15 + '@vitest/spy': 4.0.15 + '@vitest/utils': 4.0.15 es-module-lexer: 1.7.0 expect-type: 1.2.2 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 vite: 7.1.12(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/debug': 4.1.12 '@types/node': 22.18.12 transitivePeerDependencies: - jiti @@ -4739,7 +4618,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml From a0929ebca20f0b7f7f1c6a6274d659edea6a67b7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:39:13 +0100 Subject: [PATCH 11/35] refactor(verify): remove unnecessary type assertions for packages Simplified the handling of `packages` by removing type assertions in the `calculateBumps` and `topologicalOrder` function calls. This improves type safety and clarity in the code. --- src/verify.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/verify.ts b/src/verify.ts index a0554e0..789316f 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -47,12 +47,12 @@ export function constructVerifyProgram( const packages = (yield* workspace.discoverWorkspacePackages.pipe( Effect.flatMap(mergePackageCommitsIntoPackages), Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), - )) as readonly WorkspacePackage[]; + )); yield* Console.log("Discovered packages with commits and global commits:", packages); - const releases = yield* versionCalculator.calculateBumps(packages as any, overrides); - const ordered = yield* dependencyGraph.topologicalOrder(packages as any); + const releases = yield* versionCalculator.calculateBumps(packages, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages); yield* Console.log("Calculated releases:", releases); yield* Console.log("Release order:", ordered); From 8c09ca772927e4bb0498b4e652cc999a2c74609c Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:40:01 +0100 Subject: [PATCH 12/35] chore: remove unused import of `WorkspacePackage` from `verify.ts` --- src/services/git.service.ts | 2 +- src/verify.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 2546aae..467ba84 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -1,7 +1,7 @@ import { Command, CommandExecutor } from "@effect/platform"; import { NodeCommandExecutor } from "@effect/platform-node"; import * as CommitParser from "commit-parser"; -import { Effect, Layer } from "effect"; +import { Effect } from "effect"; import { ExternalCommitParserError, GitCommandError } from "../errors"; import { ReleaseScriptsOptions } from "../options"; diff --git a/src/verify.ts b/src/verify.ts index 789316f..5a72218 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -1,5 +1,4 @@ import type { NormalizedReleaseScriptsOptions } from "./options"; -import type { WorkspacePackage } from "./services/workspace.service"; import { DependencyGraphService } from "#services/dependency-graph"; import { GitService } from "#services/git"; import { GitHubService } from "#services/github"; From b6ac17c29d9f2e95e4c4e4341e7dbca5c33cd6c6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 16:46:07 +0100 Subject: [PATCH 13/35] feat(options): enhance `FindWorkspacePackagesOptions` and update related interfaces - Added `FindWorkspacePackagesOptions` interface to encapsulate package inclusion/exclusion logic. - Updated `ReleaseScriptsOptionsInput` to utilize `FindWorkspacePackagesOptions` for better clarity and structure. - Introduced `PackageUpdateOrder` interface with a new `level` property for dependency management. - Refactored imports in various services to use `ReleaseScriptsOptions` instead of `ConfigOptions` for consistency. - Added `PackageRelease` interface to define the structure of package updates, including versioning details and change indicators. --- src/options.ts | 12 ++++---- src/services/dependency-graph.service.ts | 6 +++- src/services/github.service.ts | 5 ++-- src/services/package-updater.service.ts | 33 ++++++++++++++++++++-- src/services/version-calculator.service.ts | 2 +- src/services/workspace.service.ts | 6 ++-- 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/options.ts b/src/options.ts index 7559d7c..eb3a2ea 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,15 +5,17 @@ type DeepRequired = Required<{ [K in keyof T]: T[K] extends Required ? T[K] : DeepRequired }>; +export interface FindWorkspacePackagesOptions { + exclude?: string[]; + include?: string[]; + excludePrivate?: boolean; +} + export interface ReleaseScriptsOptionsInput { dryRun?: boolean; repo: `${string}/${string}`; workspaceRoot?: string; - packages?: true | { - exclude?: string[]; - include?: string[]; - excludePrivate?: boolean; - } | string[]; + packages?: true | FindWorkspacePackagesOptions | string[]; githubToken: string; branch?: { release?: string; diff --git a/src/services/dependency-graph.service.ts b/src/services/dependency-graph.service.ts index 85e8a8c..21a13e1 100644 --- a/src/services/dependency-graph.service.ts +++ b/src/services/dependency-graph.service.ts @@ -1,7 +1,11 @@ -import type { PackageUpdateOrder } from "../shared/types"; import type { WorkspacePackage } from "./workspace.service"; import { Effect } from "effect"; +export interface PackageUpdateOrder { + package: WorkspacePackage; + level: number; +} + export class DependencyGraphService extends Effect.Service()( "@ucdjs/release-scripts/DependencyGraphService", { diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 57e5ab1..2aa7cf4 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,9 +1,8 @@ import { Effect, Schema } from "effect"; import { GitHubError } from "../errors.js"; -import { ConfigOptions } from "../options.js"; +import { ReleaseScriptsOptions } from "../options.js"; import { GitService } from "./git.service.js"; -// Schema definitions for GitHub API types export const PullRequestSchema = Schema.Struct({ number: Schema.Number, title: Schema.String, @@ -57,7 +56,7 @@ export type RepositoryInfo = Schema.Schema.Type; export class GitHubService extends Effect.Service()("@ucdjs/release-scripts/GitHubService", { effect: Effect.gen(function* () { - const config = yield* ConfigOptions; + const config = yield* ReleaseScriptsOptions; function makeRequest(endpoint: string, schema: Schema.Schema, options: RequestInit = {}) { const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`; diff --git a/src/services/package-updater.service.ts b/src/services/package-updater.service.ts index f1c1c24..84111d3 100644 --- a/src/services/package-updater.service.ts +++ b/src/services/package-updater.service.ts @@ -1,9 +1,8 @@ -import type { PackageRelease } from "../shared/types"; import type { WorkspacePackage } from "./workspace.service"; import fs from "node:fs/promises"; import path from "node:path"; import { Effect } from "effect"; -import { ConfigOptions } from "../options"; +import { ReleaseScriptsOptions } from "../options"; function nextRange(oldRange: string, newVersion: string): string { const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : ""; @@ -40,11 +39,39 @@ function updateDependencyRecord( return { updated: changed, next: changed ? next : record }; } +export type BumpKind = "none" | "patch" | "minor" | "major"; +export interface PackageRelease { + /** + * The package being updated + */ + package: WorkspacePackage; + + /** + * Current version + */ + currentVersion: string; + + /** + * New version to release + */ + newVersion: string; + + /** + * Type of version bump + */ + bumpType: BumpKind; + + /** + * Whether this package has direct changes (vs being updated due to dependency changes) + */ + hasDirectChanges: boolean; +} + export class PackageUpdaterService extends Effect.Service()( "@ucdjs/release-scripts/PackageUpdaterService", { effect: Effect.gen(function* () { - const config = yield* ConfigOptions; + const config = yield* ReleaseScriptsOptions; function writePackageJson(pkgPath: string, json: unknown) { const fullPath = path.join(pkgPath, "package.json"); diff --git a/src/services/version-calculator.service.ts b/src/services/version-calculator.service.ts index 2a0002c..872be98 100644 --- a/src/services/version-calculator.service.ts +++ b/src/services/version-calculator.service.ts @@ -1,5 +1,5 @@ -import type { BumpKind, PackageRelease } from "../shared/types"; import type { WorkspacePackageWithCommits } from "../utils/helpers"; +import type { BumpKind, PackageRelease } from "./package-updater.service"; import { Effect } from "effect"; import semver from "semver"; import { VersionCalculationError } from "../errors"; diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index c9daa4c..f21a7bf 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -1,10 +1,10 @@ -import type { FindWorkspacePackagesOptions } from "#shared/types"; +import type { FindWorkspacePackagesOptions } from "../options"; import fs from "node:fs/promises"; import path from "node:path"; import { Command, CommandExecutor } from "@effect/platform"; import { Effect, Schema } from "effect"; import { WorkspaceError } from "../errors"; -import { ConfigOptions } from "../options"; +import { ReleaseScriptsOptions } from "../options"; export const DependencyObjectSchema = Schema.Record({ key: Schema.String, @@ -46,7 +46,7 @@ const WorkspaceListSchema = Schema.Array(Schema.Struct({ export class WorkspaceService extends Effect.Service()("@ucdjs/release-scripts/WorkspaceService", { effect: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; - const config = yield* ConfigOptions; + const config = yield* ReleaseScriptsOptions; const workspacePackageListOutput = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json").pipe( Command.workingDirectory(config.workspaceRoot), From 8a8e8ad7b59e67a1a47787142b29b9905fd83e9a Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:02:57 +0100 Subject: [PATCH 14/35] refactor: simplify package handling in `createReleaseScripts` and update test cases - Removed unnecessary type assertions in `createReleaseScripts` for better type safety. - Updated test cases in `options.test.ts` to remove type assertions for `repo` parameter. --- playground.ts | 30 ------------------------------ src/index.ts | 4 ++-- test/options.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 playground.ts diff --git a/playground.ts b/playground.ts deleted file mode 100644 index d0f1879..0000000 --- a/playground.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node"; -import { Effect } from "effect"; -import { ConfigOptions } from "./src/services/config.service.ts"; -import { GitService } from "./src/services/git.service.ts"; - -const program = Effect.gen(function* () { - const git = yield* GitService; - - yield* git.commit.stage(["."]); - yield* git.commit.write("refactor: change git & github services"); - yield* git.commit.push("effect-rewrite"); - - yield* git.branches.checkout("main").pipe(Effect.catchAll((err) => { - console.error(`Error checking out main branch: ${err.message}`); - return Effect.fail(err); - })); - - return void 0; -}); - -const runnable = program.pipe( - Effect.provide(GitService.Default), - Effect.provide(NodeCommandExecutor.layer), - Effect.provide(NodeFileSystem.layer), - Effect.provide(ConfigOptions.layer({ - workspaceRoot: ".", - })), -); - -Effect.runPromise(runnable); diff --git a/src/index.ts b/src/index.ts index 8bfadc3..ee6a4e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,8 +100,8 @@ export async function createReleaseScripts(options: ReleaseScriptsOptionsInput): yield* Console.log("Discovered packages with commits and global commits:", packages); - const releases = yield* versionCalculator.calculateBumps(packages as any, overrides); - const ordered = yield* dependencyGraph.topologicalOrder(packages as any); + const releases = yield* versionCalculator.calculateBumps(packages, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages); yield* Console.log("Calculated releases:", releases); yield* Console.log("Release order:", ordered); diff --git a/test/options.test.ts b/test/options.test.ts index 9da9baa..1705079 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -26,8 +26,8 @@ describe("normalizeReleaseScriptsOptions - global", () => { }); it("throws if repo is missing or invalid", () => { - expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "invalid/" as any })).toThrow(); - expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "/invalid" as any })).toThrow(); + expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "invalid/" })).toThrow(); + expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "/invalid" })).toThrow(); }); it("throws if githubToken is missing", () => { From 44ca1e576684d60dd10f00c7769167ca0b82ee81 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:04:57 +0100 Subject: [PATCH 15/35] feat(options): add customizable `types` to `ReleaseScriptsOptionsInput` This update introduces a new `types` property to the `ReleaseScriptsOptionsInput` interface, allowing users to customize the types of changes in their release scripts. The default types are now merged with any user-defined types, providing greater flexibility in defining the structure of release notes. --- src/options.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/options.ts b/src/options.ts index eb3a2ea..0831dbe 100644 --- a/src/options.ts +++ b/src/options.ts @@ -43,6 +43,14 @@ export type NormalizedReleaseScriptsOptions = DeepRequired Date: Sun, 7 Dec 2025 19:05:47 +0100 Subject: [PATCH 16/35] feat(git): sort tags by version when retrieving most recent package tag Updated the `getMostRecentPackageTag` function to sort the tags in descending order by version using the `--sort=-version:refname` option. This change ensures that the most recent version tag is returned first, improving the accuracy of version retrieval for packages. --- src/services/git.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 467ba84..2d504f1 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -102,7 +102,7 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr } function getMostRecentPackageTag(packageName: string) { - return execGitCommand(["tag", "--list", `${packageName}@*`]).pipe( + return execGitCommand(["tag", "--list", "--sort=-version:refname", `${packageName}@*`]).pipe( Effect.map((tags) => { const tagList = tags .trim() From 558b2bce63843321a363c9c60a246438c7c48cc9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:06:40 +0100 Subject: [PATCH 17/35] fix(github): simplify pull request URL construction in `getPullRequestByBranch` --- src/services/github.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 2aa7cf4..527f8cf 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -105,8 +105,7 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea function getPullRequestByBranch(branch: string) { const head = branch.includes(":") ? branch : `${config.owner}:${branch}`; - const url = `/repos/${config.owner}/${config.repo}/pulls?state=open&head=${encodeURIComponent(head)}`; - return makeRequest(url, Schema.Array(PullRequestSchema)).pipe( + return makeRequest(`pulls?state=open&head=${encodeURIComponent(head)}`, Schema.Array(PullRequestSchema)).pipe( Effect.map((pulls) => (pulls.length > 0 ? pulls[0] : null)), Effect.mapError((e) => new GitHubError({ message: e.message, From 5f374227b3a7a7c0aecd4ade13891693e3919a89 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:07:17 +0100 Subject: [PATCH 18/35] fix(workspace): log warning for invalid packages in `WorkspaceService` Updated the error handling in `WorkspaceService` to log a warning message when an invalid package is encountered, improving debugging and visibility into package processing issues. --- src/services/workspace.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index f21a7bf..15a458a 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -186,7 +186,11 @@ export class WorkspaceService extends Effect.Service()("@ucdjs ), ); }), - Effect.catchAll(() => Effect.succeed(null)), + Effect.catchAll(() => { + return Effect.logWarning(`Skipping invalid package ${rawProject.name}`).pipe( + Effect.as(null), + ); + }), ), ), ).pipe( From 44a2e19fcfedd82315bcb5fc3f06bcdcce0c9197 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:07:51 +0100 Subject: [PATCH 19/35] fix(helpers): update `readFile` to include SHA in overrides loading Modified the `loadOverrides` function to pass the `options.sha` parameter to the `readFile` method, ensuring the correct version of the overrides file is read based on the specified SHA. --- src/utils/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e6fe5c7..ff0dd18 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -18,7 +18,7 @@ export function loadOverrides(options: LoadOverridesOptions) { return Effect.gen(function* () { const git = yield* GitService; - return yield* git.workspace.readFile(options.overridesPath, "utf8").pipe( + return yield* git.workspace.readFile(options.overridesPath, options.sha).pipe( Effect.map((content) => ({ content, readError: null as unknown, From c6e652c91e1921873e2c18d34bf8c2d1d2bd20df Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:10:17 +0100 Subject: [PATCH 20/35] fix(helpers): simplify error handling in `loadOverrides` function Refactor the `loadOverrides` function to streamline error handling when reading and parsing the overrides file. The new implementation uses `Effect.try` to directly handle JSON parsing errors, improving clarity and reducing unnecessary checks. --- src/utils/helpers.ts | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index ff0dd18..bd2f590 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -19,33 +19,16 @@ export function loadOverrides(options: LoadOverridesOptions) { const git = yield* GitService; return yield* git.workspace.readFile(options.overridesPath, options.sha).pipe( - Effect.map((content) => ({ - content, - readError: null as unknown, - })), - Effect.catchAll((err) => - Effect.succeed({ - content: "", - readError: err, + Effect.flatMap((content) => + Effect.try({ + try: () => JSON.parse(content) as VersionOverrides, + catch: (err) => new OverridesLoadError({ + message: "Failed to parse overrides file.", + cause: err, + }), }), ), - Effect.flatMap(({ content, readError }) => { - if (!content) { - return Effect.succeed({} as VersionOverrides); - } - - return Effect.try({ - try: () => JSON.parse(content) as VersionOverrides, - catch: (err) => { - return new OverridesLoadError({ - message: "Failed to parse overrides file.", - cause: readError || err, - }); - }, - }).pipe( - Effect.catchAll(() => Effect.succeed({} as VersionOverrides)), - ); - }), + Effect.catchAll(() => Effect.succeed({} as VersionOverrides)), ); }); } From 6a7827b2e1dd24d4a77f1505b55b80bdacc5f453 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 7 Dec 2025 19:25:24 +0100 Subject: [PATCH 21/35] fix(git): update handling of lastTag in commit retrieval functions - Adjusted `mergePackageCommitsIntoPackages` to use `lastTag?.name` for fetching commits. - Modified `mergeCommitsAffectingGloballyIntoPackage` to utilize `lastTag.sha.slice(0, 7)` for cutoff timestamp calculation. --- src/services/git.service.ts | 11 +++++++++++ src/utils/helpers.ts | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 2d504f1..cdcec57 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -112,6 +112,17 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr return tagList.reverse()[0] || null; }), + Effect.flatMap((tag) => { + if (tag === null) { + return Effect.succeed(null); + } + return execGitCommand(["rev-parse", tag]).pipe( + Effect.map((sha) => ({ + name: tag, + sha: sha.trim(), + })), + ); + }), ); } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index bd2f590..c8971e4 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -77,7 +77,7 @@ export function mergePackageCommitsIntoPackages( const lastTag = yield* git.tags.mostRecentForPackage(pkg.name); const commits = yield* git.commits.get({ - from: lastTag || undefined, + from: lastTag?.name || undefined, to: "HEAD", folder: pkg.path, }); @@ -165,7 +165,7 @@ export function mergeCommitsAffectingGloballyIntoPackage( for (const pkg of packages) { // Get the package's last release tag timestamp const lastTag = yield* git.tags.mostRecentForPackage(pkg.name); - const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag) ?? 0 : 0; + const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag.sha.slice(0, 7)) ?? 0 : 0; const globalCommits: CommitParser.GitCommit[] = []; From 91b9acdc4538e781f476853821035606eb8f9c32 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:37:16 +0100 Subject: [PATCH 22/35] fix(github): update `body` field in `CreatePullRequestOptionsSchema` to allow null values --- src/services/github.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 527f8cf..bb30dbb 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -24,7 +24,7 @@ export const PullRequestSchema = Schema.Struct({ export const CreatePullRequestOptionsSchema = Schema.Struct({ title: Schema.String, - body: Schema.String, + body: Schema.NullOr(Schema.String), head: Schema.String, base: Schema.String, draft: Schema.optional(Schema.Boolean), From 964e5401147d07c93ece3327339dfcd257b16ddd Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:37:24 +0100 Subject: [PATCH 23/35] fix(git): use `execGitCommandIfNotDry` for staging changes to respect dry-run mode --- src/services/git.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index cdcec57..0ea25da 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -80,7 +80,7 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr return yield* Effect.fail(new Error("No files to stage.")); } - return yield* execGitCommand(["add", ...files]); + return yield* execGitCommandIfNotDry(["add", ...files]); }); } From 0a995dd29d9a8412bfbce5e2b55b5a7cd7a57af3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:38:08 +0100 Subject: [PATCH 24/35] fix(helpers): correct cutoff timestamp calculation in `mergeCommitsAffectingGloballyIntoPackage` Updated the calculation of `cutoffTimestamp` to use the full SHA from `lastTag` instead of slicing it. This ensures accurate retrieval of commit timestamps for the most recent tag associated with a package. --- src/utils/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index c8971e4..d6a721b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -165,7 +165,7 @@ export function mergeCommitsAffectingGloballyIntoPackage( for (const pkg of packages) { // Get the package's last release tag timestamp const lastTag = yield* git.tags.mostRecentForPackage(pkg.name); - const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag.sha.slice(0, 7)) ?? 0 : 0; + const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag.sha) ?? 0 : 0; const globalCommits: CommitParser.GitCommit[] = []; From 623190b50837fc9d5e7c060725c69d0735885b61 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:38:55 +0100 Subject: [PATCH 25/35] fix(helpers): update commit timestamp retrieval in `mergeCommitsAffectingGloballyIntoPackage` Replaced the use of `commit.shortHash` with `commit.hash` for accurate timestamp and affected files retrieval. This change ensures that the correct commit information is used when filtering commits that occurred after a package's last release. --- src/utils/helpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index d6a721b..ff98dd1 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -171,12 +171,12 @@ export function mergeCommitsAffectingGloballyIntoPackage( // Filter commits that occurred after this package's last release for (const commit of allCommits) { - const commitTimestamp = commitTimestamps.get(commit.shortHash); + const commitTimestamp = commitTimestamps.get(commit.hash); if (commitTimestamp == null || commitTimestamp <= cutoffTimestamp) { continue; // Skip commits at or before the package's last release } - const files = affectedFilesPerCommit.get(commit.shortHash); + const files = affectedFilesPerCommit.get(commit.hash); if (!files) continue; // Check if this commit is a global commit @@ -294,5 +294,5 @@ export function findCommitRange(packages: readonly WorkspacePackageWithCommits[] return [null, null]; } - return [oldestCommit.shortHash, newestCommit.shortHash]; + return [oldestCommit.hash, newestCommit.hash]; } From 272dda87351eb638c59e0848f80bb9e145191b2a Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:39:58 +0100 Subject: [PATCH 26/35] fix(workspace): remove unused `excludedPackages` set in `WorkspaceService` --- src/services/workspace.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index 15a458a..46e0c03 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -152,14 +152,12 @@ export class WorkspaceService extends Effect.Service()("@ucdjs return workspacePackageListOutput.pipe( Effect.flatMap((rawProjects) => { const allPackageNames = new Set(rawProjects.map((p) => p.name)); - const excludedPackages = new Set(); return Effect.all( rawProjects.map((rawProject) => readPackageJson(rawProject.path).pipe( Effect.flatMap((packageJson) => { if (!shouldIncludePackage(packageJson, options)) { - excludedPackages.add(rawProject.name); return Effect.succeed(null); } From e9227045acdf54d2e660e7d00d64a6620982f6c7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:44:09 +0100 Subject: [PATCH 27/35] fix(git): simplify `writeCommit` and `pushChanges` functions Refactor the `writeCommit` and `pushChanges` functions to directly return the result of `execGitCommandIfNotDry`, removing unnecessary generator functions. This change improves code readability and maintains the existing functionality. --- src/services/git.service.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index 0ea25da..fc6a8dd 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -85,16 +85,11 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr } function writeCommit(message: string) { - return Effect.gen(function* () { - return yield* execGitCommandIfNotDry(["commit", "-m", message]); - }); + return execGitCommandIfNotDry(["commit", "-m", message]); } function pushChanges(branch: string, remote: string = "origin") { - return Effect.gen(function* () { - const result = yield* execGitCommandIfNotDry(["push", remote, branch]); - return result; - }); + return execGitCommandIfNotDry(["push", remote, branch]); } function readFile(filePath: string, ref: string = "HEAD") { From 366bfe2f3961a721e6c5da956611a638220f93a2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:46:11 +0100 Subject: [PATCH 28/35] fix(package-updater): enhance `nextRange` to validate complex version ranges Updated the `nextRange` function to check if the new version satisfies existing complex ranges using `semver.satisfies`. If the new version is outside the existing range, an error is thrown to prevent silent failures. This change lays the groundwork for future implementation of range updating logic. --- src/services/package-updater.service.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/services/package-updater.service.ts b/src/services/package-updater.service.ts index 84111d3..866ad69 100644 --- a/src/services/package-updater.service.ts +++ b/src/services/package-updater.service.ts @@ -2,8 +2,12 @@ import type { WorkspacePackage } from "./workspace.service"; import fs from "node:fs/promises"; import path from "node:path"; import { Effect } from "effect"; +import semver from "semver"; import { ReleaseScriptsOptions } from "../options"; +const DASH_RE = / - /; +const RANGE_OPERATION_RE = /^(?:>=|<=|[><=])/; + function nextRange(oldRange: string, newVersion: string): string { const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : ""; const raw = workspacePrefix ? oldRange.slice("workspace:".length) : oldRange; @@ -12,6 +16,26 @@ function nextRange(oldRange: string, newVersion: string): string { return `${workspacePrefix}${raw}`; } + // Check if this is a complex range (contains operators/spaces beyond simple ^ or ~) + const isComplexRange = raw.includes("||") || DASH_RE.test(raw) || RANGE_OPERATION_RE.test(raw) || (raw.includes(" ") && !DASH_RE.test(raw)); + + if (isComplexRange) { + // For complex ranges, check if the new version satisfies the existing range + if (semver.satisfies(newVersion, raw)) { + // New version is within range, keep the range as-is + return `${workspacePrefix}${raw}`; + } + + // TODO: Implement range updating logic for when new version is outside the existing range + // For now, we fail/error to avoid silently breaking dependency constraints + throw new Error( + `Cannot update range "${oldRange}" to version ${newVersion}: ` + + `new version is outside the existing range. ` + + `Complex range updating is not yet implemented.`, + ); + } + + // Handle simple ^ and ~ prefixes const prefix = raw.startsWith("^") || raw.startsWith("~") ? raw[0] : ""; return `${workspacePrefix}${prefix}${newVersion}`; } From 9a18e180dcd15e2fe656c8599a0e20062c7cfb6e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:51:49 +0100 Subject: [PATCH 29/35] fix(package-updater): refactor to use `writePackageJson` from `WorkspaceService` - Moved the `writePackageJson` function to `WorkspaceService` for better modularity. - Updated `PackageUpdaterService` to utilize the new `writePackageJson` method, improving code organization and maintainability. --- src/services/package-updater.service.ts | 28 ++++++------------------- src/services/workspace.service.ts | 19 +++++++++++++++++ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/services/package-updater.service.ts b/src/services/package-updater.service.ts index 866ad69..5112a9a 100644 --- a/src/services/package-updater.service.ts +++ b/src/services/package-updater.service.ts @@ -1,9 +1,7 @@ import type { WorkspacePackage } from "./workspace.service"; -import fs from "node:fs/promises"; -import path from "node:path"; import { Effect } from "effect"; import semver from "semver"; -import { ReleaseScriptsOptions } from "../options"; +import { WorkspaceService } from "./workspace.service"; const DASH_RE = / - /; const RANGE_OPERATION_RE = /^(?:>=|<=|[><=])/; @@ -95,23 +93,7 @@ export class PackageUpdaterService extends Effect.Service "@ucdjs/release-scripts/PackageUpdaterService", { effect: Effect.gen(function* () { - const config = yield* ReleaseScriptsOptions; - - function writePackageJson(pkgPath: string, json: unknown) { - const fullPath = path.join(pkgPath, "package.json"); - const content = `${JSON.stringify(json, null, 2)}\n`; - - if (config.dryRun) { - return Effect.succeed(`Dry run: skip writing ${fullPath}`); - } - - return Effect.tryPromise({ - try: async () => { - await fs.writeFile(fullPath, content, "utf8"); - }, - catch: (e) => e as Error, - }); - } + const workspace = yield* WorkspaceService; function applyReleases( allPackages: readonly WorkspacePackage[], @@ -157,7 +139,7 @@ export class PackageUpdaterService extends Effect.Service return "skipped" as const; } - return yield* writePackageJson(pkg.path, nextJson).pipe( + return yield* workspace.writePackageJson(pkg.path, nextJson).pipe( Effect.map(() => "written" as const), ); }), @@ -169,6 +151,8 @@ export class PackageUpdaterService extends Effect.Service applyReleases, } as const; }), - dependencies: [], + dependencies: [ + WorkspaceService.Default, + ], }, ) {} diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index 46e0c03..93740f9 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -100,6 +100,24 @@ export class WorkspaceService extends Effect.Service()("@ucdjs ); } + function writePackageJson(pkgPath: string, json: unknown) { + const fullPath = path.join(pkgPath, "package.json"); + const content = `${JSON.stringify(json, null, 2)}\n`; + + if (config.dryRun) { + return Effect.succeed(`Dry run: skip writing ${fullPath}`); + } + + return Effect.tryPromise({ + try: async () => await fs.writeFile(fullPath, content, "utf8"), + catch: (e) => new WorkspaceError({ + message: `Failed to write package.json for ${pkgPath}`, + cause: e, + operation: "writePackageJson", + }), + }); + } + const discoverWorkspacePackages = Effect.gen(function* () { let workspaceOptions: FindWorkspacePackagesOptions; let explicitPackages: string[] | undefined; @@ -237,6 +255,7 @@ export class WorkspaceService extends Effect.Service()("@ucdjs return { readPackageJson, + writePackageJson, findWorkspacePackages, discoverWorkspacePackages, findPackageByName, From c000ba7ef84a057dee53b41a354d6f1ec319b513 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:55:34 +0100 Subject: [PATCH 30/35] fix(github): clean up imports and remove unused dependencies --- src/services/github.service.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/github.service.ts b/src/services/github.service.ts index bb30dbb..c585e97 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -1,7 +1,6 @@ import { Effect, Schema } from "effect"; -import { GitHubError } from "../errors.js"; -import { ReleaseScriptsOptions } from "../options.js"; -import { GitService } from "./git.service.js"; +import { GitHubError } from "../errors"; +import { ReleaseScriptsOptions } from "../options"; export const PullRequestSchema = Schema.Struct({ number: Schema.Number, @@ -119,7 +118,5 @@ export class GitHubService extends Effect.Service()("@ucdjs/relea getPullRequestByBranch, } as const; }), - dependencies: [ - GitService.Default, - ], + dependencies: [], }) { } From 14c8add5f9db318eba8fb3d303e43482e4de581e Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:59:02 +0100 Subject: [PATCH 31/35] fix(version-calculator): refactor `maxBump` function for clarity Improves the `maxBump` function by explicitly calculating the priorities of `incoming` and `current` bump types. This change enhances readability and maintainability of the code. --- src/services/version-calculator.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/version-calculator.service.ts b/src/services/version-calculator.service.ts index 872be98..0b3e354 100644 --- a/src/services/version-calculator.service.ts +++ b/src/services/version-calculator.service.ts @@ -12,7 +12,9 @@ const BUMP_PRIORITY: Record = { }; function maxBump(current: BumpKind, incoming: BumpKind): BumpKind { - return BUMP_PRIORITY[incoming] > BUMP_PRIORITY[current] ? incoming : current; + const incomingPriority = BUMP_PRIORITY[incoming] ?? 0; + const currentPriority = BUMP_PRIORITY[current] ?? 0; + return incomingPriority > currentPriority ? incoming : current; } function bumpFromCommit(commit: { type?: string; isBreaking?: boolean }): BumpKind { From e55ea251a288759b7bd59276d29aa496339bdee1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 01:59:29 +0100 Subject: [PATCH 32/35] fix(version-calculator): limit concurrency to 10 in `calculateBumps` --- src/services/version-calculator.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/version-calculator.service.ts b/src/services/version-calculator.service.ts index 0b3e354..8425fc1 100644 --- a/src/services/version-calculator.service.ts +++ b/src/services/version-calculator.service.ts @@ -89,7 +89,7 @@ export class VersionCalculatorService extends Effect.Service Date: Mon, 8 Dec 2025 02:02:55 +0100 Subject: [PATCH 33/35] fix(dependency-graph): improve cycle detection error message and refactor queue processing Refactor the queue processing in `topologicalOrder` to use an index instead of shifting elements, enhancing performance. Additionally, improve the error message for cycle detection to include the names of unprocessed packages, providing better context for debugging. --- src/services/dependency-graph.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/services/dependency-graph.service.ts b/src/services/dependency-graph.service.ts index 21a13e1..890973a 100644 --- a/src/services/dependency-graph.service.ts +++ b/src/services/dependency-graph.service.ts @@ -54,10 +54,11 @@ export class DependencyGraphService extends Effect.Service 0) { - const current = queue.shift()!; + while (queueIndex < queue.length) { + const current = queue[queueIndex++]!; const currentLevel = levels.get(current) ?? 0; const pkg = nameToPackage.get(current); @@ -81,7 +82,9 @@ export class DependencyGraphService extends Effect.Service o.package.name)); + const unprocessed = packages.filter((p) => !processed.has(p.name)).map((p) => p.name); + return yield* Effect.fail(new Error(`Cycle detected in workspace dependencies. Packages involved: ${unprocessed.join(", ")}`)); } return ordered; From a45628019a5e7d55cb91a30f9cf45b8ab1db782f Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 02:34:48 +0100 Subject: [PATCH 34/35] fix(git.service): simplify tag retrieval logic in `getMostRecentPackageTag` --- src/services/git.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/git.service.ts b/src/services/git.service.ts index fc6a8dd..b4b7dd4 100644 --- a/src/services/git.service.ts +++ b/src/services/git.service.ts @@ -105,7 +105,7 @@ export class GitService extends Effect.Service()("@ucdjs/release-scr .map((tag) => tag.trim()) .filter((tag) => tag.length > 0); - return tagList.reverse()[0] || null; + return tagList[0] || null; }), Effect.flatMap((tag) => { if (tag === null) { From 87d3186151cf8b7b73df4b0d1962b1f7446a7079 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 8 Dec 2025 02:35:20 +0100 Subject: [PATCH 35/35] fix(helpers): update commit timestamp lookup to use full hash Changed the commit timestamp lookup in `mergeCommitsAffectingGloballyIntoPackage` to utilize the full `hash` instead of `shortHash`. This improves the accuracy of commit identification during the merge process. --- src/utils/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index ff98dd1..9637c6d 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -156,7 +156,7 @@ export function mergeCommitsAffectingGloballyIntoPackage( // Used for quick lookup of commit timestamps/cutoffs const commitTimestamps = new Map( - allCommits.map((c) => [c.shortHash, new Date(c.date).getTime()]), + allCommits.map((c) => [c.hash, new Date(c.date).getTime()]), ); const packagePaths = new Set(packages.map((p) => p.path));