diff --git a/.changeset/thick-monkeys-tan.md b/.changeset/thick-monkeys-tan.md new file mode 100644 index 0000000..9ef93b6 --- /dev/null +++ b/.changeset/thick-monkeys-tan.md @@ -0,0 +1,5 @@ +--- +"@cadamsdev/lazy-changesets": feat +--- + +Added version command to bump the version in the package.json files based on the changesets diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e900192 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,176 @@ +# Lazy Changesets - Agent Guidelines + +## Essential Commands + +### Build & Test +- `npm run build` - Compile TypeScript to dist/ using tsconfig.build.json +- `npm test` - Run all Vitest tests (watch mode) +- `npm test -- --run` - Run tests once +- `npm test -- --run src/version.test.ts` - Run single test file +- `npm test -- --run -t "test name"` - Run tests matching pattern +- `npm test:coverage` - Run tests with coverage report + +### Development +- `npm start` - Run the CLI directly using tsx + +## Code Style Guidelines + +### Imports +- Use ES module imports: `import { foo } from 'bar'` +- Use `.js` extensions for local imports: `import { foo } from './config.js'` +- Use `node:` prefix for Node.js built-ins: `import { readFileSync } from 'node:fs'` +- Group imports in order: built-ins, external deps, local deps +- Type imports should use `import type`: `import type { Foo } from './bar.js'` + +### TypeScript & Types +- Strict TypeScript enabled - always provide explicit types +- Use `interface` for object shapes, `type` for unions/primitives +- Export functions that need testing: `export function foo()` +- Use template literals for type indexing: `ChangesetReleaseType['type']` +- Default parameters: `async function foo({ dryRun = false } = {})` + +### Naming Conventions +- camelCase for functions, variables, and object properties +- PascalCase for types, interfaces, classes, and enum members +- UPPER_SNAKE_CASE for constants (rare - prefer descriptive names) +- File names: kebab-case for utilities, PascalCase for components (if any) +- Test files: `.test.ts` alongside source files + +### File Structure +- `src/*.ts` - Source code +- `src/*.test.ts` - Unit tests +- `dist/*.js` - Compiled output (generated) +- `.changeset/*.md` - Changeset files + +### Error Handling +- Throw `new Error()` with descriptive messages for programming errors +- Use `console.error()` with `pc.red()` for user-facing errors +- Use `console.warn()` for warnings +- Use `console.log()` with colored output (`pc.green()`, `pc.yellow()`) for success/info +- Exit with `process.exit(1)` on error, `process.exit(0)` on success +- Validate inputs early with guard clauses + +### CLI Pattern (citty) +- Use `defineCommand()` to define main CLI entry +- Define `subCommands` for nested commands (init, version, etc.) +- Define `args` for command options +- Use `runMain()` to execute command +- Call `process.exit(0)` explicitly in subCommands to prevent prompt rendering + +### Testing Patterns (Vitest) +- Mock external dependencies: `vi.mock('fs')`, `vi.mock('tinyglobby')` +- Use `vi.mocked()` for typed mocks: `vi.mocked(readFileSync).mockReturnValue()` +- Mock config: `vi.mock('./config.js', () => ({ readConfig: vi.fn(...) }))` +- Use `beforeEach`/`afterEach` for setup/teardown +- Spy on console: `vi.spyOn(console, 'log').mockImplementation(() => {})` +- Test grouping: `describe('functionName', () => { ... })` +- Test descriptions: `it('should do X when Y', () => { ... })` + +### File Operations +- Check existence first: `if (!existsSync(path)) { ... }` +- Use `path.join()` for path construction +- Write with encoding: `writeFileSync(path, content, { encoding: 'utf-8' })` +- JSON stringify: `JSON.stringify(obj, null, 2) + '\n'` + +### Console Output +- Use `picocolors` (`pc`) for colored terminal output +- Structure: `console.log(pc.green('✔'), pc.cyan('name'), pc.dim('(info)'))` +- Keep messages concise and user-friendly +- Use emoji sparingly and appropriately + +### General Principles +- No code comments (unless absolutely necessary) +- Keep functions small and focused +- Use Map/Set over Object when appropriate +- Process files in loops with explicit continue for error cases +- Use `Array.from()` to convert collections +- String interpolation with template literals +- Early returns over nested conditionals + +### Async/Await Patterns +- Always use `async`/`await` for async operations +- Return Promises from async functions +- Handle errors with try/catch in async functions +- Use `Promise.all()` for parallel async operations when safe + +### Map/Set Usage +- Prefer Map for key-value pairs with arbitrary keys +- Prefer Set for unique value collections +- Use `Array.from(map.values())` or `[...map.values()]` to convert to arrays +- Use `map.has()`, `map.get()`, `map.set()` methods +- Use `set.has()`, `set.add()`, `set.delete()` methods + +### String & Template Literals +- Use template literals for string interpolation: `\`Hello ${name}\`` +- Use template literals for multi-line strings +- Prefer string methods over regex for simple operations +- Use regex with `match()` for complex pattern matching + +### Array Operations +- Use array methods: `map()`, `filter()`, `find()`, `some()`, `every()` +- Use `for...of` loops when you need `break` or `continue` +- Use `Array.from()` to convert Map/Set/NodeList to arrays +- Use spread operator: `[...array]` for copying + +### Object & JSON +- Use interfaces for object shapes +- Use `JSON.stringify(obj, null, 2)` for pretty-printed JSON +- Use `JSON.parse()` with try/catch for parsing +- Destructure objects: `const { name, version } = packageJson` + +### Path Handling +- Always use `path.join()` for cross-platform paths +- Use `path.resolve()` for absolute paths +- Use `path.dirname()` to get directory path +- Use `path.basename()` to get filename + +### Package Management +- Use `npm install` to add dependencies +- Use `npm install --save-dev` for dev dependencies +- Check package.json for existing dependencies before adding new ones +- Update package.json version when making breaking changes + +### Git Workflow +- Create changeset files for all changes +- Run `npm run build` before committing +- Run `npm test -- --run` to verify tests pass +- Include changeset file in commits +- Delete consumed changeset files after version bump + +### CLI Argument Handling +- Use citty's `defineCommand()` for command structure +- Define args with type, description, required, default +- Access args via `({ args }) => { ... }` +- Use subCommands for nested CLI structure +- Call `process.exit(0)` to prevent interactive prompts after subCommands + +### Version Bumping Logic +- Parse changeset frontmatter for release types +- `feat` type = minor version bump +- `!` suffix = major version bump +- All other types = patch version bump +- Take highest bump when multiple changesets exist for same package +- Delete changeset files after successful version bump + +### File System Patterns +- Use `existsSync()` before file operations (when appropriate) +- Use `readFileSync()` with encoding: 'utf-8' +- Use `writeFileSync()` with encoding: 'utf-8' +- Use `mkdirSync()` for directory creation +- Use `unlinkSync()` for file deletion +- Use glob patterns for finding files + +### Color Console Output +- Import: `import pc from 'picocolors'` +- Success: `pc.green()`, `pc.cyan()`, `pc.green('✔')` +- Warning: `pc.yellow()` +- Error: `pc.red()` +- Dim/Info: `pc.dim()` +- Bold: `pc.bold()` + +### Type Safety +- Never use `any` type +- Use `unknown` when type is truly unknown +- Use type guards for narrowing types +- Use `as` sparingly and only when certain +- Use template literal types for dynamic keys: `T['key']` diff --git a/package-lock.json b/package-lock.json index 0b1ed27..aa309eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@clack/prompts": "^0.10.1", "citty": "^0.1.6", "human-id": "^4.1.1", + "package-manager-detector": "^1.6.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.13" }, @@ -20,7 +21,8 @@ }, "devDependencies": { "@types/node": "^22.15.15", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^4.0.16" } }, "node_modules/@clack/core": { @@ -44,16 +46,979 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.15.tgz", "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -72,11 +1037,83 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -86,6 +1123,21 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/human-id": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", @@ -95,6 +1147,59 @@ "human-id": "dist/cli.js" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -102,9 +1207,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "peer": true, "engines": { @@ -114,20 +1219,142 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -136,6 +1363,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -156,6 +1393,177 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "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": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index a3c60d1..3dbfd1d 100644 --- a/package.json +++ b/package.json @@ -8,19 +8,22 @@ }, "scripts": { "start": "npx tsx src/index.ts", - "build": "tsc -p tsconfig.build.json" + "build": "tsc -p tsconfig.build.json", + "test": "vitest" }, "author": "", "license": "ISC", "description": "", "devDependencies": { "@types/node": "^22.15.15", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^4.0.16" }, "dependencies": { "@clack/prompts": "^0.10.1", "citty": "^0.1.6", "human-id": "^4.1.1", + "package-manager-detector": "^1.6.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.13" } diff --git a/src/index.ts b/src/index.ts index a34bfda..f7c8229 100755 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import path from 'path'; import { humanId } from 'human-id'; import pc from 'picocolors'; import { ChangesetConfig, readConfig } from './config.js'; +import { version } from './version.js'; async function findPackages(config: ChangesetConfig): Promise> { const packageJsonPaths = globSync({ @@ -73,6 +74,132 @@ async function getSelectedPackages( return selectedPackages; } +async function createChangeset(args: { empty?: boolean }) { + const config = readConfig(); + + if (args.empty) { + const changesetDir = path.join(process.cwd(), '.changeset'); + + if (!existsSync(changesetDir)) { + mkdirSync(changesetDir); + } + + const changesetID = humanId({ + separator: '-', + capitalize: false, + }); + + const changesetFileName = `${changesetID}.md`; + const changesetFilePath = path.join(changesetDir, changesetFileName); + const markdownContent = '---\n---\n\n'; + writeFileSync(changesetFilePath, markdownContent, { + encoding: 'utf-8', + }); + + console.log( + pc.green('Empty Changeset added! - you can now commit it\n') + ); + console.log( + pc.green( + 'If you want to modify or expand on the changeset summary, you can find it here' + ) + ); + console.log(pc.cyan('info'), pc.blue(changesetFilePath)); + return; + } + + const packages = await findPackages(config); + + if (packages.size === 0) { + console.log('No packages found.'); + return; + } + + const selectedPackages = await getSelectedPackages(packages); + if (selectedPackages.length === 0) { + console.log('No packages selected.'); + return; + } + + const sortedTypeKeys = Object.keys(config.lazyChangesets.types).sort((a, b) => { + return config.lazyChangesets.types[a].sort - config.lazyChangesets.types[b].sort; + }); + + const msgType = await select({ + message: 'Select changelog type', + options: sortedTypeKeys.map(key => { + const type = config.lazyChangesets.types[key]; + return { + value: key, + label: `${type.emoji} ${key}`, + hint: type.displayName, + }; + }), + }); + + if (isCancel(msgType)) { + cancel('Operation cancelled.'); + process.exit(0); + } + + const changesetType = config.lazyChangesets.types[msgType]; + let isBreakingChange = false; + + if (changesetType.promptBreakingChange) { + const tempIsBreakingChange = await confirm({ + message: 'Is this a breaking change?', + initialValue: false, + }); + + if (isCancel(tempIsBreakingChange)) { + cancel('Operation cancelled.'); + process.exit(0); + } + + isBreakingChange = tempIsBreakingChange; + } + + const msg = await text({ + message: 'Enter a message for the changeset', + placeholder: 'e.g Added x feature', + validate(value) { + if (value.length === 0) return 'Message cannot be empty.'; + }, + }); + + if (isCancel(msg)) { + cancel('Operation cancelled.'); + process.exit(0); + } + + const changesetDir = path.join(process.cwd(), '.changeset'); + + if (!existsSync(changesetDir)) { + mkdirSync(changesetDir); + } + + const changesetID = humanId({ + separator: '-', + capitalize: false, + }); + + const changesetFileName = `${changesetID}.md`; + const changesetFilePath = path.join(changesetDir, changesetFileName); + let changesetContent = '---\n'; + selectedPackages.forEach((pkg) => { + changesetContent += `"${pkg}": ${msgType.toString()}${ + isBreakingChange ? '!' : '' + }\n`; + }); + + changesetContent += '---\n\n'; + changesetContent += `${msg.toString()}\n`; + + writeFileSync(changesetFilePath, changesetContent, { + encoding: 'utf-8', + }); +} + (async () => { try { const main = defineCommand({ @@ -80,12 +207,44 @@ async function getSelectedPackages( name: 'lazy-changesets', description: 'A CLI tool for generating changesets.', }, - args: { + subCommands: { init: { - type: 'positional', - description: 'Initialize changesets', - required: false, + meta: { + name: 'init', + description: 'Initialize changesets', + }, + args: {}, + run: async () => { + await init(); + process.exit(0); + }, + }, + version: { + meta: { + name: 'version', + description: 'Bump package versions based on changesets', + }, + args: { + 'dry-run': { + type: 'boolean', + description: 'Show what would be changed without modifying files', + required: false, + default: false, + }, + install: { + type: 'boolean', + description: 'Run package manager install after version bump', + required: false, + default: false, + }, + }, + run: async ({ args }) => { + await version({ dryRun: args['dry-run'], install: args.install }); + process.exit(0); + }, }, + }, + args: { empty: { type: 'boolean', description: 'Create an empty changeset', @@ -94,136 +253,7 @@ async function getSelectedPackages( }, }, run: async ({ args }) => { - if (args.init) { - await init(); - return; - } - - const config = readConfig(); - - if (args.empty) { - const changesetDir = path.join(process.cwd(), '.changeset'); - - if (!existsSync(changesetDir)) { - mkdirSync(changesetDir); - } - - const changesetID = humanId({ - separator: '-', - capitalize: false, - }); - - const changesetFileName = `${changesetID}.md`; - const changesetFilePath = path.join(changesetDir, changesetFileName); - const markdownContent = '---\n---\n\n'; - writeFileSync(changesetFilePath, markdownContent, { - encoding: 'utf-8', - }); - - console.log( - pc.green('Empty Changeset added! - you can now commit it\n') - ); - console.log( - pc.green( - 'If you want to modify or expand on the changeset summary, you can find it here' - ) - ); - console.log(pc.cyan('info'), pc.blue(changesetFilePath)); - return; - } - - const packages = await findPackages(config); - - if (packages.size === 0) { - console.log('No packages found.'); - return; - } - - const selectedPackages = await getSelectedPackages(packages); - if (selectedPackages.length === 0) { - console.log('No packages selected.'); - return; - } - - const sortedTypeKeys = Object.keys(config.lazyChangesets.types).sort((a, b) => { - return config.lazyChangesets.types[a].sort - config.lazyChangesets.types[b].sort; - }); - - const msgType = await select({ - message: 'Select changelog type', - options: sortedTypeKeys.map(key => { - const type = config.lazyChangesets.types[key]; - return { - value: key, - label: `${type.emoji} ${key}`, - hint: type.displayName, - }; - }), - }); - - if (isCancel(msgType)) { - cancel('Operation cancelled.'); - process.exit(0); - } - - const changesetType = config.lazyChangesets.types[msgType]; - let isBreakingChange = false; - - if (changesetType.promptBreakingChange) { - const tempIsBreakingChange = await confirm({ - message: 'Is this a breaking change?', - initialValue: false, - }); - - if (isCancel(tempIsBreakingChange)) { - cancel('Operation cancelled.'); - process.exit(0); - } - - isBreakingChange = tempIsBreakingChange; - } - - const msg = await text({ - message: 'Enter a message for the changeset', - placeholder: 'e.g Added x feature', - validate(value) { - if (value.length === 0) return 'Message cannot be empty.'; - }, - }); - - if (isCancel(msg)) { - cancel('Operation cancelled.'); - process.exit(0); - } - - const changesetDir = path.join(process.cwd(), '.changeset'); - - // create the changeset directory if it doesn't exist - if (!existsSync(changesetDir)) { - mkdirSync(changesetDir); - } - - const changesetID = humanId({ - separator: '-', - capitalize: false, - }); - - const changesetFileName = `${changesetID}.md`; - const changesetFilePath = path.join(changesetDir, changesetFileName); - let changesetContent = '---\n'; - selectedPackages.forEach((pkg) => { - changesetContent += `"${pkg}": ${msgType.toString()}${ - isBreakingChange ? '!' : '' - }\n`; - }); - - changesetContent += '---\n\n'; - changesetContent += `${msg.toString()}\n`; - - // write the changelog to the changeset file - writeFileSync(changesetFilePath, changesetContent, { - encoding: 'utf-8', - }); + await createChangeset({ empty: args.empty }); }, }); diff --git a/src/version.test.ts b/src/version.test.ts new file mode 100644 index 0000000..e504323 --- /dev/null +++ b/src/version.test.ts @@ -0,0 +1,799 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs'; +import path from 'path'; +import { globSync } from 'tinyglobby'; +import { execSync } from 'node:child_process'; +import { detect } from 'package-manager-detector'; +import { readConfig } from './config.js'; +import { + parseChangesetFile, + getHighestReleaseType, + bumpVersion, + version, + type ChangesetReleaseType +} from './version.js'; + +vi.mock('fs'); +vi.mock('tinyglobby'); +vi.mock('node:child_process'); +vi.mock('package-manager-detector'); +vi.mock('./config.js', () => ({ + readConfig: vi.fn(() => ({ + access: 'restricted', + baseBranch: 'main', + updateInternalDependencies: 'patch', + ignore: [], + lazyChangesets: { + types: { + feat: { + displayName: 'New Features', + emoji: '🚀', + sort: 0, + releaseType: 'minor', + promptBreakingChange: true, + }, + }, + }, + })), +})); + +describe('parseChangesetFile', () => { + it('should parse a simple changeset file with feat type', () => { + const content = `--- +"@test/package": feat +--- + +Added new feature`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'minor', packageName: '@test/package' } + ]); + }); + + it('should parse a changeset file with breaking change', () => { + const content = `--- +"@test/package": feat! +--- + +Breaking change added`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'major', packageName: '@test/package' } + ]); + }); + + it('should parse a changeset file with fix type', () => { + const content = `--- +"@test/package": fix +--- + +Bug fix`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'patch', packageName: '@test/package' } + ]); + }); + + it('should parse a changeset file with multiple packages', () => { + const content = `--- +"@test/package": feat +"@other/package": fix +--- + +Multiple packages updated`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'minor', packageName: '@test/package' }, + { type: 'patch', packageName: '@other/package' } + ]); + }); + + it('should parse a changeset file with malformed lines', () => { + const content = `--- +invalid line +"@test/package": feat +another invalid line +--- + +Test`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'minor', packageName: '@test/package' } + ]); + }); + + it('should parse a changeset file with multiple breaking changes', () => { + const content = `--- +"@test/package": feat! +"@other/package": fix! +--- + +Multiple breaking changes`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([ + { type: 'major', packageName: '@test/package' }, + { type: 'major', packageName: '@other/package' } + ]); + }); + + it('should return empty array for changeset without frontmatter', () => { + const content = `No frontmatter here`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([]); + }); + + it('should return empty array for changeset with empty frontmatter', () => { + const content = `--- +--- +`; + + vi.mocked(readFileSync).mockReturnValue(content); + vi.mocked(existsSync).mockReturnValue(true); + + const result = parseChangesetFile('.changeset/test.md'); + + expect(result).toEqual([]); + }); +}); + +describe('getHighestReleaseType', () => { + it('should return major when any release is major', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'major', packageName: '@test/package' }, + { type: 'patch', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('major'); + }); + + it('should return minor when no major but has minor', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'minor', packageName: '@test/package' }, + { type: 'patch', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('minor'); + }); + + it('should return patch when only patches', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'patch', packageName: '@test/package' }, + { type: 'patch', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('patch'); + }); + + it('should return patch for single patch', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'patch', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('patch'); + }); + + it('should return major for single major', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'major', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('major'); + }); + + it('should return minor for single minor', () => { + const releases: ChangesetReleaseType[] = [ + { type: 'minor', packageName: '@test/package' } + ]; + + expect(getHighestReleaseType(releases)).toBe('minor'); + }); +}); + +describe('bumpVersion', () => { + it('should bump major version correctly', () => { + expect(bumpVersion('1.0.0', 'major')).toBe('2.0.0'); + expect(bumpVersion('0.5.10', 'major')).toBe('1.0.0'); + }); + + it('should bump minor version correctly', () => { + expect(bumpVersion('1.0.0', 'minor')).toBe('1.1.0'); + expect(bumpVersion('1.5.10', 'minor')).toBe('1.6.0'); + }); + + it('should bump patch version correctly', () => { + expect(bumpVersion('1.0.0', 'patch')).toBe('1.0.1'); + expect(bumpVersion('1.5.10', 'patch')).toBe('1.5.11'); + }); + + it('should handle large version numbers', () => { + expect(bumpVersion('999.999.999', 'major')).toBe('1000.0.0'); + expect(bumpVersion('999.999.999', 'minor')).toBe('999.1000.0'); + expect(bumpVersion('999.999.999', 'patch')).toBe('999.999.1000'); + }); + + it('should throw error for invalid version format - missing parts', () => { + expect(() => bumpVersion('1.0', 'major')).toThrow('Invalid version format'); + }); + + it('should throw error for invalid version format - too many parts', () => { + expect(() => bumpVersion('1.0.0.0', 'major')).toThrow('Invalid version format'); + }); + + it('should throw error for invalid version format - non-numeric', () => { + expect(() => bumpVersion('a.b.c', 'major')).toThrow('Invalid version format'); + }); + + it('should throw error for invalid version format - mixed', () => { + expect(() => bumpVersion('1.b.0', 'major')).toThrow('Invalid version format'); + }); + + it('should handle zero versions', () => { + expect(bumpVersion('0.0.0', 'major')).toBe('1.0.0'); + expect(bumpVersion('0.0.0', 'minor')).toBe('0.1.0'); + expect(bumpVersion('0.0.0', 'patch')).toBe('0.0.1'); + }); +}); + +describe('version command', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.mocked(existsSync).mockImplementation((path: import('fs').PathLike) => { + const pathStr = typeof path === 'string' ? path : path.toString(); + if (pathStr.includes('.changeset')) return true; + return false; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should exit with error when .changeset directory does not exist', async () => { + vi.mocked(existsSync).mockImplementation((path: import('fs').PathLike) => { + const pathStr = typeof path === 'string' ? path : path.toString(); + return !pathStr.includes('.changeset'); + }); + + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('Process exited'); + }); + + try { + await version(); + } catch (e) { + expect((e as Error).message).toBe('Process exited'); + } + + expect(exitSpy).toHaveBeenCalledWith(1); + exitSpy.mockRestore(); + }); + + it('should log message when no changeset files found', async () => { + vi.mocked(globSync).mockReturnValue([]); + + await version(); + + expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No changeset files found')); + }); + + it('should log message when no package releases found', async () => { + vi.mocked(readFileSync).mockReturnValue(`--- +--- +`); + vi.mocked(globSync).mockReturnValue(['.changeset/empty.md']); + + await version(); + + expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No package releases found')); + }); + + it('should update package versions when changesets exist', async () => { + const changesetContent = `--- +"@test/package": feat +--- +New feature added`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + await version({ dryRun: true }); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('✔'), + expect.stringContaining('@test/package'), + expect.stringContaining('1.0.0 → 1.1.0') + ); + }); + + it('should delete changeset files after updating versions', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + await version({ dryRun: false }); + + expect(unlinkSync).toHaveBeenCalledWith('.changeset/test.md'); + }); + + it('should handle multiple packages in different changesets', async () => { + const changeset1Content = `--- +"@test/package": feat +--- +Feature for test package`; + + const changeset2Content = `--- +"@other/package": fix +--- +Fix for other package`; + + const packageJson1 = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + const packageJson2 = JSON.stringify({ + name: '@other/package', + version: '2.0.0', + }, null, 2); + + let readCallCount = 0; + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('test.md')) { + return changeset1Content; + } else if (pathStr.includes('other.md')) { + return changeset2Content; + } else if (pathStr.includes('test/package.json')) { + return packageJson1; + } else { + return packageJson2; + } + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['packages/test/package.json', 'packages/other/package.json']; + } + return ['.changeset/test.md', '.changeset/other.md']; + }); + + await version({ dryRun: true }); + + const logCalls = vi.mocked(console.log).mock.calls; + + expect(logCalls[0]).toEqual([ + expect.stringContaining('✔'), + expect.stringContaining('@test/package'), + expect.stringContaining('1.0.0 → 1.1.0') + ]); + expect(logCalls[1]).toEqual([ + expect.stringContaining('✔'), + expect.stringContaining('@other/package'), + expect.stringContaining('2.0.0 → 2.0.1') + ]); + }); + + it('should ignore specified changeset files', async () => { + const changesetContent = `--- +"@test/package": feat +--- +New feature added`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + let globCallCount = 0; + vi.mocked(globSync).mockImplementation((options) => { + globCallCount++; + if (globCallCount === 1) { + return ['.changeset/test.md', '.changeset/ignored.md']; + } + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + await version({ dryRun: true, ignore: ['ignored.md'] }); + + const logCalls = vi.mocked(console.log).mock.calls; + + expect(logCalls[0]).toEqual([ + expect.stringContaining('✔'), + expect.stringContaining('@test/package'), + expect.stringContaining('1.0.0 → 1.1.0') + ]); + }); + + it('should handle package.json without name', async () => { + const changesetContent = `--- +"@test/package": feat +--- +New feature added`; + + const packageJsonContent = JSON.stringify({ + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + await version({ dryRun: true }); + + expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Dry run - no files were modified')); + }); + + it('should handle package.json without matching changeset', async () => { + const changesetContent = `--- +"@other/package": feat +--- +New feature added`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + await version({ dryRun: true }); + + expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Dry run - no files were modified')); + }); + + describe('install flag', () => { + it('should run npm install when install flag is true', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'npm', agent: 'npm' }); + + await version({ dryRun: false, install: true }); + + expect(execSync).toHaveBeenCalledWith('npm install', { stdio: 'inherit' }); + }); + + it('should run pnpm install when pnpm is detected', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'pnpm', agent: 'pnpm' }); + + await version({ dryRun: false, install: true }); + + expect(execSync).toHaveBeenCalledWith('pnpm install', { stdio: 'inherit' }); + }); + + it('should run yarn install when yarn is detected', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'yarn', agent: 'yarn' }); + + await version({ dryRun: false, install: true }); + + expect(execSync).toHaveBeenCalledWith('yarn install', { stdio: 'inherit' }); + }); + + it('should run bun install when bun is detected', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'bun', agent: 'bun' }); + + await version({ dryRun: false, install: true }); + + expect(execSync).toHaveBeenCalledWith('bun install', { stdio: 'inherit' }); + }); + + it('should skip install when dryRun is true', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'npm', agent: 'npm' }); + + await version({ dryRun: true, install: true }); + + expect(execSync).not.toHaveBeenCalled(); + }); + + it('should skip install when no packages are updated', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@other/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(execSync).mockReturnValue(''); + + vi.mocked(detect).mockResolvedValue({ name: 'npm', agent: 'npm' }); + + await version({ dryRun: false, install: true }); + + expect(execSync).not.toHaveBeenCalled(); + }); + + it('should warn for unsupported package manager', async () => { + const changesetContent = `--- +"@test/package": patch +--- +Bug fix`; + + const packageJsonContent = JSON.stringify({ + name: '@test/package', + version: '1.0.0', + }, null, 2); + + vi.mocked(readFileSync).mockImplementation((filePath) => { + const pathStr = typeof filePath === 'string' ? filePath : filePath.toString(); + if (pathStr.includes('.changeset')) { + return changesetContent; + } + return packageJsonContent; + }); + + vi.mocked(globSync).mockImplementation((options) => { + if (options?.patterns?.[0].includes('package.json')) { + return ['package.json']; + } + return ['.changeset/test.md']; + }); + + vi.mocked(detect).mockResolvedValue({ name: 'deno', agent: 'deno' } as any); + + await version({ dryRun: false, install: true }); + + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Unsupported package manager')); + }); + }); +}); diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..6de08c6 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,190 @@ +import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'; +import { globSync } from 'tinyglobby'; +import path from 'path'; +import pc from 'picocolors'; +import { execSync } from 'node:child_process'; +import { detect } from 'package-manager-detector'; +import { readConfig } from './config.js'; + +export interface ChangesetReleaseType { + type: 'major' | 'minor' | 'patch'; + packageName: string; +} + +export function parseChangesetFile(filePath: string): ChangesetReleaseType[] { + const content = readFileSync(filePath, 'utf-8'); + const releases: ChangesetReleaseType[] = []; + + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + return releases; + } + + const frontmatter = frontmatterMatch[1]; + const lines = frontmatter.split('\n'); + + for (const line of lines) { + const match = line.match(/^"([^"]+)":\s*(\w+)(!?)/); + if (match) { + const packageName = match[1]; + const changesetType = match[2]; + const isBreaking = match[3] === '!'; + + let releaseType: ChangesetReleaseType['type'] = 'patch'; + + if (isBreaking) { + releaseType = 'major'; + } else if (changesetType === 'feat') { + releaseType = 'minor'; + } + + releases.push({ type: releaseType, packageName }); + } + } + + return releases; +} + +export function getHighestReleaseType(releases: ChangesetReleaseType[]): ChangesetReleaseType['type'] { + if (releases.some(r => r.type === 'major')) return 'major'; + if (releases.some(r => r.type === 'minor')) return 'minor'; + return 'patch'; +} + +export function bumpVersion(version: string, releaseType: ChangesetReleaseType['type']): string { + const parts = version.split('.').map(Number); + + if (parts.length !== 3 || parts.some(isNaN)) { + throw new Error(`Invalid version format: ${version}`); + } + + switch (releaseType) { + case 'major': + return `${parts[0] + 1}.0.0`; + case 'minor': + return `${parts[0]}.${parts[1] + 1}.0`; + case 'patch': + return `${parts[0]}.${parts[1]}.${parts[2] + 1}`; + } +} + +export async function version({ dryRun = false, ignore = [] as string[], install = false } = {}) { + const config = readConfig(); + const changesetDir = path.join(process.cwd(), '.changeset'); + + if (!existsSync(changesetDir)) { + console.error(pc.red('No .changeset directory found.')); + process.exit(1); + } + + const changesetFiles = globSync({ + patterns: ['.changeset/*.md'], + ignore: ['.changeset/README.md', ...ignore.map(i => `.changeset/${i}`)], + }); + + if (changesetFiles.length === 0) { + console.log(pc.yellow('No changeset files found.')); + return; + } + + const packageReleases: Map = new Map(); + + for (const changesetFile of changesetFiles) { + const releases = parseChangesetFile(changesetFile); + for (const release of releases) { + const existing = packageReleases.get(release.packageName) || []; + packageReleases.set(release.packageName, [...existing, release]); + } + } + + if (packageReleases.size === 0) { + console.log(pc.yellow('No package releases found in changeset files.')); + return; + } + + const packageJsonPaths = globSync({ + patterns: ['**/package.json', '!**/node_modules/**', '!**/dist/**'], + }); + + const updatedPackages: string[] = []; + + for (const packageJsonPath of packageJsonPaths) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageName = packageJson.name; + + if (!packageName) continue; + + const releases = packageReleases.get(packageName); + if (!releases) continue; + + const currentVersion = packageJson.version; + const highestReleaseType = getHighestReleaseType(releases); + const newVersion = bumpVersion(currentVersion, highestReleaseType); + + packageJson.version = newVersion; + + if (!dryRun) { + writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); + } + + console.log( + pc.green('✔'), + pc.cyan(packageName), + pc.dim(`(${currentVersion} → ${newVersion})`) + ); + + updatedPackages.push(packageName); + } + + if (dryRun) { + console.log(pc.yellow('\nDry run - no files were modified.')); + } else { + console.log(pc.green(`\nUpdated ${updatedPackages.length} package(s).`)); + + for (const changesetFile of changesetFiles) { + unlinkSync(changesetFile); + console.log(pc.dim(`Deleted ${changesetFile}`)); + } + + console.log(pc.green(`\nDeleted ${changesetFiles.length} changeset file(s).`)); + } + + if (install && !dryRun && updatedPackages.length > 0) { + const detected = await detect(); + if (detected) { + const agent = detected.agent || detected.name; + let installCmd = ''; + + switch (agent) { + case 'npm': + installCmd = 'npm install'; + break; + case 'yarn': + case 'yarn@berry': + installCmd = 'yarn install'; + break; + case 'pnpm': + case 'pnpm@6': + installCmd = 'pnpm install'; + break; + case 'bun': + installCmd = 'bun install'; + break; + default: + console.warn(pc.yellow(`Unsupported package manager: ${agent}. Skipping install.`)); + return; + } + + console.log(`\n${pc.dim('Running')}`, pc.cyan(installCmd), pc.dim('...\n')); + try { + execSync(installCmd, { stdio: 'inherit' }); + console.log(pc.green('✔'), 'Install completed successfully'); + } catch (error) { + console.error(pc.red('✗'), 'Install failed'); + throw error; + } + } else { + console.warn(pc.yellow('Could not detect package manager. Skipping install.')); + } + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..edc9be9 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + exclude: ["dist"] + }, +});