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/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/package.json b/package.json index f1c6776..ff7e326 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,14 @@ "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", "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", @@ -36,8 +31,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@effect/platform": "0.93.6", + "@effect/platform-node": "0.103.0", "@luxass/utils": "2.7.2", "commit-parser": "1.3.0", + "effect": "3.19.9", "farver": "1.0.0-beta.1", "mri": "1.2.0", "prompts": "2.4.2", @@ -45,15 +43,17 @@ "tinyexec": "1.0.2" }, "devDependencies": { - "@luxass/eslint-config": "6.0.1", + "@effect/language-service": "^0.60.0", + "@effect/vitest": "0.27.0", + "@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 bd44192..a3c05bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@effect/platform': + specifier: 0.93.6 + version: 0.93.6(effect@3.19.9) + '@effect/platform-node': + 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 commit-parser: specifier: 1.3.0 version: 1.3.0 + effect: + specifier: 3.19.9 + version: 3.19.9 farver: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1 @@ -30,9 +39,15 @@ importers: specifier: 1.0.2 version: 1.0.2 devDependencies: + '@effect/language-service': + specifier: ^0.60.0 + version: 0.60.0 + '@effect/vitest': + specifier: 0.27.0 + 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 @@ -46,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: @@ -93,11 +108,87 @@ packages: '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} - '@emnapi/core@1.7.0': - resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} + '@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.60.0': + resolution: {integrity: sha512-elJDWHG5Naq3OkilPt9ZRn56JfSA3MhXUIlDx9RWJeScHm96kZ+HkZ3eFBxqROzXwD6Q2DTtFctFwOM0+QLZEA==} + hasBin: true + + '@effect/platform-node-shared@0.56.0': + resolution: {integrity: sha512-0RawLcUCLHVGs4ch1nY26P4xM+U6R03ZR02MgNHMsL0slh8YYlal5PnwD/852rJ59O9prQX3Kq8zs+cGVoLAJw==} + peerDependencies: + '@effect/cluster': ^0.55.0 + '@effect/platform': ^0.93.6 + '@effect/rpc': ^0.72.2 + '@effect/sql': ^0.48.6 + effect: ^3.19.8 + + '@effect/platform-node@0.103.0': + resolution: {integrity: sha512-N2JmOvHInHAC+JFdt+ME8/Pn9vdgBwYTTcqlSXkT+mBzq6fAKdwHkXHoFUMbk8bWtJGx70oezLLEetatjsveaA==} + peerDependencies: + '@effect/cluster': ^0.55.0 + '@effect/platform': ^0.93.6 + '@effect/rpc': ^0.72.2 + '@effect/sql': ^0.48.6 + effect: ^3.19.8 + + '@effect/platform@0.93.6': + resolution: {integrity: sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w==} + peerDependencies: + effect: ^3.19.8 - '@emnapi/runtime@1.7.0': - resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} + '@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/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.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -110,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'} @@ -299,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} @@ -319,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} @@ -364,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 @@ -402,8 +485,38 @@ packages: resolution: {integrity: sha512-l2tPbXLeXR/c5ADU3YwLqULwm5UGFSRwBtyqyClY3zu3uKG3KbHq3FntyqQhJXdVW4VNZ3p/8fsALmQ7+YN/cw==} engines: {node: '>=20'} - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@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.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -417,101 +530,181 @@ 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==} + 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} - '@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==} @@ -672,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} @@ -760,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: @@ -781,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 @@ -795,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==} @@ -857,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: @@ -868,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==} @@ -907,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: @@ -921,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'} @@ -983,13 +1135,19 @@ 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'} + 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==} @@ -997,19 +1155,18 @@ 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.9: + resolution: {integrity: sha512-taMXnfG/p+j7AmMOHHQaCHvjqwu9QBO3cxuZqL2dMG/yWcEMw0ZHruHe9B49OxtfKH/vKKDDKRhZ+1GJ2p5R5w==} + electron-to-chromium@1.5.245: resolution: {integrity: sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==} @@ -1111,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 @@ -1152,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==} @@ -1245,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: @@ -1260,6 +1417,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 +1457,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'} @@ -1342,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: @@ -1377,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'} @@ -1428,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'} @@ -1458,6 +1621,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 +1782,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 +1804,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 +1826,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==} @@ -1654,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'} @@ -1742,16 +1933,18 @@ 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==} + 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} @@ -1764,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: @@ -1783,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: @@ -1802,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 @@ -1888,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'} @@ -1930,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 @@ -1955,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==} @@ -1973,12 +2160,16 @@ 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==} + 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==} @@ -1991,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 @@ -2003,6 +2204,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} @@ -2049,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 @@ -2103,6 +2308,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,13 +2379,91 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@emnapi/core@1.7.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)': + dependencies: + '@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.6(effect@3.19.9))(effect@3.19.9)': + dependencies: + '@effect/platform': 0.93.6(effect@3.19.9) + effect: 3.19.9 + uuid: 11.1.0 + + '@effect/language-service@0.60.0': {} + + '@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.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.9 + multipasta: 0.2.7 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@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.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 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform@0.93.6(effect@3.19.9)': + dependencies: + 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.6(effect@3.19.9))(effect@3.19.9)': + dependencies: + '@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.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.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.9)(vitest@4.0.15(@types/node@22.18.12)(jiti@2.6.1)(yaml@2.8.1))': + dependencies: + 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.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.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.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 @@ -2194,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 @@ -2303,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 @@ -2331,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 @@ -2347,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 @@ -2382,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 @@ -2399,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 @@ -2429,10 +2713,28 @@ snapshots: dependencies: p-retry: 7.1.0 - '@napi-rs/wasm-runtime@1.0.7': + '@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.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 @@ -2448,59 +2750,118 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@oxc-project/types@0.96.0': {} + '@oxc-project/runtime@0.101.0': {} - '@pkgr/core@0.2.9': {} + '@oxc-project/types@0.101.0': {} - '@quansync/fs@0.1.5': - dependencies: - quansync: 0.2.11 + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.46': + '@parcel/watcher-darwin-x64@2.5.1': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.46': + '@parcel/watcher-freebsd-x64@2.5.1': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.46': + '@parcel/watcher-linux-arm-glibc@2.5.1': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.46': + '@parcel/watcher-linux-arm-musl@2.5.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.46': + '@parcel/watcher-linux-arm64-glibc@2.5.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.46': + '@parcel/watcher-linux-arm64-musl@2.5.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.46': + '@parcel/watcher-linux-x64-glibc@2.5.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.46': + '@parcel/watcher-linux-x64-musl@2.5.1': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.46': + '@parcel/watcher-win32-arm64@2.5.1': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.46': + '@parcel/watcher-win32-ia32@2.5.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.46': + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + 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@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.46': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.46': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.46': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + dependencies: + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@rolldown/pluginutils@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.53': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.52.5': optional: true @@ -2621,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 @@ -2638,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 @@ -2668,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) @@ -2698,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) @@ -2734,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)) @@ -2756,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': @@ -2873,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 @@ -2882,7 +3191,7 @@ snapshots: baseline-browser-mapping@2.8.24: {} - birpc@2.7.0: {} + birpc@3.0.0: {} boolbase@1.0.0: {} @@ -2917,7 +3226,7 @@ snapshots: ccount@2.0.1: {} - chai@6.2.0: {} + chai@6.2.1: {} chalk@4.1.2: dependencies: @@ -2928,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: @@ -2978,19 +3283,25 @@ snapshots: deep-is@0.1.4: {} - defu@6.1.4: {} - 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 diff-sequences@27.5.1: {} - diff@8.0.2: {} + dts-resolver@2.1.3: {} - dts-resolver@2.1.2: {} + effect@3.19.9: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 electron-to-chromium@1.5.245: {} @@ -3095,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 @@ -3185,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 @@ -3197,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) @@ -3225,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: @@ -3324,7 +3636,7 @@ snapshots: esutils@2.0.3: {} - eta@4.0.1: {} + eta@4.4.1: {} expect-type@1.2.2: {} @@ -3334,6 +3646,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 +3684,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: @@ -3405,7 +3723,7 @@ snapshots: globals@15.15.0: {} - globals@16.4.0: {} + globals@16.5.0: {} globrex@0.1.2: {} @@ -3428,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: {} @@ -3448,7 +3768,8 @@ snapshots: isexe@2.0.0: {} - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-yaml@4.1.0: dependencies: @@ -3460,8 +3781,6 @@ snapshots: jsdoc-type-pratt-parser@6.10.0: {} - jsesc@3.0.2: {} - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -3483,6 +3802,8 @@ snapshots: kleur@3.0.3: {} + kubernetes-types@1.30.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3828,6 +4149,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 +4170,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: @@ -3861,6 +4209,8 @@ snapshots: object-deep-merge@2.0.0: {} + obug@2.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3946,11 +4296,13 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + 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: @@ -3963,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: {} @@ -3975,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: @@ -4098,8 +4448,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -4133,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: @@ -4169,15 +4517,15 @@ 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: {} + undici@7.16.0: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -4197,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 @@ -4209,6 +4564,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 @@ -4223,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 @@ -4262,7 +4618,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml @@ -4290,6 +4645,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..3f8c99c --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,47 @@ +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 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; +}> {} + +export class BranchNotFoundError extends Data.TaggedError("BranchNotFoundError")<{ + readonly branchName: string; +}> {} + +export class WorkspaceError extends Data.TaggedError("WorkspaceError")<{ + message: string; + operation?: string; + cause?: unknown; +}> { } + +export class GitHubError extends Data.TaggedError("GitHubError")<{ + message: string; + operation?: "getPullRequestByBranch" | "createPullRequest" | "updatePullRequest" | "setCommitStatus" | "request"; + 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 11b3e8c..ee6a4e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,142 @@ -export { - publish, - type PublishOptions, -} from "#publish"; -export { - release, - type ReleaseOptions, - type ReleaseResult, -} from "#release"; -export { - verify, - type VerifyOptions, -} from "#verify"; +import type { ReleaseScriptsOptionsInput } 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 { 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 { normalizeReleaseScriptsOptions, ReleaseScriptsOptions } from "./options"; +import { + loadOverrides, + mergeCommitsAffectingGloballyIntoPackage, + mergePackageCommitsIntoPackages, +} from "./utils/helpers"; +import { constructVerifyProgram } from "./verify"; + +export interface ReleaseScripts { + verify: () => Promise; + prepare: () => Promise; + publish: () => Promise; + packages: { + list: () => Promise; + get: (packageName: string) => Promise; + }; +} + +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 => { + const provided = program.pipe(Effect.provide(AppLayer)); + return Effect.runPromise(provided as Effect.Effect); + }; + + const safeguardProgram = Effect.gen(function* () { + const git = yield* GitService; + return yield* git.workspace.assertWorkspaceReady; + }); + + 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; + + 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", + }); + + yield* Console.log("Loaded overrides:", overrides); + + const packages = (yield* workspace.discoverWorkspacePackages.pipe( + Effect.flatMap(mergePackageCommitsIntoPackages), + Effect.flatMap((pkgs) => 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); + + yield* Console.log("Calculated releases:", releases); + yield* Console.log("Release order:", ordered); + + yield* packageUpdater.applyReleases(packages, releases); + }); + + 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.discoverWorkspacePackages; + }); + + 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/options.ts b/src/options.ts new file mode 100644 index 0000000..0831dbe --- /dev/null +++ b/src/options.ts @@ -0,0 +1,119 @@ +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 FindWorkspacePackagesOptions { + exclude?: string[]; + include?: string[]; + excludePrivate?: boolean; +} + +export interface ReleaseScriptsOptionsInput { + dryRun?: boolean; + repo: `${string}/${string}`; + workspaceRoot?: string; + packages?: true | FindWorkspacePackagesOptions | 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}}`; +const DEFAULT_TYPES = { + feat: { title: "🚀 Features" }, + fix: { title: "🐞 Bug Fixes" }, + refactor: { title: "🔧 Code Refactoring" }, + perf: { title: "🏎 Performance" }, + docs: { title: "📚 Documentation" }, + style: { title: "🎨 Styles" }, +}; + +export function normalizeReleaseScriptsOptions(options: ReleaseScriptsOptionsInput): NormalizedReleaseScriptsOptions { + const { + workspaceRoot = process.cwd(), + githubToken = "", + repo: fullRepo, + packages = true, + branch = {}, + globalCommitMode = "dependencies", + pullRequest = {}, + changelog = {}, + types = {}, + 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: options.types ? { ...DEFAULT_TYPES, ...types } : DEFAULT_TYPES, + }; +} + +export class ReleaseScriptsOptions extends Context.Tag("@ucdjs/release-scripts/ReleaseScriptsOptions")< + ReleaseScriptsOptions, + NormalizedReleaseScriptsOptions +>() { } 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/dependency-graph.service.ts b/src/services/dependency-graph.service.ts new file mode 100644 index 0000000..890973a --- /dev/null +++ b/src/services/dependency-graph.service.ts @@ -0,0 +1,100 @@ +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", + { + 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); + } + } + + let queueIndex = 0; + const ordered: PackageUpdateOrder[] = []; + + while (queueIndex < queue.length) { + const current = queue[queueIndex++]!; + 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) { + const processed = new Set(ordered.map((o) => 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; + }); + } + + return { + topologicalOrder, + } as const; + }), + dependencies: [], + }, +) {} diff --git a/src/services/git.service.ts b/src/services/git.service.ts new file mode 100644 index 0000000..b4b7dd4 --- /dev/null +++ b/src/services/git.service.ts @@ -0,0 +1,225 @@ +import { Command, CommandExecutor } from "@effect/platform"; +import { NodeCommandExecutor } from "@effect/platform-node"; +import * as CommitParser from "commit-parser"; +import { Effect } from "effect"; +import { ExternalCommitParserError, GitCommandError } from "../errors"; +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* ReleaseScriptsOptions; + + const execGitCommand = (args: readonly string[]) => + executor.string(Command.make("git", ...args).pipe( + Command.workingDirectory(config.workspaceRoot), + )).pipe( + Effect.mapError((err) => { + return new GitCommandError({ + command: `git ${args.join(" ")}`, + stderr: err.message, + }); + }), + ); + + // 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.gen(function* () { + const output = yield* execGitCommand(["branch", "--list"]); + return output + .trim() + .split("\n") + .filter((line) => line.length > 0) + .map((line) => line.replace(/^\* /, "").trim()) + .map((line) => line.trim()); + }); + + const isWorkingDirectoryClean = Effect.gen(function* () { + const status = yield* execGitCommand(["status", "--porcelain"]); + return status.trim().length === 0; + }); + + function doesBranchExist(branch: string) { + return listBranches.pipe( + Effect.map((branches) => branches.includes(branch)), + ); + } + + function createBranch(branch: string, base: string = config.branch.default) { + return execGitCommandIfNotDry(["branch", branch, base]); + } + + const getBranch = Effect.gen(function* () { + const output = yield* execGitCommand(["rev-parse", "--abbrev-ref", "HEAD"]); + return output.trim(); + }); + + function checkoutBranch(branch: string) { + return execGitCommand(["checkout", branch]); + } + + 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* execGitCommandIfNotDry(["add", ...files]); + }); + } + + function writeCommit(message: string) { + return execGitCommandIfNotDry(["commit", "-m", message]); + } + + function pushChanges(branch: string, remote: string = "origin") { + return execGitCommandIfNotDry(["push", remote, branch]); + } + + function readFile(filePath: string, ref: string = "HEAD") { + return execGitCommand(["show", `${ref}:${filePath}`]); + } + + function getMostRecentPackageTag(packageName: string) { + return execGitCommand(["tag", "--list", "--sort=-version:refname", `${packageName}@*`]).pipe( + Effect.map((tags) => { + const tagList = tags + .trim() + .split("\n") + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); + + return tagList[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(), + })), + ); + }), + ); + } + + 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; + }), + ); + } + + 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, + exists: doesBranchExist, + create: createBranch, + checkout: checkoutBranch, + get: getBranch, + }, + commits: { + stage: stageChanges, + write: writeCommit, + push: pushChanges, + get: getCommits, + filesChangesBetweenRefs, + }, + tags: { + mostRecentForPackage: getMostRecentPackageTag, + }, + workspace: { + readFile, + isWithinRepository, + isWorkingDirectoryClean, + assertWorkspaceReady, + }, + } 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..c585e97 --- /dev/null +++ b/src/services/github.service.ts @@ -0,0 +1,122 @@ +import { Effect, Schema } from "effect"; +import { GitHubError } from "../errors"; +import { ReleaseScriptsOptions } from "../options"; + +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.NullOr(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, +}); + +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 config = yield* ReleaseScriptsOptions; + + function makeRequest(endpoint: string, schema: Schema.Schema, options: RequestInit = {}) { + const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`; + return Effect.tryPromise({ + try: async () => { + const res = await fetch(url, { + ...options, + headers: { + "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, + }, + }); + + 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}`; + 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, + operation: "getPullRequestByBranch", + cause: e.cause, + })), + ); + } + + return { + getPullRequestByBranch, + } 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..5112a9a --- /dev/null +++ b/src/services/package-updater.service.ts @@ -0,0 +1,158 @@ +import type { WorkspacePackage } from "./workspace.service"; +import { Effect } from "effect"; +import semver from "semver"; +import { WorkspaceService } from "./workspace.service"; + +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; + + if (raw === "*" || raw === "latest") { + 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}`; +} + +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 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 workspace = yield* WorkspaceService; + + 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* workspace.writePackageJson(pkg.path, nextJson).pipe( + Effect.map(() => "written" as const), + ); + }), + ), + ); + } + + return { + applyReleases, + } as const; + }), + dependencies: [ + WorkspaceService.Default, + ], + }, +) {} diff --git a/src/services/version-calculator.service.ts b/src/services/version-calculator.service.ts new file mode 100644 index 0000000..8425fc1 --- /dev/null +++ b/src/services/version-calculator.service.ts @@ -0,0 +1,102 @@ +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"; + +const BUMP_PRIORITY: Record = { + none: 0, + patch: 1, + minor: 2, + major: 3, +}; + +function maxBump(current: BumpKind, incoming: BumpKind): BumpKind { + 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 { + 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: 10 }, + ); + } + + return { + calculateBumps, + } as const; + }), + dependencies: [], + }, +) {} diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts new file mode 100644 index 0000000..93740f9 --- /dev/null +++ b/src/services/workspace.service.ts @@ -0,0 +1,265 @@ +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 { ReleaseScriptsOptions } from "../options"; + +export const DependencyObjectSchema = Schema.Record({ + key: Schema.String, + value: Schema.String, +}); + +export const PackageJsonSchema = Schema.Struct({ + name: Schema.String, + private: Schema.optional(Schema.Boolean), + version: Schema.optional(Schema.String), + dependencies: Schema.optional(DependencyObjectSchema), + devDependencies: Schema.optional(DependencyObjectSchema), + peerDependencies: Schema.optional(DependencyObjectSchema), +}); + +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* ReleaseScriptsOptions; + + 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), + 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, + ); + + 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", + }), + ), + )), + ); + } + + 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; + + // 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, + ); + + // 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)); + + return Effect.all( + rawProjects.map((rawProject) => + readPackageJson(rawProject.path).pipe( + Effect.flatMap((packageJson) => { + if (!shouldIncludePackage(packageJson, options)) { + 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(() => { + return Effect.logWarning(`Skipping invalid package ${rawProject.name}`).pipe( + Effect.as(null), + ); + }), + ), + ), + ).pipe( + Effect.map((packages) => + packages.filter( + (pkg): pkg is WorkspacePackage => pkg !== null, + ), + ), + ); + }), + ); + } + + 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 { + readPackageJson, + writePackageJson, + findWorkspacePackages, + discoverWorkspacePackages, + findPackageByName, + } 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/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/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/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..9637c6d --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,298 @@ +import type { WorkspacePackage } from "#services/workspace"; +import type * as CommitParser from "commit-parser"; +import { GitService } from "#services/git"; +import { WorkspacePackageSchema } from "#services/workspace"; +import { Effect, Schema } from "effect"; +import { OverridesLoadError } from "../errors"; + +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.workspace.readFile(options.overridesPath, options.sha).pipe( + Effect.flatMap((content) => + Effect.try({ + try: () => JSON.parse(content) as VersionOverrides, + catch: (err) => new OverridesLoadError({ + message: "Failed to parse overrides file.", + cause: err, + }), + }), + ), + 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?.name || 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.hash, 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.sha) ?? 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.hash); + if (commitTimestamp == null || commitTimestamp <= cutoffTimestamp) { + continue; // Skip commits at or before the package's last release + } + + const files = affectedFilesPerCommit.get(commit.hash); + 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 yield* 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 + */ +export 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.) + */ +export 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 + */ +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) { + 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.hash, newestCommit.hash]; +} diff --git a/src/verify.ts b/src/verify.ts index 0c2c3f3..5a72218 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -1,146 +1,64 @@ -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 type { NormalizedReleaseScriptsOptions } from "./options"; +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 { - 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."); + 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.`)); } - } 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, - ); + yield* Console.log(`✅ Release pull request #${releasePullRequest.number} exists.`); - const { allUpdates: expectedUpdates } = await calculateAndPrepareVersionUpdates({ - workspacePackages: mainPackages, - packageCommits: mainCommits, - workspaceRoot, - showPrompt: false, - globalCommitsPerPackage, - overrides: existingOverrides, - }); + 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 expectedVersionMap = new Map( - expectedUpdates.map((u) => [u.package.name, u.newVersion]), - ); + const overrides = yield* loadOverrides({ + sha: releasePullRequest.head.sha, + overridesPath: ".github/ucdjs-release.overrides.json", + }); - // 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); - } - } + yield* Console.log("Loaded overrides:", overrides); - if (originalBranch !== defaultBranch) { - await checkoutBranch(originalBranch, workspaceRoot); - } + const packages = (yield* workspace.discoverWorkspacePackages.pipe( + Effect.flatMap(mergePackageCommitsIntoPackages), + Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)), + )); - 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; - } + yield* Console.log("Discovered packages with commits and global commits:", packages); - 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 releases = yield* versionCalculator.calculateBumps(packages, overrides); + const ordered = yield* dependencyGraph.topologicalOrder(packages); - const statusContext = "ucdjs/release-verify"; + yield* Console.log("Calculated releases:", releases); + yield* Console.log("Release order:", ordered); - 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'."); - } + // 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/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/helpers.test.ts b/test/helpers.test.ts new file mode 100644 index 0000000..b1e14c3 --- /dev/null +++ b/test/helpers.test.ts @@ -0,0 +1,283 @@ +import type { GitCommit } from "commit-parser"; +import type { WorkspacePackageWithCommits } from "../src/utils/helpers"; +import { describe, expect, it } from "vitest"; +import { + findCommitRange, + isDependencyFile, + isGlobalCommit, +} from "../src/utils/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/test/options.test.ts b/test/options.test.ts new file mode 100644 index 0000000..1705079 --- /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/" })).toThrow(); + expect(() => normalizeReleaseScriptsOptions({ githubToken: "token", repo: "/invalid" })).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/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"]