diff --git a/.changeset/README.md b/.changeset/README.md index c5206f019..e23084528 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -1,5 +1,5 @@ # Changesets -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with mono-repos or single package repos to help you version and release your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with mono-repos or single package repos to help you version and release your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets). -We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) +We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/afraid-radios-fetch.md b/.changeset/afraid-radios-fetch.md new file mode 100644 index 000000000..12269c580 --- /dev/null +++ b/.changeset/afraid-radios-fetch.md @@ -0,0 +1,12 @@ +--- +"@changesets/apply-release-plan": patch +"@changesets/release-utils": patch +"@changesets/config": patch +"@changesets/write": patch +"@changesets/read": patch +"@changesets/cli": patch +"@changesets/git": patch +"@changesets/pre": patch +--- + +Replace `fs-extra` usage with `node:fs` diff --git a/.changeset/bright-points-think.md b/.changeset/bright-points-think.md new file mode 100644 index 000000000..61ed78428 --- /dev/null +++ b/.changeset/bright-points-think.md @@ -0,0 +1,6 @@ +--- +"@changesets/apply-release-plan": patch +"@changesets/cli": patch +--- + +Fixed resolution of changelog and commit generator modules so built-in modules can still be loaded when they are not installed in the target project. diff --git a/.changeset/busy-points-guess.md b/.changeset/busy-points-guess.md new file mode 100644 index 000000000..30176dbc5 --- /dev/null +++ b/.changeset/busy-points-guess.md @@ -0,0 +1,5 @@ +--- +"@changesets/read": minor +--- + +Remove support for reading changesets from version 1 diff --git a/.changeset/chatty-kings-bake.md b/.changeset/chatty-kings-bake.md new file mode 100644 index 000000000..8004d4797 --- /dev/null +++ b/.changeset/chatty-kings-bake.md @@ -0,0 +1,9 @@ +--- +"@changesets/release-utils": major +--- + +Removed `execWithOutput` and `spawnWithOutput`. + +They were never intended to be used externally, but are now removed either way. + +If you used them, we recommend using `tinyexec` or `node:child_process#exec` directly. diff --git a/.changeset/chilly-insects-attack.md b/.changeset/chilly-insects-attack.md new file mode 100644 index 000000000..8e818c39d --- /dev/null +++ b/.changeset/chilly-insects-attack.md @@ -0,0 +1,5 @@ +--- +"@changesets/get-github-info": patch +--- + +Remove `node-fetch` dependency diff --git a/.changeset/clean-cameras-fix.md b/.changeset/clean-cameras-fix.md new file mode 100644 index 000000000..049a52cb5 --- /dev/null +++ b/.changeset/clean-cameras-fix.md @@ -0,0 +1,6 @@ +--- +"@changesets/config": major +"@changesets/types": major +--- + +Replaced the `prettier` config option with `format`. `format` supports `"auto"`, `"prettier"`, `"oxfmt"`, `"deno"`, `"dprint"`, and `false`. If you previously used `prettier: false`, migrate to `format: false`. diff --git a/.changeset/clean-cobras-pull.md b/.changeset/clean-cobras-pull.md new file mode 100644 index 000000000..611c232a9 --- /dev/null +++ b/.changeset/clean-cobras-pull.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Fixed `changeset publish` to respect ignored packages for both publishing and private package tagging. diff --git a/.changeset/clever-frogs-kick.md b/.changeset/clever-frogs-kick.md new file mode 100644 index 000000000..4aae4db8c --- /dev/null +++ b/.changeset/clever-frogs-kick.md @@ -0,0 +1,16 @@ +--- +"@changesets/release-utils": major +--- + +Changed `runPublish()` signature. + +Now requires passing separate `command` and `args` parameters: + +```diff +runPublish({ +- script: `node -e 'console.log("test")'` ++ command: "node", ++ args: ["-e", `console.log("test")`] + cwd: import.meta.dirname, +}) +``` diff --git a/.changeset/config.json b/.changeset/config.json index e64346c9e..d1eca29bc 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,4 +1,5 @@ { + "$schema": "../packages/config/schema.json", "changelog": [ "@changesets/changelog-github", { "repo": "changesets/changesets" } @@ -6,6 +7,7 @@ "baseBranch": "main", "commit": false, "access": "public", + "ignore": ["@changesets/test-utils", "@changesets/color"], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "updateInternalDependents": "always" } diff --git a/.changeset/cool-camels-type.md b/.changeset/cool-camels-type.md new file mode 100644 index 000000000..e5edf3424 --- /dev/null +++ b/.changeset/cool-camels-type.md @@ -0,0 +1,40 @@ +--- +"@changesets/config": major +--- + +Removed `read` and `parse` functions in favor of `readConfig`, which returns `{ config, warnings, errors }` instead of throwing on issues. + +```ts +// before.ts +import { parse } from "@changesets/config"; +import { getPackages } from "@manypkg/get-packages"; + +const config = parse({ commit: true }, await getPackages()); + +try { + return parse({ commit: true }, packages); +} catch (err) { + if (err instanceof ValidationError) { + console.error(`Invalid config: ${err.message}`); + } else { + throw err; + } +} + +// after.ts +import { readConfig } from "@changesets/config"; +import { getPackages } from "@manypkg/get-packages"; + +// both arguments are optional +const { config, warnings, errors } = readConfig( + process.cwd(), + await getPackages(), +); + +if (warnings.length !== 0) { + console.warn(warnings.join("\n")); +} +if (config == null) { + console.error(errors.join("\n")); +} +``` diff --git a/.changeset/cool-places-hug.md b/.changeset/cool-places-hug.md new file mode 100644 index 000000000..e06aa38e8 --- /dev/null +++ b/.changeset/cool-places-hug.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": minor +--- + +Remove support for the legacy Changeset v1 format. diff --git a/.changeset/cozy-knives-brake.md b/.changeset/cozy-knives-brake.md new file mode 100644 index 000000000..c029e9dec --- /dev/null +++ b/.changeset/cozy-knives-brake.md @@ -0,0 +1,5 @@ +--- +"@changesets/types": major +--- + +Use stricter types for the options parameter for `CommitFunctions`, `ChangelogFunctions`, `Config` & `WrittenConfig`'s `commit` and `changelog` properties, to `null | Record` instead of `any` or `Record` diff --git a/.changeset/curly-kids-thank.md b/.changeset/curly-kids-thank.md new file mode 100644 index 000000000..046fda3c4 --- /dev/null +++ b/.changeset/curly-kids-thank.md @@ -0,0 +1,5 @@ +--- +"@changesets/config": patch +--- + +Removed `@changesets/logger`. diff --git a/.changeset/curly-loops-crash.md b/.changeset/curly-loops-crash.md new file mode 100644 index 000000000..4378d5245 --- /dev/null +++ b/.changeset/curly-loops-crash.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Enable guide line for `add` command and use box design for dependent patch bump note diff --git a/.changeset/cute-deserts-like.md b/.changeset/cute-deserts-like.md new file mode 100644 index 000000000..7f2f9cb8f --- /dev/null +++ b/.changeset/cute-deserts-like.md @@ -0,0 +1,5 @@ +--- +"@changesets/errors": minor +--- + +Added `cause` option to `ExitError` to help surface the cause behind exiting. diff --git a/.changeset/deep-coins-attend.md b/.changeset/deep-coins-attend.md new file mode 100644 index 000000000..6e43ee7f1 --- /dev/null +++ b/.changeset/deep-coins-attend.md @@ -0,0 +1,24 @@ +--- +"@changesets/get-version-range-type": minor +"@changesets/assemble-release-plan": major +"@changesets/get-dependents-graph": major +"@changesets/should-skip-package": minor +"@changesets/apply-release-plan": major +"@changesets/changelog-github": minor +"@changesets/get-release-plan": major +"@changesets/get-github-info": minor +"@changesets/changelog-git": minor +"@changesets/release-utils": minor +"@changesets/config": major +"@changesets/errors": minor +"@changesets/logger": minor +"@changesets/parse": minor +"@changesets/types": major +"@changesets/write": minor +"@changesets/read": minor +"@changesets/cli": major +"@changesets/git": major +"@changesets/pre": major +--- + +Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. diff --git a/.changeset/eager-peas-hide.md b/.changeset/eager-peas-hide.md new file mode 100644 index 000000000..300dfe14d --- /dev/null +++ b/.changeset/eager-peas-hide.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": minor +--- + +Preserve the existing formatting of `package.json` when updating version and dependency ranges diff --git a/.changeset/eight-ears-study.md b/.changeset/eight-ears-study.md new file mode 100644 index 000000000..78ee1c9a2 --- /dev/null +++ b/.changeset/eight-ears-study.md @@ -0,0 +1,6 @@ +--- +"@changesets/assemble-release-plan": minor +"@changesets/cli": minor +--- + +Support `{commit-short}` placeholder for the `snapshot.prereleaseTemplate` config, which is a 7 character variant of `{commit}` diff --git a/.changeset/every-boats-crash.md b/.changeset/every-boats-crash.md new file mode 100644 index 000000000..46bd8c98d --- /dev/null +++ b/.changeset/every-boats-crash.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": major +--- + +Set supported package manager versions in `"engines"` field, including npm >=10.9.0, pnpm >=10.0.0, and yarn >=4.5.2. diff --git a/.changeset/fair-lamps-relate.md b/.changeset/fair-lamps-relate.md new file mode 100644 index 000000000..6d7393d8a --- /dev/null +++ b/.changeset/fair-lamps-relate.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +For pnpm projects, Changesets now match pnpm's native registry behavior more closely during unpublished package checks. Both scope-based `publishConfig` registry overrides and `publishConfig.registry` are now ignored. diff --git a/.changeset/few-badgers-sing.md b/.changeset/few-badgers-sing.md new file mode 100644 index 000000000..3afdeb92d --- /dev/null +++ b/.changeset/few-badgers-sing.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Lazy-load CLI commands so `changeset` only loads the code needed for the command being run. diff --git a/.changeset/fix-version-no-changesets-exit-code.md b/.changeset/fix-version-no-changesets-exit-code.md new file mode 100644 index 000000000..d3a0b19f6 --- /dev/null +++ b/.changeset/fix-version-no-changesets-exit-code.md @@ -0,0 +1,7 @@ +--- +"@changesets/cli": major +--- + +`changeset version` now exits with code 1 when there are no unreleased changesets, instead of silently exiting with code 0. + +This makes it easier to detect when a version step is a no-op — for example, to prevent accidentally publishing packages with incorrect version tags when using `--snapshot` mode. diff --git a/.changeset/free-results-love-2.md b/.changeset/free-results-love-2.md new file mode 100644 index 000000000..59ed2b751 --- /dev/null +++ b/.changeset/free-results-love-2.md @@ -0,0 +1,24 @@ +--- +"@changesets/get-version-range-type": major +"@changesets/assemble-release-plan": major +"@changesets/get-dependents-graph": major +"@changesets/should-skip-package": major +"@changesets/apply-release-plan": major +"@changesets/changelog-github": major +"@changesets/get-release-plan": major +"@changesets/get-github-info": major +"@changesets/changelog-git": major +"@changesets/release-utils": major +"@changesets/config": major +"@changesets/errors": major +"@changesets/logger": major +"@changesets/parse": major +"@changesets/types": major +"@changesets/write": major +"@changesets/read": major +"@changesets/cli": major +"@changesets/git": major +"@changesets/pre": major +--- + +Bumped supported Node versions to `^22.11 || ^24 || >=26` diff --git a/.changeset/free-results-love.md b/.changeset/free-results-love.md new file mode 100644 index 000000000..7e50920e4 --- /dev/null +++ b/.changeset/free-results-love.md @@ -0,0 +1,24 @@ +--- +"@changesets/get-version-range-type": minor +"@changesets/assemble-release-plan": major +"@changesets/get-dependents-graph": major +"@changesets/should-skip-package": minor +"@changesets/apply-release-plan": major +"@changesets/changelog-github": minor +"@changesets/get-release-plan": major +"@changesets/get-github-info": minor +"@changesets/changelog-git": minor +"@changesets/release-utils": minor +"@changesets/config": major +"@changesets/errors": minor +"@changesets/logger": minor +"@changesets/parse": minor +"@changesets/types": major +"@changesets/write": minor +"@changesets/read": minor +"@changesets/cli": major +"@changesets/git": major +"@changesets/pre": major +--- + +Bumps minimum node version to `>=20.19.0` diff --git a/.changeset/fuzzy-melons-unite.md b/.changeset/fuzzy-melons-unite.md new file mode 100644 index 000000000..09741206e --- /dev/null +++ b/.changeset/fuzzy-melons-unite.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Added a `changeset publish-plan` command to inspect which packages would be published or tagged, with optional JSON output. diff --git a/.changeset/gentle-windows-enter.md b/.changeset/gentle-windows-enter.md new file mode 100644 index 000000000..5afa2277b --- /dev/null +++ b/.changeset/gentle-windows-enter.md @@ -0,0 +1,5 @@ +--- +"@changesets/changelog-github": minor +--- + +Bump `dotenv` dependency to v16 diff --git a/.changeset/green-bottles-leave.md b/.changeset/green-bottles-leave.md new file mode 100644 index 000000000..623cbeef6 --- /dev/null +++ b/.changeset/green-bottles-leave.md @@ -0,0 +1,5 @@ +--- +"@changesets/types": minor +--- + +Export `Package` and `Packages` from `@changesets/types`. They are meant to be used instead of the types from `@manypkg/get-packages`. diff --git a/.changeset/green-forks-carry.md b/.changeset/green-forks-carry.md new file mode 100644 index 000000000..4f78ae2b5 --- /dev/null +++ b/.changeset/green-forks-carry.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": major +--- + +The `prettier` option in `.changeset/config.json` has been removed in favor of `format`. `format` supports `"auto"`, `"prettier"`, `"oxfmt"`, `"deno"`, and `"dprint"`, and `false` disables formatting. If you previously used `prettier: false`, migrate to `format: false` or remove the option to use automatic formatter detection. diff --git a/.changeset/green-pianos-sneeze.md b/.changeset/green-pianos-sneeze.md new file mode 100644 index 000000000..477ef3ae5 --- /dev/null +++ b/.changeset/green-pianos-sneeze.md @@ -0,0 +1,5 @@ +--- +"@changesets/assemble-release-plan": major +--- + +Drop the legacy compatibility shim in `assembleReleasePlan` that accepted older `config` and `snapshot` argument shapes. diff --git a/.changeset/happy-news-wave.md b/.changeset/happy-news-wave.md new file mode 100644 index 000000000..d7310b415 --- /dev/null +++ b/.changeset/happy-news-wave.md @@ -0,0 +1,5 @@ +--- +"@changesets/release-utils": minor +--- + +Update markdown dependencies to latest. The returned markdown from `getChangelogEntry` may be formatted differently, but should semantically represent the same content. diff --git a/.changeset/huge-flowers-fall.md b/.changeset/huge-flowers-fall.md new file mode 100644 index 000000000..4ccfb2bca --- /dev/null +++ b/.changeset/huge-flowers-fall.md @@ -0,0 +1,5 @@ +--- +"@changesets/changelog-github": patch +--- + +Improve type-check for options object diff --git a/.changeset/late-animals-drive.md b/.changeset/late-animals-drive.md new file mode 100644 index 000000000..c31da2df7 --- /dev/null +++ b/.changeset/late-animals-drive.md @@ -0,0 +1,5 @@ +--- +"@changesets/changelog-github": patch +--- + +Use `parseEnv` instead of `dotenv` to load the `.env` file and avoid loading them to `process.env` diff --git a/.changeset/legal-islands-heal.md b/.changeset/legal-islands-heal.md new file mode 100644 index 000000000..e38e923aa --- /dev/null +++ b/.changeset/legal-islands-heal.md @@ -0,0 +1,6 @@ +--- +"@changesets/config": patch +"@changesets/git": patch +--- + +Refactored from `micromatch` to `picomatch` for globbing patterns diff --git a/.changeset/long-badgers-relax.md b/.changeset/long-badgers-relax.md new file mode 100644 index 000000000..c6b814e8c --- /dev/null +++ b/.changeset/long-badgers-relax.md @@ -0,0 +1,5 @@ +--- +"@changesets/release-utils": minor +--- + +Replace markdown parsing with regex diff --git a/.changeset/loose-towns-care.md b/.changeset/loose-towns-care.md new file mode 100644 index 000000000..e141cfb53 --- /dev/null +++ b/.changeset/loose-towns-care.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Use `cac` for CLI arg parsing and handling diff --git a/.changeset/lovely-years-spend.md b/.changeset/lovely-years-spend.md new file mode 100644 index 000000000..335bd1cb0 --- /dev/null +++ b/.changeset/lovely-years-spend.md @@ -0,0 +1,5 @@ +--- +"@changesets/release-utils": major +--- + +Dropped support for v1 `changeset bump` command in `runVersion()` diff --git a/.changeset/new-brooms-turn.md b/.changeset/new-brooms-turn.md new file mode 100644 index 000000000..136496465 --- /dev/null +++ b/.changeset/new-brooms-turn.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Fixed publish error printing for pnpm 11. diff --git a/.changeset/old-paths-bake.md b/.changeset/old-paths-bake.md new file mode 100644 index 000000000..80d99fd41 --- /dev/null +++ b/.changeset/old-paths-bake.md @@ -0,0 +1,5 @@ +--- +"@changesets/git": patch +--- + +Remove `is-subdir` dependency diff --git a/.changeset/orange-cups-ask.md b/.changeset/orange-cups-ask.md new file mode 100644 index 000000000..b736340c0 --- /dev/null +++ b/.changeset/orange-cups-ask.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": major +--- + +Removed warning messages about using v1 configs. They will now be silently ignored. diff --git a/.changeset/petite-buttons-shake.md b/.changeset/petite-buttons-shake.md new file mode 100644 index 000000000..28da89dda --- /dev/null +++ b/.changeset/petite-buttons-shake.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": patch +--- + +Move `outdent` as a dev dependency diff --git a/.changeset/plain-planes-arrive.md b/.changeset/plain-planes-arrive.md new file mode 100644 index 000000000..5d29a6e34 --- /dev/null +++ b/.changeset/plain-planes-arrive.md @@ -0,0 +1,13 @@ +--- +"@changesets/assemble-release-plan": major +"@changesets/get-dependents-graph": major +"@changesets/apply-release-plan": major +"@changesets/get-release-plan": major +"@changesets/release-utils": major +"@changesets/config": major +"@changesets/cli": major +"@changesets/git": major +"@changesets/pre": major +--- + +Update `@manypkg/get-packages` which drops support for detecting packages in Bolt monorepos and adds support for npm monorepos diff --git a/.changeset/plenty-forks-sip.md b/.changeset/plenty-forks-sip.md new file mode 100644 index 000000000..b4ad65553 --- /dev/null +++ b/.changeset/plenty-forks-sip.md @@ -0,0 +1,5 @@ +--- +"@changesets/config": major +--- + +Change the `defaultWrittenConfig` `baseBranch` value from `"master"` to `"main"` diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..4e3e60c4e --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,104 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@changesets/apply-release-plan": "7.0.5", + "@changesets/assemble-release-plan": "6.0.4", + "@changesets/changelog-git": "0.2.0", + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.8", + "@changesets/config": "3.0.3", + "@changesets/errors": "0.2.0", + "@changesets/get-dependents-graph": "2.1.2", + "@changesets/get-github-info": "0.6.0", + "@changesets/get-release-plan": "4.0.4", + "@changesets/get-version-range-type": "0.4.0", + "@changesets/git": "3.0.1", + "@changesets/logger": "0.1.1", + "@changesets/parse": "0.4.0", + "@changesets/pre": "2.0.1", + "@changesets/read": "0.6.1", + "@changesets/release-utils": "0.2.1", + "@changesets/should-skip-package": "0.1.1", + "@changesets/types": "6.0.0", + "@changesets/write": "0.3.2", + "@changesets/test-utils": "0.0.7", + "@changesets/color": "1.0.0" + }, + "changesets": [ + "afraid-radios-fetch", + "bright-points-think", + "busy-points-guess", + "chatty-kings-bake", + "chilly-insects-attack", + "clean-cameras-fix", + "clean-cobras-pull", + "clever-frogs-kick", + "cool-camels-type", + "cool-places-hug", + "cozy-knives-brake", + "curly-kids-thank", + "curly-loops-crash", + "cute-deserts-like", + "deep-coins-attend", + "eager-peas-hide", + "eight-ears-study", + "every-boats-crash", + "fair-lamps-relate", + "few-badgers-sing", + "fix-version-no-changesets-exit-code", + "free-results-love-2", + "free-results-love", + "fuzzy-melons-unite", + "gentle-windows-enter", + "green-bottles-leave", + "green-forks-carry", + "green-pianos-sneeze", + "happy-news-wave", + "huge-flowers-fall", + "late-animals-drive", + "legal-islands-heal", + "long-badgers-relax", + "loose-towns-care", + "lovely-years-spend", + "new-brooms-turn", + "old-paths-bake", + "orange-cups-ask", + "petite-buttons-shake", + "plain-planes-arrive", + "plenty-forks-sip", + "public-pianos-judge", + "public-zebras-hope", + "quick-sheep-vanish", + "rare-carrots-read", + "rare-toys-march", + "real-horns-shiver", + "red-emus-wave", + "rich-geckos-end", + "round-kings-smile", + "seven-islands-care", + "shiny-monkeys-notice", + "silent-beds-reply", + "silly-bushes-hang", + "small-tips-unpack", + "social-snails-battle", + "soft-sloths-burn", + "some-papayas-bet", + "some-papayas-gamble", + "spicy-nights-change", + "spotty-chairs-call", + "stale-plants-juggle", + "tangy-buses-smoke", + "thick-emus-refuse", + "thin-chicken-smell", + "tough-balloons-buy", + "tricky-spies-hunt", + "two-hoops-fix", + "wet-loops-watch", + "whole-aliens-notice", + "wicked-dryers-shave", + "wild-drinks-hang", + "wise-mirrors-fry", + "yummy-garlics-carry" + ] +} diff --git a/.changeset/public-pianos-judge.md b/.changeset/public-pianos-judge.md new file mode 100644 index 000000000..5a18a50ca --- /dev/null +++ b/.changeset/public-pianos-judge.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Auto-create the directory for the target publish plan file when executing `changeset publish-plan --output ` diff --git a/.changeset/public-zebras-hope.md b/.changeset/public-zebras-hope.md new file mode 100644 index 000000000..cf0c886bd --- /dev/null +++ b/.changeset/public-zebras-hope.md @@ -0,0 +1,19 @@ +--- +"@changesets/get-version-range-type": minor +"@changesets/assemble-release-plan": minor +"@changesets/get-dependents-graph": minor +"@changesets/apply-release-plan": minor +"@changesets/changelog-github": minor +"@changesets/get-release-plan": minor +"@changesets/changelog-git": minor +"@changesets/release-utils": minor +"@changesets/parse": minor +"@changesets/write": minor +"@changesets/read": minor +"@changesets/cli": minor +"@changesets/git": minor +--- + +Add a named export that mirrors the current `default` export + +The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. diff --git a/.changeset/quick-sheep-vanish.md b/.changeset/quick-sheep-vanish.md new file mode 100644 index 000000000..7726fbaee --- /dev/null +++ b/.changeset/quick-sheep-vanish.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": minor +--- + +Improve the default unformatted changelog new lines handling to ensure consistent spacing after the heading and the spacing between release lines diff --git a/.changeset/rare-carrots-read.md b/.changeset/rare-carrots-read.md new file mode 100644 index 000000000..903053983 --- /dev/null +++ b/.changeset/rare-carrots-read.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": major +--- + +Remove support for the `--sinceMaster` flag for `changeset status`. Use `--since=master` or `--since=main` instead. diff --git a/.changeset/rare-toys-march.md b/.changeset/rare-toys-march.md new file mode 100644 index 000000000..a341290b7 --- /dev/null +++ b/.changeset/rare-toys-march.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Packages are now listed in alphabetical order when possible. diff --git a/.changeset/real-horns-shiver.md b/.changeset/real-horns-shiver.md new file mode 100644 index 000000000..77b4104d6 --- /dev/null +++ b/.changeset/real-horns-shiver.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Order releases into dependency-aware chunks so packages are grouped in publish order. diff --git a/.changeset/red-emus-wave.md b/.changeset/red-emus-wave.md new file mode 100644 index 000000000..5e18c6086 --- /dev/null +++ b/.changeset/red-emus-wave.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Removed extra leftover code related to Changesets v1 diff --git a/.changeset/rich-geckos-end.md b/.changeset/rich-geckos-end.md new file mode 100644 index 000000000..ce3df03ab --- /dev/null +++ b/.changeset/rich-geckos-end.md @@ -0,0 +1,5 @@ +--- +"@changesets/config": minor +--- + +Refactored config parsing to use Valibot and validation rules. diff --git a/.changeset/round-kings-smile.md b/.changeset/round-kings-smile.md new file mode 100644 index 000000000..fdab83bd6 --- /dev/null +++ b/.changeset/round-kings-smile.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Added `changeset publish --from-pack-dir ` to publish packages from a previously created pack output directory. diff --git a/.changeset/seven-islands-care.md b/.changeset/seven-islands-care.md new file mode 100644 index 000000000..1f253bb1b --- /dev/null +++ b/.changeset/seven-islands-care.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Added a `changeset pack` command that requires `--out-dir` and writes publishable package tarballs plus an enriched `publish-plan.json` into that directory, either from the current workspace or from a saved publish plan via `--from-plan`. diff --git a/.changeset/shiny-monkeys-notice.md b/.changeset/shiny-monkeys-notice.md new file mode 100644 index 000000000..f9d482650 --- /dev/null +++ b/.changeset/shiny-monkeys-notice.md @@ -0,0 +1,5 @@ +--- +"@changesets/read": minor +--- + +Ignore more markdown files in the `.changeset` directory when reading changesets, including AGENTS.md, CLAUDE.md, and GEMINI.md diff --git a/.changeset/silent-beds-reply.md b/.changeset/silent-beds-reply.md new file mode 100644 index 000000000..062483551 --- /dev/null +++ b/.changeset/silent-beds-reply.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Fixed accidental success logs on failed npm publishes diff --git a/.changeset/silly-bushes-hang.md b/.changeset/silly-bushes-hang.md new file mode 100644 index 000000000..f705e1af8 --- /dev/null +++ b/.changeset/silly-bushes-hang.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": patch +--- + +Remove `@changesets/get-version-range-type` dependency diff --git a/.changeset/small-tips-unpack.md b/.changeset/small-tips-unpack.md new file mode 100644 index 000000000..7bbdccc73 --- /dev/null +++ b/.changeset/small-tips-unpack.md @@ -0,0 +1,5 @@ +--- +"@changesets/write": minor +--- + +Changeset files are now formatted with [@changesets/format](https://github.com/changesets/format) instead of depending on Prettier directly. Formatter selection can be auto-detected from the project configuration or controlled via the `format` config option. diff --git a/.changeset/social-snails-battle.md b/.changeset/social-snails-battle.md new file mode 100644 index 000000000..a8ceeb496 --- /dev/null +++ b/.changeset/social-snails-battle.md @@ -0,0 +1,7 @@ +--- +"@changesets/cli": minor +--- + +Choosing a change type now shows a preview of which part of the version it affects. + +> Which packages should have a major (**X**.X.X) bump? diff --git a/.changeset/soft-sloths-burn.md b/.changeset/soft-sloths-burn.md new file mode 100644 index 000000000..bbc07cc83 --- /dev/null +++ b/.changeset/soft-sloths-burn.md @@ -0,0 +1,6 @@ +--- +"@changesets/apply-release-plan": minor +"@changesets/cli": minor +--- + +Bumped the default Prettier version used in the absence of the local installation to v3 diff --git a/.changeset/some-papayas-bet.md b/.changeset/some-papayas-bet.md new file mode 100644 index 000000000..df36f58e2 --- /dev/null +++ b/.changeset/some-papayas-bet.md @@ -0,0 +1,5 @@ +--- +"@changesets/changelog-git": major +--- + +`ChangelogFunctions` can now be both sync and async, and the `defaultChangelogFunctions` are now sync. diff --git a/.changeset/some-papayas-gamble.md b/.changeset/some-papayas-gamble.md new file mode 100644 index 000000000..05a5918cd --- /dev/null +++ b/.changeset/some-papayas-gamble.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": major +--- + +`CommitFunctions` can now be both sync and async, and the `defaultCommitFunctions` are now sync. diff --git a/.changeset/spicy-nights-change.md b/.changeset/spicy-nights-change.md new file mode 100644 index 000000000..9b2bdeeb2 --- /dev/null +++ b/.changeset/spicy-nights-change.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": patch +--- + +Update `detect-indent` package to v7 diff --git a/.changeset/spotty-chairs-call.md b/.changeset/spotty-chairs-call.md new file mode 100644 index 000000000..cbb26f976 --- /dev/null +++ b/.changeset/spotty-chairs-call.md @@ -0,0 +1,24 @@ +--- +"@changesets/get-version-range-type": major +"@changesets/assemble-release-plan": major +"@changesets/get-dependents-graph": major +"@changesets/should-skip-package": major +"@changesets/apply-release-plan": major +"@changesets/changelog-github": major +"@changesets/get-release-plan": major +"@changesets/get-github-info": major +"@changesets/changelog-git": major +"@changesets/release-utils": major +"@changesets/config": major +"@changesets/errors": major +"@changesets/logger": major +"@changesets/parse": major +"@changesets/types": major +"@changesets/write": major +"@changesets/read": major +"@changesets/cli": major +"@changesets/git": major +"@changesets/pre": major +--- + +From now on this package is going to be published as ES module. diff --git a/.changeset/stale-plants-juggle.md b/.changeset/stale-plants-juggle.md new file mode 100644 index 000000000..0e104f1af --- /dev/null +++ b/.changeset/stale-plants-juggle.md @@ -0,0 +1,5 @@ +--- +"@changesets/parse": patch +--- + +Refactor yaml parsing to use the `yaml` package diff --git a/.changeset/tangy-buses-smoke.md b/.changeset/tangy-buses-smoke.md new file mode 100644 index 000000000..5cd2a5d54 --- /dev/null +++ b/.changeset/tangy-buses-smoke.md @@ -0,0 +1,8 @@ +--- +"@changesets/cli": major +"@changesets/assemble-release-plan": major +"@changesets/config": major +"@changesets/types": major +--- + +Remove support for the deprecated `___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.useCalculatedVersionForSnapshots` config. The `snapshot.useCalculatedVersion` config should be used instead. diff --git a/.changeset/thick-emus-refuse.md b/.changeset/thick-emus-refuse.md new file mode 100644 index 000000000..984a9e638 --- /dev/null +++ b/.changeset/thick-emus-refuse.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Remove `term-size` dependency diff --git a/.changeset/thin-chicken-smell.md b/.changeset/thin-chicken-smell.md new file mode 100644 index 000000000..1ffc1e343 --- /dev/null +++ b/.changeset/thin-chicken-smell.md @@ -0,0 +1,8 @@ +--- +"@changesets/apply-release-plan": patch +"@changesets/release-utils": patch +"@changesets/cli": patch +"@changesets/git": patch +--- + +Replaced `spawndamnit` with `tinyexec` diff --git a/.changeset/tough-balloons-buy.md b/.changeset/tough-balloons-buy.md new file mode 100644 index 000000000..d473d7441 --- /dev/null +++ b/.changeset/tough-balloons-buy.md @@ -0,0 +1,8 @@ +--- +"@changesets/cli": patch +"@changesets/get-dependents-graph": patch +"@changesets/logger": patch +"@changesets/read": patch +--- + +Replace `picocolors` with `node:util`'s `styleText` diff --git a/.changeset/tricky-spies-hunt.md b/.changeset/tricky-spies-hunt.md new file mode 100644 index 000000000..62cde14d5 --- /dev/null +++ b/.changeset/tricky-spies-hunt.md @@ -0,0 +1,5 @@ +--- +"@changesets/get-github-info": patch +--- + +Automatically load `GITHUB_` environment variables from `.env` file diff --git a/.changeset/two-hoops-fix.md b/.changeset/two-hoops-fix.md new file mode 100644 index 000000000..d74c96aca --- /dev/null +++ b/.changeset/two-hoops-fix.md @@ -0,0 +1,5 @@ +--- +"@changesets/release-utils": patch +--- + +Removed the unused `signal-exit` dependency diff --git a/.changeset/wet-loops-watch.md b/.changeset/wet-loops-watch.md new file mode 100644 index 000000000..195ef8b7e --- /dev/null +++ b/.changeset/wet-loops-watch.md @@ -0,0 +1,9 @@ +--- +"@changesets/cli": major +--- + +Migrated from `enquirer` + `@inquirer/launch-editor` to `@clack/prompts` + `launch-editor`. + +This means the CLI flows will have minor changes, but they are largely the same. + +This change also fixes various issues related to `enquirer` like cancelling prompts crashing the CLI. diff --git a/.changeset/whole-aliens-notice.md b/.changeset/whole-aliens-notice.md new file mode 100644 index 000000000..d394e62ac --- /dev/null +++ b/.changeset/whole-aliens-notice.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Remove deprecated flag warnings, including `--updateChangelog`, `--isPublic`, `--skipCI`, and `--commit` diff --git a/.changeset/wicked-dryers-shave.md b/.changeset/wicked-dryers-shave.md new file mode 100644 index 000000000..86d321ba4 --- /dev/null +++ b/.changeset/wicked-dryers-shave.md @@ -0,0 +1,5 @@ +--- +"@changesets/errors": major +--- + +Removed `ValidationError`. diff --git a/.changeset/wild-drinks-hang.md b/.changeset/wild-drinks-hang.md new file mode 100644 index 000000000..81bfbae27 --- /dev/null +++ b/.changeset/wild-drinks-hang.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": minor +--- + +Show if a package is private when selecting packages in `changeset add` diff --git a/.changeset/wise-mirrors-fry.md b/.changeset/wise-mirrors-fry.md new file mode 100644 index 000000000..4a4e8da97 --- /dev/null +++ b/.changeset/wise-mirrors-fry.md @@ -0,0 +1,5 @@ +--- +"@changesets/apply-release-plan": major +--- + +Generated changelog entries and rewritten `package.json` files are now formatted with [@changesets/format](https://github.com/changesets/format) instead of depending on Prettier directly. Formatter selection can be auto-detected from the project configuration or controlled via the `format` config option. diff --git a/.changeset/yummy-garlics-carry.md b/.changeset/yummy-garlics-carry.md new file mode 100644 index 000000000..f836cebc4 --- /dev/null +++ b/.changeset/yummy-garlics-carry.md @@ -0,0 +1,5 @@ +--- +"@changesets/cli": patch +--- + +Log "New tag: ..." messages when running `changeset publish` to fix compatibility with the Changesets release GitHub action to create GitHub releases and push the new tags diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json deleted file mode 100644 index 0b9cc8935..000000000 --- a/.codesandbox/ci.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "packages": ["packages/*"], - "node": "14" -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..78c6ddee2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 218e15d84..000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -__fixtures__ -node_modules -scratchings.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 757a3f65c..000000000 --- a/.eslintrc +++ /dev/null @@ -1,68 +0,0 @@ -{ - "extends": [ - "plugin:@typescript-eslint/eslint-recommended", - "standard", - "prettier" - ], - "plugins": ["@typescript-eslint"], - "parser": "@typescript-eslint/parser", - "rules": { - "standard/computed-property-even-spacing": "off", - "lines-between-class-members": "off", - "no-template-curly-in-string": "off", - "camelcase": "off", - "import/no-duplicates": "off", - "no-unusued-vars": "off", - "no-use-before-define": "off", - "no-useless-constructor": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { "argsIgnorePattern": "^_", "ignoreRestSiblings": true } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "**/__tests__/**/*.{ts,js}", - "**/*.test.{ts,js}", - "**/test.{ts,js}" - ] - } - ] - }, - "overrides": [ - { - "files": [ - "*.test.js", - "**/__tests__/**", - "*.test.ts", - "**/test.ts", - "**/test-utils/**/*" - ], - "env": { - "jest": true - } - }, - { - "files": ["**/packages/*/src/*", "**/packages/*/src/**/*"], - "rules": { - "import/no-commonjs": "error" - } - }, - { - "files": ["**/__fixtures__/*"], - "rules": { - "no-unused-vars": "off" - }, - "env": { - "jest": false - } - }, - { - "files": ["*.ts"], - "rules": { - "prefer-const": "off" - } - } - ] -} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..16ade9b9c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# prettier v3 migration +303cacdde85c94f2ef4d1408b401165ff25d263d diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..cccb81b4a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +/.github/** @Andarist @bluwy @emmatown diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index a44f173f7..248a1df21 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -1,14 +1,41 @@ name: Setup CI +inputs: + node-version: + description: "Node.js version" + required: false + default: "22" + skip-cache: + description: "Whether to skip the cache" + required: false + default: "false" + download-dist: + description: "Whether to download a dist folder artifact from the current run" + required: false + default: "false" + runs: using: composite steps: - - name: Setup Node.js 20.x - uses: actions/setup-node@v4 + - name: Set up pnpm + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 20.x - cache: yarn + node-version: ${{ inputs.node-version }} + package-manager-cache: ${{ inputs.skip-cache != 'true' }} + cache: ${{ inputs.skip-cache != 'true' && 'pnpm' || '' }} - name: Install dependencies shell: bash - run: yarn install --frozen-lockfile + run: pnpm install --frozen-lockfile + + - name: Check with manypkg + shell: bash + run: pnpm manypkg check + + - name: Download dist artifacts + if: ${{ inputs.download-dist == 'true' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a3cce576..3ebb2358a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,32 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "npm" + directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 10 + versioning-strategy: increase + groups: + production-dependencies: + dependency-type: "production" + update-types: + - minor + - patch + development-dependencies: + dependency-type: "development" + update-types: + - minor + - patch + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/changeset-version.yml b/.github/workflows/changeset-version.yml deleted file mode 100644 index 044e5651f..000000000 --- a/.github/workflows/changeset-version.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Release - -on: - push: - branches: - - main - - next - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - release: - name: Release - if: github.repository == 'changesets/changesets' - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: write # to create release (changesets/action) - issues: write # to post issue comments (changesets/action) - pull-requests: write # to create pull request (changesets/action) - id-token: write # to use OpenID Connect token for provenance (changesets/action) - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/ci-setup - - - name: Create Release Pull Request or Publish to npm - # https://github.com/changesets/action - uses: changesets/action@v1 - with: - # this expects you to have a script called release which does a build for your packages and calls changeset publish - publish: yarn release - version: yarn version-packages - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 341b0ea89..146503d18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,20 +2,87 @@ name: CI on: pull_request: - push: - branches: [main] + # merge queue is required so all commits on target branches trigger this workflow + # despite lack of the push event trigger here + merge_group: + branches: + - main + - next + # merge group rulesets don't allow wildcards so in settings each maintenance branch needs to be added separately + - "maintenance/v*" # branch rulesets don't support v[0-9]+ permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + + - name: Build artifacts + run: pnpm build + + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + # the size of the dist artifacts is not very large, so we can use better compression without a slowdown + compression-level: 8 + # store it for as short as possible, since we only need it for the same workflow run + retention-days: 1 + name: dist + path: | + packages/*/dist + scripts/*/dist + + lint-workflows: + name: Lint workflows + runs-on: ubuntu-latest + permissions: + actions: read # only required in private repos + security-events: write # allow writing security events + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + persona: pedantic + annotations: true + advanced-security: false + test: - name: Test + name: "Test using Node ${{ matrix.node_version }}" + needs: [build] runs-on: ubuntu-latest timeout-minutes: 20 + strategy: + matrix: + node_version: [22, 24, 26] + fail-fast: false steps: - - uses: actions/checkout@v4 + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: ./.github/actions/ci-setup + with: + node-version: ${{ matrix.node_version }} + download-dist: true - name: Check Git version run: git --version @@ -23,52 +90,66 @@ jobs: - name: Setup mock Git user run: git config --global user.email "you@example.com" && git config --global user.name "Your Name" - - name: Jest tests - run: yarn jest --ci --color --runInBand --coverage --reporters=default --reporters=jest-junit + - name: Vitest tests + if: matrix.node_version != 24 + run: pnpm test + + # Run coverage only once on node 24 to avoid duplicate codecov uploads + - name: Vitest tests with coverage + if: matrix.node_version == 24 + run: pnpm test --coverage - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + if: matrix.node_version == 24 with: token: ${{ secrets.CODECOV_TOKEN }} typecheck: name: Typecheck + needs: [build] runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: ./.github/actions/ci-setup + with: + download-dist: true - name: Typecheck - run: yarn types:check + run: pnpm types:check lint: name: Lint + needs: [build] runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: ./.github/actions/ci-setup + with: + download-dist: true - name: Lint - run: yarn lint + run: pnpm lint - name: Format - run: yarn format + run: pnpm format ci-ok: name: CI OK runs-on: ubuntu-latest if: always() - needs: [test, typecheck, lint] - env: - FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }} + needs: [lint-workflows, build, test, typecheck, lint] steps: - - name: Check for failure - run: | - echo $FAILURE - if [ "$FAILURE" = "false" ]; then - exit 0 - else - exit 1 - fi + - name: Exit with error if some jobs are not successful + run: exit 1 + if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml new file mode 100644 index 000000000..d1a1a5ebe --- /dev/null +++ b/.github/workflows/pkg-pr-new.yml @@ -0,0 +1,36 @@ +name: pkg-pr-new + +on: + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + queue: max + +permissions: {} + +jobs: + publish: + name: Publish + if: ${{ contains(github.event.pull_request.labels.*.name, 'pkg.pr.new') }} + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + with: + node-version: 26 + skip-cache: true # avoid cache poisoning attacks + + - name: Build + run: pnpm build + + - run: + | # zizmor: ignore[use-trusted-publishing] we're not publishing to npm here + pnpm exec pkg-pr-new publish --pnpm --packageManager=pnpm --commentWithDev --commentWithSha './packages/*' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..ef5398ccb --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,140 @@ +name: Publish + +on: + push: + branches: + - main + - next + - "maintenance/v*" # branch rulesets don't support v[0-9]+ + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + queue: max + +permissions: {} # each job should define its own permission explicitly + +jobs: + select-mode: + name: Select Mode + runs-on: ubuntu-latest + environment: version + timeout-minutes: 20 + outputs: + mode: ${{ steps.select-mode.outputs.mode }} + publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }} + permissions: + contents: read # to check out repo (actions/checkout) + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + with: + skip-cache: true # avoid cache poisoning attacks + + - name: Build artifacts + run: pnpm build + + - name: Select mode + id: select-mode + # https://github.com/changesets/action + uses: changesets/action/select-mode@8f7aee55a02899b4d6140157e482211eb49fcbee # v2.0.0-next.2 + + version: + name: Version + needs: select-mode + if: needs.select-mode.outputs.mode == 'version' + runs-on: ubuntu-latest + environment: version + timeout-minutes: 20 + permissions: + contents: read # to check out repo (actions/checkout) + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + with: + skip-cache: true # avoid cache poisoning attacks + + - name: Build artifacts + run: pnpm build + + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write # to create version commits (changesets/action) + permission-pull-requests: write # to create pull request (changesets/action) + + - name: Create or update release pull request + id: changesets + # https://github.com/changesets/action + uses: changesets/action/version@8f7aee55a02899b4d6140157e482211eb49fcbee # v2.0.0-next.2 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: pnpm version-packages + + pack: + name: Pack + needs: select-mode + if: needs.select-mode.outputs.mode == 'publish' + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + pack-dir-artifact-id: ${{ steps.pack.outputs.pack-dir-artifact-id }} + permissions: + contents: read # to check out repo (actions/checkout) + steps: + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + with: + node-version: 24 + skip-cache: true # avoid cache poisoning attacks + + - name: Build artifacts + run: pnpm build + + - name: Pack + id: pack + # https://github.com/changesets/action + uses: changesets/action/pack@8f7aee55a02899b4d6140157e482211eb49fcbee # v2.0.0-next.2 + with: + publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }} + + publish: + name: Publish + needs: pack + runs-on: ubuntu-latest + environment: npm + timeout-minutes: 20 + permissions: + contents: write # to create release (changesets/action) + id-token: write # to use OpenID Connect token for trusted publishing (changesets/action) + steps: + - name: Check out repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + with: + node-version: 24 + skip-cache: true # avoid cache poisoning attacks + + - name: Build artifacts + run: pnpm build + + - name: Publish to npm + # https://github.com/changesets/action + uses: changesets/action/publish@8f7aee55a02899b4d6140157e482211eb49fcbee # v2.0.0-next.2 + with: + pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }} diff --git a/.gitignore b/.gitignore index 61ff6af85..2f9132364 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -node_modules/ -*error.log -scratchings.js +.idea/ +coverage/ dist/ +node_modules/ .env -coverage/ +*error.log diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..620c5e1e9 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v22.9.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..be8f8adfa --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "sortImports": { + "newlinesBetween": false + } +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index d89cffabd..000000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -__fixtures__ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 89a5171e2..5a4e6d3b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,16 +4,13 @@ { "type": "node", "request": "launch", - "name": "Jest Current File", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["${relativeFile}", "--config", "jest.config.js", "--no-cache"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - }, - "smartStep": false + "name": "Debug Current Test File", + "autoAttachChildProcesses": true, + "skipFiles": [], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal" } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 22012885b..b6a93fc58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "grammarly.selectors": [ - { - "language": "markdown", - "scheme": "file" - } - ] + "js/ts.tsdk.path": "node_modules/typescript/lib", + "editor.defaultFormatter": "oxc.oxc-vscode" } diff --git a/FUNDING.json b/FUNDING.json deleted file mode 100644 index ee8bdc3dd..000000000 --- a/FUNDING.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "drips": { - "ethereum": { - "ownedBy": "0x334C0464Ec1e9B32835B18465250c95aCa86FaF9" - } - } -} diff --git a/README.md b/README.md index ba837b0f0..45460d0c5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,28 @@ +> [!IMPORTANT] +> This is development branch for Changesets v3. For the v2 code, check out the [maintenance/v2](https://github.com/changesets/changesets/tree/maintenance/v2) branch. +

- + + + Changesets banner +

A tool to manage versioning and changelogs
- with a focus on multi-package repositories + with a focus on monorepos +

+ +

+ + Read the docs to learn more +


-[![npm package](https://img.shields.io/npm/v/@changesets/cli?label=%40changesets%2Fcli)](https://npmjs.com/package/@changesets/cli) -[![View changelog](https://img.shields.io/badge/Explore%20Changelog-brightgreen)](./packages/cli/CHANGELOG.md) +[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/@changesets/cli?name=true)](https://npmx.dev/package/@changesets/cli) +[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/@changesets/cli?color=229fe4&value=View+changelog&label=+)](./packages/cli/CHANGELOG.md) +[![pkg.pr.new](https://pkg.pr.new/badge/changesets/changesets)](https://pkg.pr.new/~/changesets/changesets) The `changesets` workflow is designed to help when people are making changes, all the way through to publishing. It lets contributors declare how their changes should be released, then we automate updating package versions, and changelogs, and publishing new versions of packages based on the provided information. @@ -75,6 +88,7 @@ To make releasing easier, you can use [this changesets github action](https://gi - [verdaccio](https://verdaccio.org) - [Chakra UI](https://chakra-ui.com) - [Astro](https://astro.build) +- [Biome](https://biomejs.dev) - [SvelteKit](https://kit.svelte.dev) - [Hydrogen](https://hydrogen.shopify.dev) - [react-pdf](https://github.com/diegomura/react-pdf) @@ -96,6 +110,7 @@ To make releasing easier, you can use [this changesets github action](https://gi - [Apollo Client](https://github.com/apollographql/apollo-client) - [Adobe Spectrum CSS](https://github.com/adobe/spectrum-css) - [Adobe Spectrum Web Components](https://github.com/adobe/spectrum-web-components) +- [React Email](https://react.email) diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 085fb659b..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,9 +0,0 @@ -# Security policy - -## Supported versions - -The latest version of the project is currently supported with security updates. - -## Reporting a vulnerability - -You can report a vulnerability by contacting maintainers via email. diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index e63b6c255..000000000 --- a/babel.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - presets: [ - [ - "@babel/preset-env", - { - targets: { node: 8 }, - }, - ], - ], - overrides: [{ test: "**/*.ts", presets: ["@babel/preset-typescript"] }], -}; diff --git a/docs/adding-a-changeset.md b/docs/adding-a-changeset.md index 97307cee0..fed4c0a5a 100644 --- a/docs/adding-a-changeset.md +++ b/docs/adding-a-changeset.md @@ -12,7 +12,7 @@ A changeset is a piece of information about changes made in a branch or commit. ## I am in a multi-package repository (a mono-repo) -1. Run the command line script `yarn changeset` or `npx @changesets/cli`. +1. Run the command line script `yarn changeset` or `pnpm changeset` or `npx @changesets/cli`. 2. Select the packages you want to include in the changeset using and to navigate to packages, and space to select a package. Hit enter when all desired packages are selected. 3. You will be prompted to select a bump type for each selected package. Select an appropriate bump type for the changes made. See [here](https://semver.org/) for information on semver versioning 4. Your final prompt will be to provide a message to go alongside the changeset. This will be written into the changelog when the next release occurs. @@ -36,7 +36,7 @@ While not every changeset is going to need a huge amount of detail, a good idea ## I am in a single-package repository -1. Run the command line script `yarn changeset` or `npx @changesets/cli`. +1. Run the command line script `yarn changeset` or `pnpm changeset` or `npx @changesets/cli`. 2. Select an appropriate bump type for the changes made. See [here](https://semver.org/) for information on semver versioning. 3. Your final prompt will be to provide a message to go alongside the changeset. This will be written into the changelog when the next release occurs. diff --git a/docs/command-line-options.md b/docs/command-line-options.md index c6d9590d0..3f70744a1 100644 --- a/docs/command-line-options.md +++ b/docs/command-line-options.md @@ -3,7 +3,7 @@ The command line for changesets is the main way of interacting with it. There are 4 main commands. If you are looking for how we recommend you setup and manage changesets with the commands, check out our [intro to using changesets](./intro-to-using-changesets.md) - init -- add [--empty][--open] +- add [--empty] [--open] [--since ] [--message ] - version [--ignore, --snapshot] - publish [--otp=code, --tag] - status [--since=master --verbose --output=JSON_FILE.json] @@ -65,6 +65,13 @@ A changeset created with the empty flag would look like this: If you set the commit option in the config, the command will add the updated changeset files and then commit them. - `--open` - opens the created changeset in an external editor +- `--message` (or `-m`) - provides the changeset summary from the command line instead of prompting for it. + +- `--since` - uses the provided branch, tag, or git ref (such as `main` or a git commit hash) to detect which packages have changed when populating the list of changed packages in the CLI. This is useful in gitflow workflows where you have multiple target branches and `baseBranch` in the config doesn't cover all use cases. If not provided, the command falls back to the `baseBranch` value in your `.changeset/config.json`. + +``` +changeset add --since=develop +``` ## version @@ -139,7 +146,7 @@ The status command provides information about the changesets that currently exis changeset pre [exit|enter {tag}] ``` -The pre command enters and exits pre mode. The command does not do any actual versioning, when doing a pre-release, you should run changeset pre enter next(or a different tag, the tag is what is in versions and is the npm dist tag) and then do the normal release process with changeset version and changeset publish. For more information about the pre command, see the prereleases [the prereleases documentation](https://github.com/changesets/changesets/blob/master/docs/prereleases.md). +The pre command enters and exits pre mode. The command does not do any actual versioning, when doing a pre-release, you should run changeset pre enter next(or a different tag, the tag is what is in versions and is the npm dist tag) and then do the normal release process with changeset version and changeset publish. For more information about the pre command, see the prereleases [the prereleases documentation](./prereleases.md). > NOTE: pre-releases are a very complicated feature. Many of the safety rails that changesets helps you with will be taken off. We recommend that you read both [problems publishing in monorepos](./problems-publishing-in-monorepos.md) and be clear on both exiting and entering pre-releases before using it. You may also prefer using [snapshot releases](./snapshot-releases.md) for a slightly less involved process. diff --git a/docs/config-file-options.md b/docs/config-file-options.md index 1d8d79150..db798d4cb 100644 --- a/docs/config-file-options.md +++ b/docs/config-file-options.md @@ -9,9 +9,13 @@ Changesets has a minimal amount of configuration options. Mostly these are for w "fixed": [], "linked": [], "access": "restricted", - "baseBranch": "main", + "baseBranch": "master", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "bumpVersionsWithWorkspaceProtocolOnly": false, + "changedFilePatterns": ["**"], + "format": "auto", + "privatePackages": { "version": true, "tag": false } } ``` @@ -54,9 +58,11 @@ If you want to prevent a package from being published to npm, set `private: true ## `baseBranch` (git branch name) -The branch to which changesets will make comparisons. A number of internal changesets features use git to compare present changesets against another branch. This defaults what branch will be used for these comparisons. This should generally set to the major branch you merge changes into. Commands that use this information accept a `--since` option which can be used to override this. +The branch to which changesets will make comparisons to detect what has changed since the last commit of the base branch. This should generally be set to the default branch you merge changes into, e.g. `main` or `master`. -> To help make coding a more inclusive experience, we recommend changing the name of your `master` branch to `main`. +Commands that use this information accept a `--since` option which can be used to override this. + +Locally, make sure the base branch exists and is up to date so changesets can make accurate comparisons. ## `ignore` (array of packages) @@ -71,7 +77,7 @@ There are two caveats to this. These restrictions exist to ensure your repository or published code do not end up in a broken state. For a more detailed intricacies of publishing, check out our guide on [problems publishing in monorepos](./problems-publishing-in-monorepos.md). -> NOTE: you can also provide glob expressions to match the packages, according to the [micromatch](https://www.npmjs.com/package/micromatch) format. +> NOTE: you can also provide glob expressions to match the packages, according to the [picomatch](https://npmx.dev/picomatch) format. ## `fixed` (array of arrays of package names) @@ -156,9 +162,13 @@ You would specify our github changelog generator with: } ``` +If you want to disable thank you messages, add `"disableThanks": true` to the options. + For more details on these functions and information on how to write your own see [changelog-functions](./modifying-changelog-format.md) -## `bumpVersionsWithWorkspaceProtocolOnly` (boolean) +## `bumpVersionsWithWorkspaceProtocolOnly` (optional boolean) + +Default value: `false` Determines whether Changesets should only bump dependency ranges that use workspace protocol of packages that are part of the workspace. @@ -195,6 +205,18 @@ You can use the following placeholders for customizing the snapshot release vers If you are not specifying `prereleaseTemplate`, the default behavior will fall back to using the following template: `{tag}-{datetime}`, and in cases where the tag is empty (`--snapshot` with no tag name), it will use `{datetime}` only. +## `prettier` (optional boolean) + +This option configures whether Changesets will format its output using Prettier. When set to `false`, Changesets will skip formatting with Prettier. + +Default value: `true` + +```json +{ + "prettier": false +} +``` + ## `privatePackages` (object or false) This option is for setting how private packages should be handled. By default, Changesets will update the changelog for private packages and update their version, but will not create a tag. You can configure this option to change the default behavior. @@ -221,3 +243,31 @@ When `tag` is set to `true`, Changesets will create a tag for private packages. } } ``` + +## `changedFilePatterns` (array of strings) + +Glob patterns for changed files that should mark a package as changed. Useful to fine-tune what counts as a change (e.g. only source files, ignoring test files, etc). + +Default value: + +```json +{ + "changedFilePatterns": ["**"] +} +``` + +Example: + +```json +{ + "changedFilePatterns": ["src/**", "lib/**"] +} +``` + +### `format` (optional string or boolean) + +Default value: `"auto"` + +The formatter to use to format changesets and changelogs. Set `false` to disable formatting. The default value of `"auto"` will auto-detect the formatter based on the project's configuration files. See the [`@changesets/format` documentation](https://github.com/changesets/format) for more details. + +Supported formatters include `"prettier"`, `"oxfmt"`, `"deno"`, and `"dprint"`. diff --git a/docs/decisions.md b/docs/decisions.md index 313bae1c6..bddc29e24 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -6,7 +6,7 @@ This file is a discussion of some of the rules and design decisions that have go Changesets are designed to be as easy to accumulate as possible. As such, when changesets are consumed with `version`, we flatten the version bumps into one single bump at the highest semver range specified. -For example: if you run `version`, and we have `packageA` at `1.1.1`, which has two `minor` changesets, and one `patch` changeset, we will bump `packageA` to `1.2.1`. +For example: if you run `version`, and we have `packageA` at `1.1.1`, which has two `minor` changesets, and one `patch` changeset, we will bump `packageA` to `1.2.0`. This allows changesets to be added and accumulated safely, with the knowledge that packages will only be released once at an appropriate version for the combined set of changesets, while still ensuring each change is captured in the changelog, with an indication of what kind of change it is. diff --git a/docs/fixed-packages.md b/docs/fixed-packages.md index a71f8564e..0afdcecfb 100644 --- a/docs/fixed-packages.md +++ b/docs/fixed-packages.md @@ -46,4 +46,4 @@ For example: It will match all packages starting with `pkg-`. -**The glob expressions must be defined according to the [micromatch](https://www.npmjs.com/package/micromatch) format.** +**The glob expressions must be defined according to the [picomatch](https://npmx.dev/picomatch) format.** diff --git a/docs/intro-to-using-changesets.md b/docs/intro-to-using-changesets.md index 8d68da4ff..61e71faf3 100644 --- a/docs/intro-to-using-changesets.md +++ b/docs/intro-to-using-changesets.md @@ -17,7 +17,7 @@ The second two steps can be made part of a CI process. ## Add the changeset tool ```shell -yarn add @changesets/cli && yarn changeset init +yarn add -D @changesets/cli && yarn changeset init ``` or diff --git a/docs/linked-packages.md b/docs/linked-packages.md index bf648beb2..6e826af57 100644 --- a/docs/linked-packages.md +++ b/docs/linked-packages.md @@ -86,4 +86,4 @@ For example: It will match all packages starting with `pkg-`. -**The glob expressions must be defined according to the [micromatch](https://www.npmjs.com/package/micromatch) format.** +**The glob expressions must be defined according to the [picomatch](https://npmx.dev/picomatch) format.** diff --git a/docs/prereleases.md b/docs/prereleases.md index 4c5e716e4..6c71fdcb3 100644 --- a/docs/prereleases.md +++ b/docs/prereleases.md @@ -2,7 +2,7 @@ > Warning! Prereleases are very complicated! Using them requires a thorough understanding of all parts of npm publishes. Mistakes can lead to repository and publish states that are very hard to fix. -> Warning: If you decide to do prereleases from the main branch of your repository, without having a branch for your last stable release without the prerelease changes, you will block other changes until you are ready to exit prerelease mode. We thoroughly recommend only running prereleases from a branch other than the main branch. +> Warning: If you decide to do prereleases from the default branch of your repository, without having a branch for your last stable release without the prerelease changes, you will block other changes until you are ready to exit prerelease mode. We thoroughly recommend only running prereleases from a branch other than the default branch. You might want to release a version of your packages before you do an actual release, Changesets lets you do this but there are some caveats because of the complexity that monorepos add that are important to understand. @@ -46,7 +46,7 @@ The repo would now look like this: ``` packages/ - pkg-a@1.0.1-next.0 has dep on pkg-b@^2.0.1 + pkg-a@1.0.1-next.0 has dep on pkg-b@^2.1.0-next.0 pkg-b@2.1.0-next.0 has no deps pkg-c@3.0.0 has no deps .changeset/ @@ -72,7 +72,7 @@ Let's say we add some changesets and a new package so our repo looks like this ``` packages/ - pkg-a@1.0.1-next.0 has dep on pkg-b@^2.0.1 + pkg-a@1.0.1-next.0 has dep on pkg-b@^2.1.0-next.0 pkg-b@2.1.0-next.0 has no deps pkg-c@3.0.0 has no deps pkg-d@0.0.0 has no deps @@ -91,7 +91,7 @@ The version command will behave just like it does for the first versioning of a ``` packages/ - pkg-a@1.1.0-next.1 has dep on pkg-b@^2.0.1 + pkg-a@1.1.0-next.1 has dep on pkg-b@^2.1.0-next.0 pkg-b@2.1.0-next.0 has no deps pkg-c@3.0.1-next.0 has no deps pkg-d@1.0.0-next.0 has no deps @@ -128,7 +128,7 @@ The version command will apply any changesets currently in the repo and then rem ``` packages/ - pkg-a@1.1.0 has dep on pkg-b@^2.0.1 + pkg-a@1.1.0 has dep on pkg-b@^2.1.0 pkg-b@2.1.0 has no deps pkg-c@3.0.1 has no deps pkg-d@1.0.0 has no deps diff --git a/docs/versioning-apps.md b/docs/versioning-apps.md index 6a3e08950..23bd80d9c 100644 --- a/docs/versioning-apps.md +++ b/docs/versioning-apps.md @@ -20,3 +20,17 @@ To enable a project to be tracked by changesets, it needs a minimal package.json "version": "0.0.1" } ``` + +## Private dependencies + +When a versioned private package (app) depends on another private package that is skipped (either via the `ignore` option or `privatePackages.version: false`), changesets will not require the app to also be skipped. Since private packages are not published to npm, it is safe for them to depend on skipped packages. + +For example, if you have an app `A` that depends on a private library `B`, you can ignore `B` while still versioning `A`: + +```json +{ + "ignore": ["B"] +} +``` + +This works because `A` is private and will never be published to npm with a stale reference to `B`. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..0eea75f17 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,125 @@ +import e18e from "@e18e/eslint-plugin"; +import js from "@eslint/js"; +import vitest from "@vitest/eslint-plugin"; +import eslintConfigPrettier from "eslint-config-prettier/flat"; +import importLite from "eslint-plugin-import-lite"; +import node from "eslint-plugin-n"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig( + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + "site/.vitepress", + "packages/cli/bin.js", + "**/*.snap", + ], + }, + { + plugins: { + e18e, + js, + node, + tseslint, + vitest, + }, + extends: [ + "e18e/modernization", + "e18e/moduleReplacements", + "e18e/performanceImprovements", + "js/recommended", + "node/flat/recommended", + "tseslint/recommended", + "tseslint/recommendedTypeChecked", + importLite.configs.recommended, + "vitest/recommended", + ], + linterOptions: { + reportUnusedDisableDirectives: "error", + reportUnusedInlineConfigs: "error", + }, + languageOptions: { + parserOptions: { projectService: true }, + }, + rules: { + // enforce using `x == null` for nullish checks (no triple equals, no undefined) + eqeqeq: ["error", "always", { null: "never" }], + "no-restricted-syntax": [ + "error", + { + selector: "BinaryExpression:has(Identifier[name='undefined'])", + message: "Use `== null` instead of comparing with `undefined`.", + }, + ], + + "e18e/prefer-static-regex": "off", + + "@typescript-eslint/consistent-type-exports": [ + "error", + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports", disallowTypeAnnotations: false }, + ], + "import-lite/consistent-type-specifier-style": [ + "error", + "prefer-top-level", + ], + + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/unbound-method": "off", + + // these rules are slow, require extensive config, and/or don't provide much + "n/no-extraneous-import": "off", + "n/no-missing-import": "off", + "n/no-process-exit": "off", + "n/no-unpublished-import": "off", + + "n/prefer-node-protocol": "error", + "n/no-unsupported-features/node-builtins": [ + "error", + { allowExperimental: true }, + ], + + "import-lite/no-default-export": "error", + "import-lite/no-mutable-exports": "error", + }, + }, + { + files: [ + "**/index.ts", // to be removed in next release (v4) when we are dropping default export + "**/vitest.config.mts", + "**/eslint.config.mjs", + ], + rules: { + "import-lite/no-default-export": "off", + }, + }, + { + files: ["**/*.{js,mjs}"], + ...tseslint.configs.disableTypeChecked, + }, + { + files: ["**/*.test.*"], + rules: { + // mock functions often have to be async to match the original fn + "@typescript-eslint/require-await": "off", + }, + }, + { + files: ["**/*.config.*"], + rules: { + // config files often return default exports + "import-lite/no-default-export": "off", + }, + }, + eslintConfigPrettier, +); diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 63254f211..000000000 --- a/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - clearMocks: true, - watchPlugins: [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname", - ], -}; diff --git a/package.json b/package.json index 0f6d37a25..c87d3a235 100644 --- a/package.json +++ b/package.json @@ -2,78 +2,72 @@ "name": "@changesets/repository", "version": "1.0.0", "private": true, - "description": "A tool to help manage the versioning and changelogs for open source packages", - "scripts": { - "test": "jest", - "build": "preconstruct build", - "watch": "preconstruct watch", - "postinstall": "preconstruct dev && manypkg check", - "lint": "yarn eslint . --ext .ts,.tsx,.js", - "types:check": "tsc", - "format": "prettier --list-different \"**/*.{js,ts,tsx,md}\"", - "format:fix": "prettier --write \"**/*.{js,ts,tsx,md}\"", - "changeset": "packages/cli/bin.js", - "check-all": "yarn test && yarn types:check && yarn lint && yarn format", - "version-packages": "changeset version && yarn format:fix", - "release": "yarn build && changeset publish" - }, + "description": "A tool to manage versioning and changelogs with a focus on monorepos", + "homepage": "https://changesets.dev", + "license": "MIT", + "author": "Changesets Contributors", + "contributors": [ + "Ben Conolly", + "Mitchell Hamilton", + "Mateusz Burzyński (https://github.com/Andarist)" + ], "repository": { "type": "git", - "url": "https://github.com/changesets/changesets.git" + "url": "git+https://github.com/changesets/changesets.git" }, - "packageManager": "yarn@1.22.22", "workspaces": [ "packages/*", "scripts/*" ], - "author": "Changesets Contributors", - "contributors": [ - "Ben Conolly", - "Mitchell Hamilton", - "Mateusz Burzyński (https://github.com/Andarist)" - ], - "license": "MIT", - "dependencies": { - "@babel/cli": "^7.27.0", - "@babel/core": "^7.26.10", - "@babel/preset-env": "^7.26.9", - "@babel/preset-typescript": "^7.27.0", - "@manypkg/cli": "^0.22.0", - "@preconstruct/cli": "^2.8.12", - "@types/fs-extra": "^5.1.0", - "@types/jest": "^24.0.12", - "@types/jest-in-case": "^1.0.6", - "@types/js-yaml": "^3.12.1", - "@types/lodash": "^4.17.13", - "@types/node": "^22.14.1", - "@types/prettier": "^2.7.1", - "@types/semver": "^7.5.0", - "@typescript-eslint/eslint-plugin": "^5.43.0", - "@typescript-eslint/parser": "^5.62.0", - "eslint": "^8.28.0", - "eslint-config-prettier": "^8.5.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-n": "^15.5.1", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-standard": "^5.0.0", - "jest": "^29.3.1", - "jest-junit": "^15.0.0", - "jest-watch-typeahead": "^2.2.2", - "prettier": "^2.7.1", - "typescript": "^5.8.3" + "type": "module", + "scripts": { + "postinstall": "manypkg check", + "build": "tsdown", + "watch": "tsdown --watch", + "format": "oxfmt --check", + "format:fix": "oxfmt", + "generate-json-schema": "node packages/config/scripts/generate-json-schema.ts", + "lint": "eslint", + "test": "vitest", + "test:fast": "vitest --tags-filter=!slow", + "types:check": "tsc", + "check-all": "pnpm build && pnpm test run && pnpm types:check && pnpm lint && pnpm format", + "version-packages": "changeset version && pnpm format:fix", + "lint-staged:setup": "pnpm run --sequential \"/lint-staged:setup:\\d/\"", + "lint-staged:setup:1": "git config set hook.\"lint-staged\".event pre-commit", + "lint-staged:setup:2": "git config set hook.\"lint-staged\".command \"node node_modules/lint-staged/bin/lint-staged.js\"" + }, + "devDependencies": { + "@changesets/changelog-github": "workspace:*", + "@changesets/cli": "workspace:*", + "@e18e/eslint-plugin": "^0.5.1", + "@eslint/js": "10.0.1", + "@manypkg/cli": "^0.25.1", + "@tsconfig/node22": "^22.0.5", + "@types/node": "^25.9.3", + "@types/semver": "^7.7.1", + "@vitest/coverage-v8": "^4.1.9", + "@vitest/eslint-plugin": "^1.6.20", + "eslint": "^10.5.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-lite": "~0.6.0", + "eslint-plugin-n": "^18.1.0", + "lint-staged": "^17.0.7", + "oxfmt": "^0.54.0", + "pkg-pr-new": "^0.0.75", + "publint": "^0.3.21", + "tsdown": "^0.22.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.0", + "vitest": "^4.1.9" + }, + "lint-staged": { + "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json,json5,yaml,yml,css,scss,md}": [ + "oxfmt --write --no-error-on-unmatched-pattern" + ] }, - "preconstruct": { - "packages": [ - "packages/*", - "scripts/*" - ], - "exports": { - "importConditionDefaultExport": "default" - }, - "___experimentalFlags_WILL_CHANGE_IN_PATCH": { - "importsConditions": true - } + "engines": { + "node": "^22.11 || ^24 || >=26" }, - "prettier": {} + "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916" } diff --git a/packages/apply-release-plan/CHANGELOG.md b/packages/apply-release-plan/CHANGELOG.md index e36ae2fb3..a62c7d771 100644 --- a/packages/apply-release-plan/CHANGELOG.md +++ b/packages/apply-release-plan/CHANGELOG.md @@ -1,5 +1,163 @@ # @changesets/apply-release-plan +## 8.0.0-next.6 + +### Minor Changes + +- [#2070](https://github.com/changesets/changesets/pull/2070) [`694396c`](https://github.com/changesets/changesets/commit/694396ce49f0d7e2200c119b360e60e6bd11265f) Thanks [@bluwy](https://github.com/bluwy)! - Preserve the existing formatting of `package.json` when updating version and dependency ranges + +- [#2118](https://github.com/changesets/changesets/pull/2118) [`01f4da4`](https://github.com/changesets/changesets/commit/01f4da4e30aa90391def46b84b986fa223a055f5) Thanks [@bluwy](https://github.com/bluwy)! - Improve the default unformatted changelog new lines handling to ensure consistent spacing after the heading and the spacing between release lines + +## 8.0.0-next.5 + +### Patch Changes + +- Updated dependencies [[`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf), [`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf)]: + - @changesets/config@4.0.0-next.5 + - @changesets/types@7.0.0-next.5 + - @changesets/git@4.0.0-next.5 + - @changesets/should-skip-package@1.0.0-next.5 + +## 8.0.0-next.4 + +### Major Changes + +- [#1994](https://github.com/changesets/changesets/pull/1994) [`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8) Thanks [@bluwy](https://github.com/bluwy)! - Generated changelog entries and rewritten `package.json` files are now formatted with [@changesets/format](https://github.com/changesets/format) instead of depending on Prettier directly. Formatter selection can be auto-detected from the project configuration or controlled via the `format` config option. + +### Minor Changes + +- [#1989](https://github.com/changesets/changesets/pull/1989) [`ee10723`](https://github.com/changesets/changesets/commit/ee10723dde491ba6632da74d10876dfa2e67d0d2) Thanks [@43081j](https://github.com/43081j)! - Remove support for the legacy Changeset v1 format. + +### Patch Changes + +- [#2006](https://github.com/changesets/changesets/pull/2006) [`fc42514`](https://github.com/changesets/changesets/commit/fc425143294e63ba254ddbe8c2ea026b55a05991) Thanks [@bluwy](https://github.com/bluwy)! - Remove `@changesets/get-version-range-type` dependency + +- Updated dependencies [[`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8), [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc)]: + - @changesets/config@4.0.0-next.4 + - @changesets/types@7.0.0-next.4 + - @changesets/git@4.0.0-next.4 + - @changesets/should-skip-package@1.0.0-next.4 + +## 8.0.0-next.3 + +### Major Changes + +- [#1954](https://github.com/changesets/changesets/pull/1954) [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped supported Node versions to `^22.11 || ^24 || >=26` + +### Minor Changes + +- [#1969](https://github.com/changesets/changesets/pull/1969) [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Add a named export that mirrors the current `default` export + + The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. + +### Patch Changes + +- [#1667](https://github.com/changesets/changesets/pull/1667) [`81832f8`](https://github.com/changesets/changesets/commit/81832f855029f4093b2142ba22b747ba0de92425) Thanks [@Andarist](https://github.com/Andarist)! - Fixed resolution of changelog and commit generator modules so built-in modules can still be loaded when they are not installed in the target project. + +- [#1985](https://github.com/changesets/changesets/pull/1985) [`ad3edbd`](https://github.com/changesets/changesets/commit/ad3edbdc78c7b2ba451577969b6137df275ec430) Thanks [@bluwy](https://github.com/bluwy)! - Update `detect-indent` package to v7 + +- Updated dependencies [[`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069), [`b9407b3`](https://github.com/changesets/changesets/commit/b9407b39a458bab106d0e23a3afab01d07d8482f), [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625), [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7)]: + - @changesets/get-version-range-type@1.0.0-next.2 + - @changesets/should-skip-package@1.0.0-next.3 + - @changesets/config@4.0.0-next.3 + - @changesets/types@7.0.0-next.3 + - @changesets/git@4.0.0-next.3 + +## 8.0.0-next.2 + +### Major Changes + +- [#1655](https://github.com/changesets/changesets/pull/1655) [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2) Thanks [@bluwy](https://github.com/bluwy)! - Update `@manypkg/get-packages` which drops support for detecting packages in Bolt monorepos and adds support for npm monorepos + +### Minor Changes + +- [#1744](https://github.com/changesets/changesets/pull/1744) [`303cacd`](https://github.com/changesets/changesets/commit/303cacdde85c94f2ef4d1408b401165ff25d263d) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped the default Prettier version used in the absence of the local installation to v3 + +### Patch Changes + +- [#1875](https://github.com/changesets/changesets/pull/1875) [`12f20ea`](https://github.com/changesets/changesets/commit/12f20ea75fb5a440a378bd2bf6072a6bd749fd57) Thanks [@beeequeue](https://github.com/beeequeue)! - Replaced `spawndamnit` with `tinyexec` + +- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c), [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2), [`12f20ea`](https://github.com/changesets/changesets/commit/12f20ea75fb5a440a378bd2bf6072a6bd749fd57)]: + - @changesets/types@7.0.0-next.2 + - @changesets/config@4.0.0-next.2 + - @changesets/git@4.0.0-next.2 + - @changesets/should-skip-package@1.0.0-next.2 + +## 7.1.1 + +### Patch Changes + +- [#1888](https://github.com/changesets/changesets/pull/1888) [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix workspace protocol dependency updates for explicit ranges, aliases, and path references. Valid `workspace:` dependency forms are now preserved and only rewritten when the referenced release leaves the supported range or path. + +- Updated dependencies []: + - @changesets/config@3.1.4 + +## 7.1.0 + +### Minor Changes + +- [#1774](https://github.com/changesets/changesets/pull/1774) [`667fe5a`](https://github.com/changesets/changesets/commit/667fe5aacf04dbefcf2532584ff2753b8417855a) Thanks [@bluwy](https://github.com/bluwy)! - Support importing custom `changelog` option ES module. Previously, it used `require()` which only worked for CJS modules, however now it uses `import()` which supports both CJS and ES modules. + +### Patch Changes + +- [#1859](https://github.com/changesets/changesets/pull/1859) [`1772598`](https://github.com/changesets/changesets/commit/1772598270a59ba1fa7b0ef7e675fce6a575f850) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix changelog entry insertion when no package title is present in the `CHANGELOG.md` file. + +- [#1810](https://github.com/changesets/changesets/pull/1810) [`27fd8f4`](https://github.com/changesets/changesets/commit/27fd8f41dddafcc2e96e7df39dca04d92f916a0a) Thanks [@hirasso](https://github.com/hirasso)! - Replace deprecated `String.prototype.trimRight` with [`String.prototype.trimEnd`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd) + +- Updated dependencies [[`b6f4c74`](https://github.com/changesets/changesets/commit/b6f4c748c4ba50b5ac608f3ce41229526d1bfe94), [`6df3a5e`](https://github.com/changesets/changesets/commit/6df3a5e95522a0210cb2b5619588a75f32b502c6), [`6df3a5e`](https://github.com/changesets/changesets/commit/6df3a5e95522a0210cb2b5619588a75f32b502c6)]: + - @changesets/config@3.1.3 + +## 7.0.14 + +### Patch Changes + +- [#610](https://github.com/changesets/changesets/pull/610) [`e520bf5`](https://github.com/changesets/changesets/commit/e520bf5d4dbfe96f59ca28008e87bffaf3c9dfea) Thanks [@bencergazda](https://github.com/bencergazda)! - Add `pre.json` to the version commit + +- Updated dependencies [[`cc28222`](https://github.com/changesets/changesets/commit/cc28222ee892b3a078fa02ee26e1cef98c171532), [`13dace8`](https://github.com/changesets/changesets/commit/13dace895017fa351014bc9e13b544d33f8b4bbe)]: + - @changesets/config@3.1.2 + +## 7.0.13 + +### Patch Changes + +- [#1725](https://github.com/changesets/changesets/pull/1725) [`957f24e`](https://github.com/changesets/changesets/commit/957f24ed0446494c5709189ae57583f72c716d43) Thanks [@colinaaa](https://github.com/colinaaa)! - Fix an issue that caused an incorrect `CHANGELOG` to be generated when a changeset contained a special string replacement pattern. + +## 8.0.0-next.1 + +### Major Changes + +- [#1656](https://github.com/changesets/changesets/pull/1656) [`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d) Thanks [@bluwy](https://github.com/bluwy)! - Bumps minimum node version to `>=20.0.0` + +### Patch Changes + +- Updated dependencies [[`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d), [`b83787f`](https://github.com/changesets/changesets/commit/b83787fb090dc03ad566a7d8b7e286dbe93e2301)]: + - @changesets/get-version-range-type@1.0.0-next.1 + - @changesets/should-skip-package@1.0.0-next.1 + - @changesets/config@4.0.0-next.1 + - @changesets/types@7.0.0-next.1 + - @changesets/git@4.0.0-next.1 + +## 8.0.0-next.0 + +### Major Changes + +- [#1479](https://github.com/changesets/changesets/pull/1479) [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5) Thanks [@bluwy](https://github.com/bluwy)! - Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. + +- [#1482](https://github.com/changesets/changesets/pull/1482) [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7) Thanks [@Andarist](https://github.com/Andarist)! - From now on this package is going to be published as ES module. + +### Patch Changes + +- [#1476](https://github.com/changesets/changesets/pull/1476) [`e0e1748`](https://github.com/changesets/changesets/commit/e0e1748369b1f936c665b62590a76a0d57d1545e) Thanks [@pralkarz](https://github.com/pralkarz)! - Replace `fs-extra` usage with `node:fs` + +- [#1617](https://github.com/changesets/changesets/pull/1617) [`8f7b607`](https://github.com/changesets/changesets/commit/8f7b607b486e299e038bf8e257d28f0193ac4412) Thanks [@bluwy](https://github.com/bluwy)! - Move `outdent` as a dev dependency + +- Updated dependencies [[`e0e1748`](https://github.com/changesets/changesets/commit/e0e1748369b1f936c665b62590a76a0d57d1545e), [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5), [`3628cab`](https://github.com/changesets/changesets/commit/3628cab6cbfd931b7f2a909b38b66c1aa794d4bf), [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7)]: + - @changesets/config@4.0.0-next.0 + - @changesets/git@4.0.0-next.0 + - @changesets/get-version-range-type@1.0.0-next.0 + - @changesets/should-skip-package@1.0.0-next.0 + - @changesets/types@7.0.0-next.0 + ## 7.0.12 ### Patch Changes @@ -481,7 +639,6 @@ - [`a679b1d`](https://github.com/changesets/changesets/commit/a679b1dcdcb56652d31536e2d6326ba02a9dfe62) [#204](https://github.com/changesets/changesets/pull/204) Thanks [@Andarist](https://github.com/Andarist)! - Correctly handle the 'access' flag for packages Previously, we had access as "public" or "private", access "private" isn't valid. This was a confusing because there are three states for publishing a package: - - `private: true` - the package will not be published to npm (worked) - `access: public` - the package will be publicly published to npm (even if it uses a scope) (worked) - `access: restricted` - the package will be published to npm, but only visible/accessible by those who are part of the scope. This technically worked, but we were passing the wrong bit of information in. diff --git a/packages/apply-release-plan/README.md b/packages/apply-release-plan/README.md index dc1572f37..9cbada211 100644 --- a/packages/apply-release-plan/README.md +++ b/packages/apply-release-plan/README.md @@ -1,21 +1,22 @@ # Apply Release Plan -[![npm package](https://img.shields.io/npm/v/@changesets/apply-release-plan)](https://npmjs.com/package/@changesets/apply-release-plan) -[![View changelog](https://img.shields.io/badge/Explore%20Changelog-brightgreen)](./CHANGELOG.md) +[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/@changesets/apply-release-plan?name=true)](https://npmx.dev/package/@changesets/apply-release-plan) +[![View changelog](https://npmx.dev/api/registry/badge/version/@changesets/cli?color=229fe4&value=View+changelog&label=+)](./CHANGELOG.md) This takes a `releasePlan` object for changesets and applies the expected changes from that release. This includes updating package versions, and updating changelogs. ```ts import applyReleasePlan from "@changesets/apply-release-plan"; -import { ReleasePlan, Config } from "@changesets/types"; -import { Packages } from '@manypkg/get-packages' +import type { ReleasePlan, Config, Packages } from "@changesets/types"; await applyReleasePlan( // The release plan to be applied - see @changesets/types for information about its shape - aReleasePlan: ReleasePlan, - // The packages that applyReleasePlan should be run for from @manypkg/get-packages + releasePlan: ReleasePlan, + + // All information about to the repository packages - see @changesets/types for information about its shape packages: Packages, + // A valid @changesets/config config - see @changesets/types for information about its shape config: Config ); diff --git a/packages/apply-release-plan/package.json b/packages/apply-release-plan/package.json index b020e7e91..ab67b6327 100644 --- a/packages/apply-release-plan/package.json +++ b/packages/apply-release-plan/package.json @@ -1,40 +1,34 @@ { "name": "@changesets/apply-release-plan", - "version": "7.0.12", + "version": "8.0.0-next.6", "description": "Takes a release plan and applies it to packages", - "main": "dist/changesets-apply-release-plan.cjs.js", - "module": "dist/changesets-apply-release-plan.esm.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/changesets/changesets.git", + "directory": "packages/apply-release-plan" + }, + "type": "module", "exports": { - ".": { - "types": { - "import": "./dist/changesets-apply-release-plan.cjs.mjs", - "default": "./dist/changesets-apply-release-plan.cjs.js" - }, - "module": "./dist/changesets-apply-release-plan.esm.js", - "import": "./dist/changesets-apply-release-plan.cjs.mjs", - "default": "./dist/changesets-apply-release-plan.cjs.js" - }, + ".": "./dist/index.mjs", "./package.json": "./package.json" }, - "license": "MIT", - "repository": "https://github.com/changesets/changesets/tree/main/packages/apply-release-plan", "dependencies": { - "@changesets/config": "^3.1.1", - "@changesets/get-version-range-type": "^0.4.0", - "@changesets/git": "^3.0.4", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "detect-indent": "^6.0.0", - "fs-extra": "^7.0.1", - "lodash.startcase": "^4.4.0", - "outdent": "^0.5.0", - "prettier": "^2.7.1", - "resolve-from": "^5.0.0", - "semver": "^7.5.3" + "@changesets/config": "workspace:^", + "@changesets/format": "^0.1.0", + "@changesets/git": "workspace:^", + "@changesets/should-skip-package": "workspace:^", + "@changesets/types": "workspace:^", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" }, "devDependencies": { - "@changesets/test-utils": "*", - "spawndamnit": "^3.0.1" + "@changesets/test-utils": "workspace:*", + "@manypkg/get-packages": "^3.1.0", + "tinyexec": "^1.2.4" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } } diff --git a/packages/apply-release-plan/src/edit-json.test.ts b/packages/apply-release-plan/src/edit-json.test.ts new file mode 100644 index 000000000..057f7836d --- /dev/null +++ b/packages/apply-release-plan/src/edit-json.test.ts @@ -0,0 +1,107 @@ +import { expect, it } from "vitest"; +import { editJson } from "./edit-json.ts"; + +it("updates a direct value", () => { + const json = `{"name":"pkg-a","version":"1.0.0"}`; + + const result = editJson(json, [ + { + keys: ["version"], + value: "^2.0.0", + }, + ]); + + expect(result).toMatchInlineSnapshot(`"{"name":"pkg-a","version":"^2.0.0"}"`); +}); + +it("updates a nested value", () => { + const json = `{"name":"pkg-a","version":"1.0.0","dependencies":{"pkg-b":"^1.0.0"}}`; + + const result = editJson(json, [ + { + keys: ["dependencies", "pkg-b"], + value: "^2.0.0", + }, + ]); + + expect(result).toMatchInlineSnapshot( + `"{"name":"pkg-a","version":"1.0.0","dependencies":{"pkg-b":"^2.0.0"}}"`, + ); +}); + +it("updates a multiple values", () => { + const json = `{"name":"pkg-a","version":"1.0.0","dependencies":{"pkg-b":"^1.0.0"}}`; + + const result = editJson(json, [ + { + keys: ["version"], + value: "2.0.0-longer-than-before.0", + }, + { + keys: ["dependencies", "pkg-b"], + value: "^2.0.0", + }, + ]); + + expect(result).toMatchInlineSnapshot( + `"{"name":"pkg-a","version":"2.0.0-longer-than-before.0","dependencies":{"pkg-b":"^2.0.0"}}"`, + ); +}); + +it("preserves formatting", () => { + const json = ` {"name" :"pkg-a" ,"version":"1.0.0"} `; + + const result = editJson(json, [ + { + keys: ["version"], + value: "^2.0.0", + }, + ]); + + expect(result).toMatchInlineSnapshot( + `" {"name" :"pkg-a" ,"version":"^2.0.0"} "`, + ); +}); + +it("preserves bizarre formatting", () => { + const json = `{\n\t\n\n\n"a"\n:\t\t\n{"b":{"c":"^1.0.0"}}\n\n\n, \t \n\t"name":"pkg-a"\n\n\n\n\n}\t\t\t\t`; + + const result = editJson(json, [ + { + keys: ["a", "b", "c"], + value: "^2.0.0", + }, + ]); + + expect(result).toEqual(json.replace(`"^1.0.0"`, `"^2.0.0"`)); +}); + +it("throws when a key path does not exist", () => { + const json = `{"name":"pkg-a"}`; + + expect(() => { + editJson(json, [ + { + keys: ["version"], + value: "1.1.0", + }, + ]); + }).toThrow('Key path "version" not found in JSON'); +}); + +it("throws on malformed JSON", () => { + const jsons = [`{{{`, ``, `{//comment\n"version":"1.0.0"}`]; + + expect.assertions(jsons.length); + + for (const json of jsons) { + expect(() => { + editJson(json, [ + { + keys: ["version"], + value: "2.0.0", + }, + ]); + }).toThrow(/Failed to parse JSON/); + } +}); diff --git a/packages/apply-release-plan/src/edit-json.ts b/packages/apply-release-plan/src/edit-json.ts new file mode 100644 index 000000000..dda3276c8 --- /dev/null +++ b/packages/apply-release-plan/src/edit-json.ts @@ -0,0 +1,71 @@ +import { + applyEdits, + parseTree, + printParseErrorCode, + type EditResult, + type Node, + type ParseError, +} from "jsonc-parser"; + +export interface EditJsonOperation { + keys: string[]; + value: unknown; +} + +/** + * A simple JSON editing utility that preserves formatting. They specified operation keys + * must exist in the JSON for this implementation. + */ +export function editJson( + json: string, + operations: EditJsonOperation[], +): string { + const errors: ParseError[] = []; + const parsed = parseTree(json, errors, { + allowEmptyContent: false, + allowTrailingComma: false, + disallowComments: true, + }); + + if (!parsed) { + throw new Error("Failed to parse JSON"); + } + if (errors.length > 0) { + // Since the first error could cause subsequent errors, we only report the first one + const error = errors[0]; + throw new Error( + `Failed to parse JSON at offset ${error.offset}: ${printParseErrorCode(error.error)}`, + ); + } + + const edits: EditResult = operations.map((op) => { + const valueNode = getValueNode(parsed, op.keys); + if (!valueNode) { + throw new Error(`Key path "${op.keys.join(".")}" not found in JSON`); + } + return { + content: JSON.stringify(op.value), + offset: valueNode.offset, + length: valueNode.length, + }; + }); + + return applyEdits(json, edits); +} + +function getValueNode(root: Node, keys: string[]): Node | null { + let node = root; + for (const key of keys) { + if (node.type !== "object") return null; + const property = node.children?.find( + (child) => + child.type === "property" && + child.children?.length === 2 && + child.children[0].value === key, + ); + if (!property) return null; + // We've checked above that `children[1]` exists + node = property.children![1]; + } + return node; +} diff --git a/packages/apply-release-plan/src/get-changelog-entry.test.ts b/packages/apply-release-plan/src/get-changelog-entry.test.ts new file mode 100644 index 000000000..f5f651182 --- /dev/null +++ b/packages/apply-release-plan/src/get-changelog-entry.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { generateMarkdownForVersionType } from "./get-changelog-entry.ts"; + +describe("generateMarkdownForVersionType", () => { + it("returns undefined when there are empty lines", () => { + expect(generateMarkdownForVersionType("patch", ["", ""])).toBeUndefined(); + }); + + it("returns proper heading based on version type", () => { + expect.soft(generateMarkdownForVersionType("major", ["- something"])) + .toMatchInlineSnapshot(` + "### Major Changes + + - something" + `); + expect.soft(generateMarkdownForVersionType("minor", ["- something"])) + .toMatchInlineSnapshot(` + "### Minor Changes + + - something" + `); + expect.soft(generateMarkdownForVersionType("patch", ["- something"])) + .toMatchInlineSnapshot(` + "### Patch Changes + + - something" + `); + }); + + it("trims surrounding whitespace from release lines", () => { + expect(generateMarkdownForVersionType("minor", ["\n - something \n"])) + .toMatchInlineSnapshot(` + "### Minor Changes + + - something" + `); + }); + + it("keeps preferred spacing between entries clamped between one and two new lines", () => { + expect( + generateMarkdownForVersionType("patch", [ + "trimmed", + "\nleading one", + "\n\nleading two", + "\n\n\nleading three", + "trailing one\n", + "trailing two\n\n", + "trailing three\n\n\n", + "\nmixed one\n", + "\n\nmixed two\n\n", + "\n\n\nmixed three\n\n\n", + ]), + ).toMatchInlineSnapshot(` + "### Patch Changes + + trimmed + leading one + + leading two + + leading three + trailing one + trailing two + + trailing three + + mixed one + + mixed two + + mixed three" + `); + }); +}); diff --git a/packages/apply-release-plan/src/get-changelog-entry.ts b/packages/apply-release-plan/src/get-changelog-entry.ts index 12f991305..975e78996 100644 --- a/packages/apply-release-plan/src/get-changelog-entry.ts +++ b/packages/apply-release-plan/src/get-changelog-entry.ts @@ -1,9 +1,10 @@ -import { ChangelogFunctions, NewChangesetWithCommit } from "@changesets/types"; - -import { ModCompWithPackage } from "@changesets/types"; -import startCase from "lodash.startcase"; -import { shouldUpdateDependencyBasedOnConfig } from "./utils"; -import validRange from "semver/ranges/valid"; +import type { + ChangelogFunctions, + ModCompWithPackage, + NewChangesetWithCommit, +} from "@changesets/types"; +import validRange from "semver/ranges/valid.js"; +import { capitalize, shouldUpdateDependencyBasedOnConfig } from "./utils.ts"; type ChangelogLines = { major: Array>; @@ -11,31 +12,21 @@ type ChangelogLines = { patch: Array>; }; -async function generateChangesForVersionTypeMarkdown( - obj: ChangelogLines, - type: keyof ChangelogLines -) { - let releaseLines = await Promise.all(obj[type]); - releaseLines = releaseLines.filter((x) => x); - if (releaseLines.length) { - return `### ${startCase(type)} Changes\n\n${releaseLines.join("\n")}\n`; - } -} - // release is the package and version we are releasing -export default async function getChangelogEntry( +export async function getChangelogEntry( + cwd: string, release: ModCompWithPackage, releases: ModCompWithPackage[], changesets: NewChangesetWithCommit[], changelogFuncs: ChangelogFunctions, - changelogOpts: any, + changelogOpts: null | Record, { updateInternalDependencies, onlyUpdatePeerDependentsWhenOutOfRange, }: { updateInternalDependencies: "patch" | "minor"; onlyUpdatePeerDependentsWhenOutOfRange: boolean; - } + }, ) { if (release.type === "none") return null; @@ -53,11 +44,13 @@ export default async function getChangelogEntry( const rls = cs.releases.find((r) => r.name === release.name); if (rls && rls.type !== "none") { changelogLines[rls.type].push( - changelogFuncs.getReleaseLine(cs, rls.type, changelogOpts) + Promise.resolve( + changelogFuncs.getReleaseLine(cs, rls.type, changelogOpts), + ), ); } }); - let dependentReleases = releases.filter((rel) => { + const dependentReleases = releases.filter((rel) => { const dependencyVersionRange = release.packageJson.dependencies?.[rel.name]; const peerDependencyVersionRange = release.packageJson.peerDependencies?.[rel.name]; @@ -66,9 +59,15 @@ export default async function getChangelogEntry( const usesWorkspaceRange = versionRange?.startsWith("workspace:"); return ( versionRange && - (usesWorkspaceRange || validRange(versionRange) !== null) && + (usesWorkspaceRange || validRange(versionRange) != null) && shouldUpdateDependencyBasedOnConfig( - { type: rel.type, version: rel.newVersion }, + cwd, + { + type: rel.type, + version: rel.newVersion, + oldVersion: rel.oldVersion, + dir: rel.dir, + }, { depVersionRange: versionRange, depType: dependencyVersionRange ? "dependencies" : "peerDependencies", @@ -76,12 +75,12 @@ export default async function getChangelogEntry( { minReleaseType: updateInternalDependencies, onlyUpdatePeerDependentsWhenOutOfRange, - } + }, ) ); }); - let relevantChangesetIds: Set = new Set(); + const relevantChangesetIds: Set = new Set(); dependentReleases.forEach((rel) => { rel.changesets.forEach((cs) => { @@ -89,24 +88,62 @@ export default async function getChangelogEntry( }); }); - let relevantChangesets = changesets.filter((cs) => - relevantChangesetIds.has(cs.id) + const relevantChangesets = changesets.filter((cs) => + relevantChangesetIds.has(cs.id), ); changelogLines.patch.push( - changelogFuncs.getDependencyReleaseLine( - relevantChangesets, - dependentReleases, - changelogOpts - ) + Promise.resolve( + changelogFuncs.getDependencyReleaseLine( + relevantChangesets, + dependentReleases, + changelogOpts, + ), + ), ); + const resolvedChangelogLines = { + major: await Promise.all(changelogLines.major), + minor: await Promise.all(changelogLines.minor), + patch: await Promise.all(changelogLines.patch), + }; + return [ `## ${release.newVersion}`, - await generateChangesForVersionTypeMarkdown(changelogLines, "major"), - await generateChangesForVersionTypeMarkdown(changelogLines, "minor"), - await generateChangesForVersionTypeMarkdown(changelogLines, "patch"), + generateMarkdownForVersionType("major", resolvedChangelogLines.major), + generateMarkdownForVersionType("minor", resolvedChangelogLines.minor), + generateMarkdownForVersionType("patch", resolvedChangelogLines.patch), ] .filter((line) => line) - .join("\n"); + .join("\n\n"); +} + +// Exported for test only +export function generateMarkdownForVersionType( + type: keyof ChangelogLines, + lines: Array, +) { + const releaseLines = lines.filter((l) => l); + if (!releaseLines.length) return; + + let content = `### ${capitalize(type)} Changes`; + // Track the new lines to be added between release lines. Start with two as we + // want the extra spacing after the heading. + let newLines = 2; + + for (const line of releaseLines) { + // Factor in the starting new lines preferred by the release line + const startNewLinesCount = line.match(/^\n*/)?.[0].length ?? 0; + newLines += startNewLinesCount; + + // Ensure a minimum of one new line and maximum of two new lines between release lines + const newLinesContent = "\n".repeat(Math.min(Math.max(newLines, 1), 2)); + content += newLinesContent + line.trim(); + + // Count the ending new lines preferred by the release line for the next run + const endNewLinesCount = line.match(/\n*$/)?.[0].length ?? 0; + newLines = endNewLinesCount; + } + + return content; } diff --git a/packages/apply-release-plan/src/index.test.ts b/packages/apply-release-plan/src/index.test.ts index 8103e5d1f..c7a7f24c7 100644 --- a/packages/apply-release-plan/src/index.test.ts +++ b/packages/apply-release-plan/src/index.test.ts @@ -1,23 +1,34 @@ -import { - ReleasePlan, - Config, - NewChangeset, - ComprehensiveRelease, -} from "@changesets/types"; -import * as git from "@changesets/git"; -import fs from "fs-extra"; -import path from "path"; -import outdent from "outdent"; -import spawn from "spawndamnit"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; import { defaultConfig } from "@changesets/config"; - -import applyReleasePlan from "./"; -import { getPackages } from "@manypkg/get-packages"; +import * as git from "@changesets/git"; import { + type Fixture, + outputFile, temporarilySilenceLogs, testdir, - Fixture, } from "@changesets/test-utils"; +import type { + ComprehensiveRelease, + Config, + NewChangeset, + ReleasePlan, + PreState, +} from "@changesets/types"; +import { getPackages } from "@manypkg/get-packages"; +import { exec } from "tinyexec"; +import { describe, expect, it, test } from "vitest"; +import { applyReleasePlan } from "./index.ts"; + +const changesetsCliChangelogPath = path.resolve( + import.meta.dirname, + "../../cli/dist/changelog.mjs", +); +const changesetsCliCommitPath = path.resolve( + import.meta.dirname, + "../../cli/dist/commit.mjs", +); class FakeReleasePlan { changesets: NewChangeset[]; @@ -27,7 +38,7 @@ class FakeReleasePlan { constructor( changesets: NewChangeset[] = [], releases: ComprehensiveRelease[] = [], - config: Partial = {} + config: Partial = {}, ) { const baseChangeset: NewChangeset = { id: "quick-lions-devour", @@ -51,7 +62,7 @@ class FakeReleasePlan { baseBranch: "main", updateInternalDependencies: "patch", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -81,8 +92,8 @@ async function testSetup( fixture: Fixture, releasePlan: ReleasePlan, config?: Config, - snapshot?: string | undefined, - setupFunc?: (tempDir: string) => Promise + snapshot?: string, + setupFunc?: (tempDir: string) => Promise, ) { if (!config) { config = { @@ -95,7 +106,7 @@ async function testSetup( baseBranch: "main", updateInternalDependencies: "patch", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, snapshot: { useCalculatedVersion: false, @@ -107,35 +118,41 @@ async function testSetup( }, }; } - let tempDir = await testdir(fixture); + const tempDir = await testdir(fixture); if (setupFunc) { await setupFunc(tempDir); } if (config.commit) { - await spawn("git", ["init"], { cwd: tempDir }); + await exec("git", ["init"], { nodeOptions: { cwd: tempDir } }); await git.add(".", tempDir); await git.commit("first commit", tempDir); } + const packages = await getPackages(tempDir); + return { changedFiles: await applyReleasePlan( releasePlan, - await getPackages(tempDir), + packages, config, - snapshot + snapshot, ), tempDir, }; } +async function readJson(path: string) { + return JSON.parse(await fs.readFile(path, "utf8")); +} + describe("apply release plan", () => { describe("versioning", () => { describe("formatting", () => { it("should not reformat a small array in a package.json", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": `{ "name": "pkg-a", @@ -146,12 +163,12 @@ describe("apply release plan", () => { }`, }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); + const pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); + const pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", @@ -163,7 +180,7 @@ describe("apply release plan", () => { }); it("should not change tab indentation in a package.json", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify( { @@ -171,16 +188,16 @@ describe("apply release plan", () => { version: "1.0.0", }, null, - "\t" + "\t", ), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); + const pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); + const pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ \t"name": "pkg-a", @@ -189,20 +206,24 @@ describe("apply release plan", () => { }); it("should not add trailing newlines in a package.json if they don't exist", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { - "package.json": JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - }), + "package.json": JSON.stringify( + { + name: "pkg-a", + version: "1.0.0", + }, + null, + 2, + ), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); + const pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); + const pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", @@ -211,21 +232,25 @@ describe("apply release plan", () => { }); it("should not remove trailing newlines in a package.json if they exist", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": - JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - }) + "\n", + JSON.stringify( + { + name: "pkg-a", + version: "1.0.0", + }, + null, + 2, + ) + "\n", }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); + const pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); + const pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", @@ -236,32 +261,34 @@ describe("apply release plan", () => { it("should update a version for one package", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readJSON(pkgPath); + const pkgJSON = await readJson(pkgPath); expect(pkgJSON).toMatchObject({ name: "pkg-a", version: "1.1.0", }); }); + it("should not update ranges set to *", async () => { const releasePlan = new FakeReleasePlan( [ @@ -279,14 +306,15 @@ describe("apply release plan", () => { oldVersion: "1.0.0", type: "minor", }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -300,14 +328,14 @@ describe("apply release plan", () => { }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readJSON(pkgPath); + const pkgJSON = await readJson(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", @@ -317,6 +345,7 @@ describe("apply release plan", () => { }, }); }); + it("should update workspace ranges", async () => { const releasePlan = new FakeReleasePlan( [ @@ -334,14 +363,15 @@ describe("apply release plan", () => { oldVersion: "1.0.0", type: "minor", }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -355,14 +385,14 @@ describe("apply release plan", () => { }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readJSON(pkgPath); + const pkgJSON = await readJson(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", @@ -372,6 +402,7 @@ describe("apply release plan", () => { }, }); }); + it("should not update workspace version aliases", async () => { const releasePlan = new FakeReleasePlan( [ @@ -413,14 +444,15 @@ describe("apply release plan", () => { oldVersion: "1.0.0", type: "minor", }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -444,14 +476,14 @@ describe("apply release plan", () => { }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readJSON(pkgPath); + const pkgJSON = await readJson(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", @@ -463,6 +495,7 @@ describe("apply release plan", () => { }, }); }); + it("should update workspace ranges only with bumpVersionsWithWorkspaceProtocolOnly", async () => { const releasePlan = new FakeReleasePlan( [ @@ -493,14 +526,15 @@ describe("apply release plan", () => { ], { bumpVersionsWithWorkspaceProtocolOnly: true, - } + }, ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -521,14 +555,14 @@ describe("apply release plan", () => { }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgAPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgAPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgAPath) throw new Error(`could not find an updated package json`); - let pkgAJSON = await fs.readJSON(pkgAPath); + const pkgAJSON = await readJson(pkgAPath); expect(pkgAJSON).toEqual({ name: "pkg-a", @@ -538,12 +572,12 @@ describe("apply release plan", () => { }, }); - let pkgCPath = changedFiles.find((a) => - a.endsWith(`pkg-c${path.sep}package.json`) + const pkgCPath = changedFiles.find((a) => + a.endsWith(`pkg-c${path.sep}package.json`), ); if (!pkgCPath) throw new Error(`could not find an updated package json`); - let pkgCJSON = await fs.readJSON(pkgCPath); + const pkgCJSON = await readJson(pkgCPath); expect(pkgCJSON).toEqual({ name: "pkg-c", @@ -553,6 +587,7 @@ describe("apply release plan", () => { }, }); }); + it("should update a version for two packages with different new versions", async () => { const releasePlan = new FakeReleasePlan( [], @@ -564,15 +599,16 @@ describe("apply release plan", () => { newVersion: "2.0.0", changesets: [], }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -586,20 +622,20 @@ describe("apply release plan", () => { }), }, releasePlan.getReleasePlan(), - releasePlan.config + releasePlan.config, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -610,13 +646,15 @@ describe("apply release plan", () => { version: "2.0.0", }); }); + it("should not update the version of the dependent package if the released dep is a dev dep", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -667,7 +705,7 @@ describe("apply release plan", () => { baseBranch: "main", changedFilePatterns: ["**"], updateInternalDependencies: "patch", - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ignore: [], ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { @@ -678,20 +716,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -705,8 +743,9 @@ describe("apply release plan", () => { version: "1.1.0", }); }); + it("should skip dependencies that have the same name as the package", async () => { - let { tempDir } = await testSetup( + const { tempDir } = await testSetup( { "package.json": JSON.stringify({ name: "self-referenced", @@ -744,7 +783,7 @@ describe("apply release plan", () => { baseBranch: "main", changedFilePatterns: ["**"], updateInternalDependencies: "patch", - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ignore: [], ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { @@ -755,10 +794,10 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgJSON = await fs.readJSON(path.join(tempDir, "package.json")); + const pkgJSON = await readJson(path.join(tempDir, "package.json")); expect(pkgJSON).toMatchObject({ name: "self-referenced", @@ -768,13 +807,15 @@ describe("apply release plan", () => { }, }); }); + it("should not update dependent versions when a package has a changeset type of none", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -806,32 +847,34 @@ describe("apply release plan", () => { ], preState: undefined, }, - { ...defaultConfig, changelog: false } + { ...defaultConfig, changelog: false }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); expect(pkgPathA).toBeUndefined(); if (!pkgPathB) throw new Error(`could not find an updated package json`); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.0.0", }); }); + it("should not update workspace dependent versions when a package has a changeset type of none", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -863,25 +906,26 @@ describe("apply release plan", () => { ], preState: undefined, }, - { ...defaultConfig, changelog: false } + { ...defaultConfig, changelog: false }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); expect(pkgPathA).toBeUndefined(); if (!pkgPathB) throw new Error(`could not find an updated package json`); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.0.0", }); }); + it("should use exact versioning when snapshot release is applied, and ignore any range modifiers", async () => { const releasePlan = new FakeReleasePlan( [ @@ -899,14 +943,15 @@ describe("apply release plan", () => { oldVersion: "1.0.0", type: "minor", }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -921,15 +966,15 @@ describe("apply release plan", () => { }, releasePlan.getReleasePlan(), releasePlan.config, - "canary" + "canary", ); - let pkgPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); if (!pkgPath) throw new Error(`could not find an updated package json`); - let pkgJSON = await fs.readJSON(pkgPath); + const pkgJSON = await readJson(pkgPath); expect(pkgJSON).toMatchObject({ name: "pkg-a", @@ -944,12 +989,13 @@ describe("apply release plan", () => { describe("updateInternalDependencies set to patch", () => { const updateInternalDependencies = "patch"; it("should update min version ranges of patch bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1004,7 +1050,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1014,20 +1060,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1045,12 +1091,13 @@ describe("apply release plan", () => { }); }); it("should still update min version ranges of patch bumped internal dependencies that have left semver range", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1121,7 +1168,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1131,20 +1178,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1163,12 +1210,13 @@ describe("apply release plan", () => { }); }); it("should update min version ranges of minor bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1223,7 +1271,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1233,20 +1281,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1264,12 +1312,13 @@ describe("apply release plan", () => { }); }); it("should update min version ranges of major bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1324,7 +1373,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1334,20 +1383,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1365,12 +1414,13 @@ describe("apply release plan", () => { }); }); it("should not update dependant's dependency range when it depends on a tag of a bumped dependency", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1425,7 +1475,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1435,20 +1485,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1469,12 +1519,13 @@ describe("apply release plan", () => { describe("updateInternalDependencies set to minor", () => { const updateInternalDependencies = "minor"; it("should NOT update min version ranges of patch bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1529,7 +1580,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1539,20 +1590,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1570,12 +1621,13 @@ describe("apply release plan", () => { }); }); it("should still update min version ranges of patch bumped internal dependencies that have left semver range", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1646,7 +1698,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1656,20 +1708,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1688,12 +1740,13 @@ describe("apply release plan", () => { }); }); it("should update min version ranges of minor bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1756,7 +1809,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1766,20 +1819,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1797,12 +1850,13 @@ describe("apply release plan", () => { }); }); it("should update min version ranges of major bumped internal dependencies", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -1857,7 +1911,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies, ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -1867,20 +1921,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathA = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}package.json`) + const pkgPathA = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}package.json`), ); - let pkgPathB = changedFiles.find((b) => - b.endsWith(`pkg-b${path.sep}package.json`) + const pkgPathB = changedFiles.find((b) => + b.endsWith(`pkg-b${path.sep}package.json`), ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } - let pkgJSONA = await fs.readJSON(pkgPathA); - let pkgJSONB = await fs.readJSON(pkgPathB); + const pkgJSONA = await readJson(pkgPathA); + const pkgJSONB = await readJson(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", @@ -1902,12 +1956,13 @@ describe("apply release plan", () => { describe("onlyUpdatePeerDependentsWhenOutOfRange set to true", () => { it("should not bump peerDependencies if they are still in range", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/depended-upon/package.json": JSON.stringify({ name: "depended-upon", version: "1.0.0", @@ -1959,7 +2014,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies: "patch", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: true, @@ -1969,20 +2024,20 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgPathDependent = changedFiles.find((a) => - a.endsWith(`has-peer-dep${path.sep}package.json`) + const pkgPathDependent = changedFiles.find((a) => + a.endsWith(`has-peer-dep${path.sep}package.json`), ); - let pkgPathDepended = changedFiles.find((b) => - b.endsWith(`depended-upon${path.sep}package.json`) + const pkgPathDepended = changedFiles.find((b) => + b.endsWith(`depended-upon${path.sep}package.json`), ); if (!pkgPathDependent || !pkgPathDepended) { throw new Error(`could not find an updated package json`); } - let pkgJSONDependent = await fs.readJSON(pkgPathDependent); - let pkgJSONDepended = await fs.readJSON(pkgPathDepended); + const pkgJSONDependent = await readJson(pkgPathDependent); + const pkgJSONDepended = await readJson(pkgPathDepended); expect(pkgJSONDependent).toMatchObject({ name: "has-peer-dep", @@ -1998,15 +2053,17 @@ describe("apply release plan", () => { }); }); }); + describe("changelogs", () => { it("should not generate any changelogs", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2016,21 +2073,23 @@ describe("apply release plan", () => { { ...releasePlan.config, changelog: false, - } + }, ); expect( - changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`)) + changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`)), ).toBeUndefined(); }); + it("should update a changelog for one package", async () => { const releasePlan = new FakeReleasePlan(); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2039,28 +2098,73 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], - } + changelog: [changesetsCliChangelogPath, null], + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); if (!readmePath) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.1.0 + ## 1.1.0 - ### Minor Changes + ### Minor Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); }); + + it("should insert new entry before existing version heading when no package title is present", async () => { + const releasePlan = new FakeReleasePlan(); + const { changedFiles } = await testSetup( + { + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "yarn.lock": "", + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + "packages/pkg-a/CHANGELOG.md": + "## 1.0.0\n\n### Minor Changes\n\n- Initial release\n", + }, + releasePlan.getReleasePlan(), + { + ...releasePlan.config, + changelog: [changesetsCliChangelogPath, null], + }, + ); + + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), + ); + + if (!readmePath) throw new Error(`could not find an updated changelog`); + const readme = await fs.readFile(readmePath, "utf-8"); + + expect(readme).toMatchInlineSnapshot(` + "## 1.1.0 + + ### Minor Changes + + - Hey, let's have fun with testing! + ## 1.0.0 + + ### Minor Changes + + - Initial release + " + `); + }); + it("should update a changelog for two packages", async () => { const releasePlan = new FakeReleasePlan( [], @@ -2072,15 +2176,16 @@ describe("apply release plan", () => { newVersion: "2.0.0", changesets: [], }, - ] + ], ); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2096,48 +2201,51 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], - } + changelog: [changesetsCliChangelogPath, null], + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); - let readmePathB = changedFiles.find((a) => - a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) + const readmePathB = changedFiles.find((a) => + a.endsWith(`pkg-b${path.sep}CHANGELOG.md`), ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - let readmeB = await fs.readFile(readmePathB, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); + const readmeB = await fs.readFile(readmePathB, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.1.0 + ## 1.1.0 - ### Minor Changes + ### Minor Changes - - Hey, let's have fun with testing! + - Hey, let's have fun with testing! - ### Patch Changes + ### Patch Changes - - pkg-b@2.0.0`); + - pkg-b@2.0.0" + `); - expect(readmeB.trim()).toEqual(outdent`# pkg-b + expect(readmeB.trim()).toMatchInlineSnapshot(` + "# pkg-b - ## 2.0.0`); + ## 2.0.0" + `); }); + it("should not update the changelog if only devDeps changed", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2186,13 +2294,10 @@ describe("apply release plan", () => { access: "restricted", baseBranch: "main", changedFilePatterns: ["**"], - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], + changelog: [changesetsCliChangelogPath, null], updateInternalDependencies: "patch", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -2202,10 +2307,10 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let pkgAChangelogPath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const pkgAChangelogPath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); expect(pkgAChangelogPath).toBeUndefined(); @@ -2226,12 +2331,13 @@ describe("apply release plan", () => { ]); releasePlan.releases[0].changesets.push("some-id-1", "some-id-2"); - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2240,40 +2346,41 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], - } + changelog: [changesetsCliChangelogPath, null], + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); if (!readmePath) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - expect(readme.trim()).toEqual( - [ - "# pkg-a\n", - "## 1.1.0\n", - "### Minor Changes\n", - "- Hey, let's have fun with testing!", - "- Random stuff\n", - " get it while it's hot!\n", - "- New feature, much wow\n", - " look at this shiny stuff!", - ].join("\n") - ); + const readme = await fs.readFile(readmePath, "utf-8"); + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a + + ## 1.1.0 + + ### Minor Changes + + - Hey, let's have fun with testing! + - Random stuff + + get it while it's hot! + - New feature, much wow + + look at this shiny stuff!" + `); }); it("should add an updated dependencies line when dependencies have been updated", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -2319,10 +2426,7 @@ describe("apply release plan", () => { preState: undefined, }, { - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], + changelog: [changesetsCliChangelogPath, null], commit: false, fixed: [], linked: [], @@ -2331,7 +2435,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies: "patch", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -2341,49 +2445,54 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); - let readmePathB = changedFiles.find((a) => - a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) + const readmePathB = changedFiles.find((a) => + a.endsWith(`pkg-b${path.sep}CHANGELOG.md`), ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - let readmeB = await fs.readFile(readmePathB, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); + const readmeB = await fs.readFile(readmePathB, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.0.4 + ## 1.0.4 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing! - - Updated dependencies - - pkg-b@1.2.1`); + - Hey, let's have fun with testing! + - Updated dependencies + - pkg-b@1.2.1" + `); - expect(readmeB.trim()).toEqual(outdent`# pkg-b + expect(readmeB.trim()).toMatchInlineSnapshot(` + "# pkg-b - ## 1.2.1 + ## 1.2.1 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing! - - Updated dependencies - - pkg-a@1.0.4`); + - Hey, let's have fun with testing! + - Updated dependencies + - pkg-a@1.0.4" + `); }); it("should NOT add updated dependencies line if dependencies have NOT been updated", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -2429,10 +2538,7 @@ describe("apply release plan", () => { preState: undefined, }, { - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], + changelog: [changesetsCliChangelogPath, null], commit: false, fixed: [], linked: [], @@ -2441,7 +2547,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies: "minor", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -2451,45 +2557,50 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); - let readmePathB = changedFiles.find((a) => - a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) + const readmePathB = changedFiles.find((a) => + a.endsWith(`pkg-b${path.sep}CHANGELOG.md`), ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - let readmeB = await fs.readFile(readmePathB, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); + const readmeB = await fs.readFile(readmePathB, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.0.4 + ## 1.0.4 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); - expect(readmeB.trim()).toEqual(outdent`# pkg-b + expect(readmeB.trim()).toMatchInlineSnapshot(` + "# pkg-b - ## 1.2.1 + ## 1.2.1 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); }); it("should only add updated dependencies line for dependencies that have been updated", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -2551,10 +2662,7 @@ describe("apply release plan", () => { preState: undefined, }, { - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], + changelog: [changesetsCliChangelogPath, null], commit: false, fixed: [], linked: [], @@ -2563,7 +2671,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies: "minor", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -2573,59 +2681,66 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); - let readmePathB = changedFiles.find((a) => - a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) + const readmePathB = changedFiles.find((a) => + a.endsWith(`pkg-b${path.sep}CHANGELOG.md`), ); - let readmePathC = changedFiles.find((a) => - a.endsWith(`pkg-c${path.sep}CHANGELOG.md`) + const readmePathC = changedFiles.find((a) => + a.endsWith(`pkg-c${path.sep}CHANGELOG.md`), ); if (!readmePath || !readmePathB || !readmePathC) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - let readmeB = await fs.readFile(readmePathB, "utf-8"); - let readmeC = await fs.readFile(readmePathC, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); + const readmeB = await fs.readFile(readmePathB, "utf-8"); + const readmeC = await fs.readFile(readmePathC, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.0.4 + ## 1.0.4 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); - expect(readmeB.trim()).toEqual(outdent`# pkg-b + expect(readmeB.trim()).toMatchInlineSnapshot(` + "# pkg-b - ## 1.2.1 + ## 1.2.1 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing! - - Updated dependencies - - pkg-c@2.1.0`); + - Hey, let's have fun with testing! + - Updated dependencies + - pkg-c@2.1.0" + `); - expect(readmeC.trim()).toEqual(outdent`# pkg-c + expect(readmeC.trim()).toMatchInlineSnapshot(` + "# pkg-c - ## 2.1.0 + ## 2.1.0 - ### Minor Changes + ### Minor Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); }); it("should still add updated dependencies line for dependencies that have a bump type less than the minimum internal bump range but leave semver range", async () => { - let { changedFiles } = await testSetup( + const { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", @@ -2687,10 +2802,7 @@ describe("apply release plan", () => { preState: undefined, }, { - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], + changelog: [changesetsCliChangelogPath, null], commit: false, fixed: [], linked: [], @@ -2699,7 +2811,7 @@ describe("apply release plan", () => { baseBranch: "main", updateInternalDependencies: "minor", ignore: [], - prettier: true, + format: "auto", privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, @@ -2709,64 +2821,72 @@ describe("apply release plan", () => { useCalculatedVersion: false, prereleaseTemplate: null, }, - } + }, ); - let readmePath = changedFiles.find((a) => - a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) + const readmePath = changedFiles.find((a) => + a.endsWith(`pkg-a${path.sep}CHANGELOG.md`), ); - let readmePathB = changedFiles.find((a) => - a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) + const readmePathB = changedFiles.find((a) => + a.endsWith(`pkg-b${path.sep}CHANGELOG.md`), ); - let readmePathC = changedFiles.find((a) => - a.endsWith(`pkg-c${path.sep}CHANGELOG.md`) + const readmePathC = changedFiles.find((a) => + a.endsWith(`pkg-c${path.sep}CHANGELOG.md`), ); if (!readmePath || !readmePathB || !readmePathC) throw new Error(`could not find an updated changelog`); - let readme = await fs.readFile(readmePath, "utf-8"); - let readmeB = await fs.readFile(readmePathB, "utf-8"); - let readmeC = await fs.readFile(readmePathC, "utf-8"); + const readme = await fs.readFile(readmePath, "utf-8"); + const readmeB = await fs.readFile(readmePathB, "utf-8"); + const readmeC = await fs.readFile(readmePathC, "utf-8"); - expect(readme.trim()).toEqual(outdent`# pkg-a + expect(readme.trim()).toMatchInlineSnapshot(` + "# pkg-a - ## 1.0.4 + ## 1.0.4 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); - expect(readmeB.trim()).toEqual(outdent`# pkg-b + expect(readmeB.trim()).toMatchInlineSnapshot(` + "# pkg-b - ## 1.2.1 + ## 1.2.1 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing! - - Updated dependencies - - pkg-c@2.0.1`); + - Hey, let's have fun with testing! + - Updated dependencies + - pkg-c@2.0.1" + `); - expect(readmeC.trim()).toEqual(outdent`# pkg-c + expect(readmeC.trim()).toMatchInlineSnapshot(` + "# pkg-c - ## 2.0.1 + ## 2.0.1 - ### Patch Changes + ### Patch Changes - - Hey, let's have fun with testing!`); + - Hey, let's have fun with testing!" + `); }); }); + describe("should error and not write if", () => { // This is skipped as *for now* we are assuming we have been passed // valid releasePlans - this may get work done on it in the future - it.skip("a package appears twice", async () => { + it.todo("a package appears twice", async () => { let changedFiles; try { - let testResults = await testSetup( + const testResults = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2797,10 +2917,11 @@ describe("apply release plan", () => { }, ], preState: undefined, - } + }, ); changedFiles = testResults.changedFiles; } catch (e) { + // eslint-disable-next-line vitest/no-conditional-expect expect((e as Error).message).toEqual("some string probably"); return; @@ -2808,12 +2929,13 @@ describe("apply release plan", () => { throw new Error( `expected error but instead got changed files: \n${changedFiles.join( - "\n" - )}` + "\n", + )}`, ); }); + it("a package cannot be found", async () => { - let releasePlan = new FakeReleasePlan( + const releasePlan = new FakeReleasePlan( [], [ { @@ -2823,14 +2945,15 @@ describe("apply release plan", () => { newVersion: "1.0.0", changesets: [], }, - ] + ], ); - let tempDir = await testdir({ + const tempDir = await testdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2844,42 +2967,49 @@ describe("apply release plan", () => { }), }); - await spawn("git", ["init"], { cwd: tempDir }); + await exec("git", ["init"], { nodeOptions: { cwd: tempDir } }); await git.add(".", tempDir); await git.commit("first commit", tempDir); try { + const packages = await getPackages(tempDir); await applyReleasePlan( releasePlan.getReleasePlan(), - await getPackages(tempDir), - releasePlan.config + packages, + releasePlan.config, ); } catch (e) { + // eslint-disable-next-line vitest/no-conditional-expect expect((e as Error).message).toEqual( - "Could not find matching package for release of: impossible-package" + "Could not find matching package for release of: impossible-package", ); - let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); + const gitCmd = await exec("git", ["status"], { + nodeOptions: { cwd: tempDir }, + }); + // eslint-disable-next-line vitest/no-conditional-expect expect(gitCmd.stdout.toString().includes("nothing to commit")).toEqual( - true + true, ); return; } throw new Error("Expected test to exit before this point"); }); + it( "a provided changelog function fails", temporarilySilenceLogs(async () => { - let releasePlan = new FakeReleasePlan(); + const releasePlan = new FakeReleasePlan(); - let tempDir = await testdir({ + const tempDir = await testdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2893,31 +3023,37 @@ describe("apply release plan", () => { }), }); - await spawn("git", ["init"], { cwd: tempDir }); + await exec("git", ["init"], { nodeOptions: { cwd: tempDir } }); await git.add(".", tempDir); await git.commit("first commit", tempDir); try { - await applyReleasePlan( - releasePlan.getReleasePlan(), - await getPackages(tempDir), - { - ...releasePlan.config, - changelog: [ - path.resolve(__dirname, "test-utils/failing-functions"), - null, - ], - } - ); + const packages = await getPackages(tempDir); + + await applyReleasePlan(releasePlan.getReleasePlan(), packages, { + ...releasePlan.config, + changelog: [ + path.resolve( + import.meta.dirname, + "test-utils/failing-functions.ts", + ), + null, + ], + }); } catch (e) { + // eslint-disable-next-line vitest/no-conditional-expect expect((e as Error).message).toEqual("no chance"); - let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); + const gitCmd = await exec("git", ["status"], { + nodeOptions: { cwd: tempDir }, + }); + // eslint-disable-next-line vitest/no-conditional-expect expect( - gitCmd.stdout.toString().includes("nothing to commit") + gitCmd.stdout.toString().includes("nothing to commit"), ).toEqual(true); + // eslint-disable-next-line vitest/no-conditional-expect expect((console.error as any).mock.calls).toMatchInlineSnapshot(` [ [ @@ -2932,14 +3068,15 @@ describe("apply release plan", () => { } throw new Error("Expected test to exit before this point"); - }) + }), ); }); + describe("changesets", () => { it("should delete one changeset after it is applied", async () => { const releasePlan = new FakeReleasePlan(); - let changesetPath: string; + let changesetPath!: string; const setupFunc = (tempDir: string) => Promise.all( @@ -2947,8 +3084,8 @@ describe("apply release plan", () => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; - return fs.outputFile(thisPath, content); - }) + return outputFile(thisPath, content); + }), ); await testSetup( @@ -2957,6 +3094,7 @@ describe("apply release plan", () => { private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -2965,17 +3103,17 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), releasePlan.config, undefined, - setupFunc + setupFunc, ); - // @ts-ignore this is possibly bad - let pathExists = await fs.pathExists(changesetPath); - expect(pathExists).toEqual(false); + const changesetExists = existsSync(changesetPath); + expect(changesetExists).toEqual(false); }); + it("should NOT delete changesets for ignored packages", async () => { const releasePlan = new FakeReleasePlan(); - let changesetPath: string; + let changesetPath!: string; const setupFunc = (tempDir: string) => Promise.all( @@ -2983,8 +3121,8 @@ describe("apply release plan", () => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; - return fs.outputFile(thisPath, content); - }) + return outputFile(thisPath, content); + }), ); await testSetup( @@ -2993,6 +3131,7 @@ describe("apply release plan", () => { private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -3001,17 +3140,17 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, ignore: ["pkg-a"] }, undefined, - setupFunc + setupFunc, ); - // @ts-ignore this is possibly bad - let pathExists = await fs.pathExists(changesetPath); - expect(pathExists).toEqual(true); + const changesetExists = existsSync(changesetPath); + expect(changesetExists).toEqual(true); }); + it("should NOT delete changesets for private unversioned packages", async () => { const releasePlan = new FakeReleasePlan(); - let changesetPath: string; + let changesetPath!: string; const setupFunc = (tempDir: string) => Promise.all( @@ -3019,8 +3158,8 @@ describe("apply release plan", () => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; - return fs.outputFile(thisPath, content); - }) + return outputFile(thisPath, content); + }), ); await testSetup( @@ -3029,6 +3168,7 @@ describe("apply release plan", () => { private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -3041,160 +3181,25 @@ describe("apply release plan", () => { privatePackages: { version: false, tag: false }, }, undefined, - setupFunc - ); - - // @ts-ignore this is possibly bad - let pathExists = await fs.pathExists(changesetPath); - expect(pathExists).toEqual(true); - }); - it("should delete an old format changeset if it is applied", async () => { - const releasePlan = new FakeReleasePlan(); - - let changesetMDPath: string; - let changesetJSONPath: string; - - const setupFunc = (tempDir: string) => - Promise.all( - releasePlan - .getReleasePlan() - .changesets.map(async ({ id, summary }) => { - changesetMDPath = path.resolve( - tempDir, - ".changeset", - id, - `changes.md` - ); - changesetJSONPath = path.resolve( - tempDir, - ".changeset", - id, - `changes.json` - ); - await fs.outputFile(changesetMDPath, summary); - await fs.outputFile( - changesetJSONPath, - JSON.stringify({ id, summary }) - ); - }) - ); - - await testSetup( - { - "package.json": JSON.stringify({ - private: true, - workspaces: ["packages/*"], - }), - "packages/pkg-a/package.json": JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - }), - }, - releasePlan.getReleasePlan(), - releasePlan.config, - undefined, - setupFunc + setupFunc, ); - // @ts-ignore this is possibly bad - let mdPathExists = await fs.pathExists(changesetMDPath); - // @ts-ignore this is possibly bad - let JSONPathExists = await fs.pathExists(changesetMDPath); - - expect(mdPathExists).toEqual(false); - expect(JSONPathExists).toEqual(false); + const changesetExists = existsSync(changesetPath); + expect(changesetExists).toEqual(true); }); }); - it("should get the commit for an old changeset", async () => { - const releasePlan = new FakeReleasePlan(); - - let changesetMDPath: string; - let changesetJSONPath: string; - - const setupFunc = (tempDir: string) => - Promise.all( - releasePlan.changesets.map(async ({ id, summary }) => { - changesetMDPath = path.resolve( - tempDir, - ".changeset", - id, - `changes.md` - ); - changesetJSONPath = path.resolve( - tempDir, - ".changeset", - id, - `changes.json` - ); - await fs.outputFile(changesetMDPath, summary); - await fs.outputFile( - changesetJSONPath, - JSON.stringify({ id, summary }) - ); - }) - ); - - let { tempDir } = await testSetup( - { - "package.json": JSON.stringify({ - private: true, - workspaces: ["packages/*"], - }), - "packages/pkg-a/package.json": JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - }), - }, - releasePlan.getReleasePlan(), - { - ...releasePlan.config, - commit: [ - path.resolve(__dirname, "test-utils/simple-get-commit-entry"), - null, - ], - changelog: [ - path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), - null, - ], - }, - undefined, - setupFunc - ); - - let thing = await spawn("git", ["rev-list", "HEAD"], { cwd: tempDir }); - let commits = thing.stdout - .toString("utf8") - .split("\n") - .filter((x) => x); - - let lastCommit = commits[commits.length - 1].substring(0, 7); - - expect( - await fs.readFile( - path.join(tempDir, "packages", "pkg-a", "CHANGELOG.md"), - "utf8" - ) - ).toBe(`# pkg-a - -## 1.1.0 - -### Minor Changes - -- ${lastCommit}: Hey, let's have fun with testing! -`); - }); - describe("files", () => { it("shouldn't commit updated files from packages", async () => { const releasePlan = new FakeReleasePlan(); - let { tempDir } = await testSetup( + const { tempDir } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -3210,24 +3215,25 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, - commit: [ - path.resolve(__dirname, "test-utils/simple-get-commit-entry"), - null, - ], - } + commit: [changesetsCliCommitPath, null], + }, ); - let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); + const gitCmd = await exec("git", ["status"], { + nodeOptions: { cwd: tempDir }, + }); expect(gitCmd.stdout.toString()).toContain( - "Changes not staged for commit" + "Changes not staged for commit", ); expect(gitCmd.stdout.toString()).toContain( - "modified: packages/pkg-a/package.json" + "modified: packages/pkg-a/package.json", ); - let lastCommit = await spawn("git", ["log", "-1"], { cwd: tempDir }); + const lastCommit = await exec("git", ["log", "-1"], { + nodeOptions: { cwd: tempDir }, + }); expect(lastCommit.stdout.toString()).toContain("first commit"); }); @@ -3235,7 +3241,7 @@ describe("apply release plan", () => { it("should remove applied changesets", async () => { const releasePlan = new FakeReleasePlan(); - let changesetPath: string; + let changesetPath!: string; const setupFunc = (tempDir: string) => Promise.all( @@ -3243,16 +3249,17 @@ describe("apply release plan", () => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; - return fs.outputFile(thisPath, content); - }) + return outputFile(thisPath, content); + }), ); - let { tempDir } = await testSetup( + const { tempDir } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), + "package-lock.json": "", "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", @@ -3268,21 +3275,19 @@ describe("apply release plan", () => { releasePlan.getReleasePlan(), { ...releasePlan.config, - commit: [ - path.resolve(__dirname, "test-utils/simple-get-commit-entry"), - null, - ], + commit: [changesetsCliCommitPath, null], }, undefined, - setupFunc + setupFunc, ); - // @ts-ignore this is possibly bad - let pathExists = await fs.pathExists(changesetPath); + const changesetExists = existsSync(changesetPath); - expect(pathExists).toEqual(false); + expect(changesetExists).toEqual(false); - let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); + const gitCmd = await exec("git", ["status"], { + nodeOptions: { cwd: tempDir }, + }); const changesetsDeleted = releasePlan.changesets.reduce( (prev, { id }) => { @@ -3291,11 +3296,65 @@ describe("apply release plan", () => { gitCmd.stdout.toString().includes(`deleted: .changeset/${id}.md`) ); }, - true + true, ); expect(releasePlan.changesets.length).toBeGreaterThan(0); expect(changesetsDeleted).toBe(true); }); + + it("should include pre.json in pre-release", async () => { + const releasePlan = new FakeReleasePlan(); + const preState: PreState = { + mode: "pre", + tag: "beta", + initialVersions: { + "pkg-a": "1.0.0", + "pkg-b": "1.0.0", + }, + changesets: [], + }; + + const { tempDir } = await testSetup( + { + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "yarn.lock": "", + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + dependencies: { + "pkg-b": "1.0.0", + }, + }), + "packages/pkg-b/package.json": JSON.stringify({ + name: "pkg-b", + version: "1.0.0", + }), + ".changeset/pre.json": JSON.stringify(preState), + }, + { + ...releasePlan.getReleasePlan(), + preState, + }, + { + ...releasePlan.config, + commit: [changesetsCliCommitPath, null], + }, + ); + + const gitCmd = await exec("git", ["status"], { + nodeOptions: { cwd: tempDir }, + }); + + expect(gitCmd.stdout.toString()).toContain( + "modified: .changeset/pre.json", + ); + expect(gitCmd.stdout.toString()).toContain( + "modified: packages/pkg-a/package.json", + ); + }); }); }); diff --git a/packages/apply-release-plan/src/index.ts b/packages/apply-release-plan/src/index.ts index 810cd77f1..e2b1dcb37 100644 --- a/packages/apply-release-plan/src/index.ts +++ b/packages/apply-release-plan/src/index.ts @@ -1,91 +1,87 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; import { defaultConfig } from "@changesets/config"; +import { + defaultDetectOrder, + detect as detectFormatter, + format, +} from "@changesets/format"; import * as git from "@changesets/git"; import { shouldSkipPackage } from "@changesets/should-skip-package"; -import { +import type { ChangelogFunctions, + Packages, Config, ModCompWithPackage, NewChangeset, ReleasePlan, } from "@changesets/types"; -import { Packages } from "@manypkg/get-packages"; -import detectIndent from "detect-indent"; -import fs from "fs-extra"; -import path from "path"; -import prettier from "prettier"; -import resolveFrom from "resolve-from"; -import getChangelogEntry from "./get-changelog-entry"; -import versionPackage from "./version-package"; - -function getPrettierInstance(cwd: string): typeof prettier { - try { - return require(require.resolve("prettier", { paths: [cwd] })); - } catch (err) { - if (!err || (err as any).code !== "MODULE_NOT_FOUND") { - throw err; - } - return prettier; - } -} +import { resolve } from "import-meta-resolve"; +import { editJson } from "./edit-json.ts"; +import { getChangelogEntry } from "./get-changelog-entry.ts"; +import { + versionPackage, + type ModCompWithPackageAndChangelog, +} from "./version-package.ts"; -function stringDefined(s: string | undefined): s is string { - return !!s; +function importResolveFromDir(specifier: string, dir: string) { + return resolve(specifier, pathToFileURL(path.join(dir, "x.mjs")).toString()); } + async function getCommitsThatAddChangesets( changesetIds: string[], - cwd: string + cwd: string, ) { const paths = changesetIds.map((id) => `.changeset/${id}.md`); const commits = await git.getCommitsThatAddFiles(paths, { cwd }); - if (commits.every(stringDefined)) { - // We have commits for all files - return commits; - } - - // Some files didn't exist. Try legacy filenames instead - const missingIds = changesetIds - .map((id, i) => (commits[i] ? undefined : id)) - .filter(stringDefined); - - const legacyPaths = missingIds.map((id) => `.changeset/${id}/changes.json`); - const commitsForLegacyPaths = await git.getCommitsThatAddFiles(legacyPaths, { - cwd, - }); + return commits; +} - // Fill in the blanks in the array of commits - changesetIds.forEach((id, i) => { - if (!commits[i]) { - const missingIndex = missingIds.indexOf(id); - commits[i] = commitsForLegacyPaths[missingIndex]; - } - }); +async function getFormatter( + config: Config["format"], + cwd: string, +): Promise<(patterns: string[]) => Promise> { + if (config === false) return async () => {}; + + const formatter = + config === "auto" + ? await detectFormatter({ + cwd, + // Biome doesn't support formatting markdown files + order: defaultDetectOrder.filter((f) => f !== "biome"), + }) + : config; + if (!formatter) return async () => {}; - return commits; + return async (patterns: string[]) => { + await format(patterns, { cwd, formatter }); + }; } -export default async function applyReleasePlan( +export async function applyReleasePlan( releasePlan: ReleasePlan, packages: Packages, config: Config = defaultConfig, snapshot?: string | boolean, - contextDir = __dirname + contextDir = import.meta.dirname, ) { - let cwd = packages.root.dir; + const cwd = packages.rootDir; - let touchedFiles = []; + const touchedFiles: string[] = []; const packagesByName = new Map( - packages.packages.map((x) => [x.packageJson.name, x]) + packages.packages.map((x) => [x.packageJson.name, x]), ); - let { releases, changesets } = releasePlan; + const { releases, changesets } = releasePlan; - let releasesWithPackage = releases.map((release) => { - let pkg = packagesByName.get(release.name); + const releasesWithPackage = releases.map((release) => { + const pkg = packagesByName.get(release.name); if (!pkg) throw new Error( - `Could not find matching package for release of: ${release.name}` + `Could not find matching package for release of: ${release.name}`, ); return { ...release, @@ -94,34 +90,43 @@ export default async function applyReleasePlan( }); // I think this might be the wrong place to do this, but gotta do it somewhere - add changelog entries to releases - let releaseWithChangelogs = await getNewChangelogEntry( + const releaseWithChangelogs = await getNewChangelogEntry( releasesWithPackage, changesets, config, cwd, - contextDir + contextDir, ); - if (releasePlan.preState !== undefined && snapshot === undefined) { + if (releasePlan.preState != null && snapshot == null) { if (releasePlan.preState.mode === "exit") { - await fs.remove(path.join(cwd, ".changeset", "pre.json")); + await fs.rm(path.join(cwd, ".changeset", "pre.json"), { + recursive: true, + force: true, + }); } else { await fs.writeFile( path.join(cwd, ".changeset", "pre.json"), - JSON.stringify(releasePlan.preState, null, 2) + "\n" + JSON.stringify(releasePlan.preState, null, 2) + "\n", ); } + touchedFiles.push(path.join(cwd, ".changeset", "pre.json")); } - let versionsToUpdate = releases.map(({ name, newVersion, type }) => ({ - name, - version: newVersion, - type, - })); + const versionsToUpdate = releases.map( + ({ name, newVersion, oldVersion, type }) => ({ + name, + version: newVersion, + oldVersion, + type, + dir: packagesByName.get(name)!.dir, + }), + ); // iterate over releases updating packages - let finalisedRelease = releaseWithChangelogs.map((release) => { + const finalisedRelease = releaseWithChangelogs.map((release) => { return versionPackage(release, versionsToUpdate, { + cwd, updateInternalDependencies: config.updateInternalDependencies, onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH @@ -132,55 +137,61 @@ export default async function applyReleasePlan( }); }); - let prettierInstance = - config.prettier !== false ? getPrettierInstance(cwd) : undefined; - - for (let release of finalisedRelease) { - let { changelog, packageJson, dir, name } = release; + const filesToFormat: string[] = []; + for (const release of finalisedRelease) { + const { changelog, pkgJsonEdits, dir, name } = release; - const pkgJSONPath = path.resolve(dir, "package.json"); - await updatePackageJson(pkgJSONPath, packageJson); - touchedFiles.push(pkgJSONPath); + const pkgJsonPath = path.resolve(dir, "package.json"); + const pkgRaw = await fs.readFile(pkgJsonPath, "utf8"); + const pkgUpdated = editJson(pkgRaw, pkgJsonEdits); + await fs.writeFile(pkgJsonPath, pkgUpdated); + touchedFiles.push(pkgJsonPath); if (changelog && changelog.length > 0) { const changelogPath = path.resolve(dir, "CHANGELOG.md"); - await updateChangelog(changelogPath, changelog, name, prettierInstance); + await updateChangelog(changelogPath, changelog, name); touchedFiles.push(changelogPath); + filesToFormat.push(changelogPath); } } - if ( - releasePlan.preState === undefined || - releasePlan.preState.mode === "exit" - ) { - let changesetFolder = path.resolve(cwd, ".changeset"); + if (filesToFormat.length > 0) { + const formatter = await getFormatter(config.format, cwd); + await formatter(filesToFormat); + } + + if (releasePlan.preState == null || releasePlan.preState.mode === "exit") { + const changesetFolder = path.resolve(cwd, ".changeset"); await Promise.all( changesets.map(async (changeset) => { - let changesetPath = path.resolve(changesetFolder, `${changeset.id}.md`); - let changesetFolderPath = path.resolve(changesetFolder, changeset.id); - if (await fs.pathExists(changesetPath)) { + const changesetPath = path.resolve( + changesetFolder, + `${changeset.id}.md`, + ); + if ( + await fs.access(changesetPath).then( + () => true, + () => false, + ) + ) { // DO NOT remove changeset for skipped packages // Mixed changeset that contains both skipped packages and not skipped packages are disallowed // At this point, we know there is no such changeset, because otherwise the program would've already failed, // so we just check if any skipped package exists in this changeset, and only remove it if none exists // options to skip packages were added in v2, so we don't need to do it for v1 changesets if ( - !changeset.releases.find((release) => + !changeset.releases.some((release) => shouldSkipPackage(packagesByName.get(release.name)!, { ignore: config.ignore, allowPrivatePackages: config.privatePackages.version, - }) + }), ) ) { touchedFiles.push(changesetPath); - await fs.remove(changesetPath); + await fs.rm(changesetPath, { recursive: true, force: true }); } - // TO REMOVE LOGIC - this works to remove v1 changesets. We should be removed in the future - } else if (await fs.pathExists(changesetFolderPath)) { - touchedFiles.push(changesetFolderPath); - await fs.remove(changesetFolderPath); } - }) + }), ); } @@ -193,14 +204,14 @@ async function getNewChangelogEntry( changesets: NewChangeset[], config: Config, cwd: string, - contextDir: string -) { + contextDir: string, +): Promise { if (!config.changelog) { return Promise.resolve( releasesWithPackage.map((release) => ({ ...release, changelog: null, - })) + })), ); } @@ -210,18 +221,23 @@ async function getNewChangelogEntry( }; const changelogOpts = config.changelog[1]; - let changesetPath = path.join(cwd, ".changeset"); + const changesetPath = path.join(cwd, ".changeset"); let changelogPath; try { - changelogPath = resolveFrom(changesetPath, config.changelog[0]); + changelogPath = importResolveFromDir(config.changelog[0], changesetPath); } catch { - changelogPath = resolveFrom(contextDir, config.changelog[0]); + changelogPath = importResolveFromDir(config.changelog[0], contextDir); } - let possibleChangelogFunc = require(changelogPath); + let possibleChangelogFunc = await import(changelogPath); if (possibleChangelogFunc.default) { possibleChangelogFunc = possibleChangelogFunc.default; + + // Check nested default again in case it's CJS with `__esModule` interop + if (possibleChangelogFunc.default) { + possibleChangelogFunc = possibleChangelogFunc.default; + } } if ( typeof possibleChangelogFunc.getReleaseLine === "function" && @@ -232,18 +248,19 @@ async function getNewChangelogEntry( throw new Error("Could not resolve changelog generation functions"); } - let commits = await getCommitsThatAddChangesets( + const commits = await getCommitsThatAddChangesets( changesets.map((cs) => cs.id), - cwd + cwd, ); - let moddedChangesets = changesets.map((cs, i) => ({ + const moddedChangesets = changesets.map((cs, i) => ({ ...cs, commit: commits[i], })); return Promise.all( releasesWithPackage.map(async (release) => { - let changelog = await getChangelogEntry( + const changelog = await getChangelogEntry( + cwd, release, releasesWithPackage, moddedChangesets, @@ -254,20 +271,20 @@ async function getNewChangelogEntry( onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH .onlyUpdatePeerDependentsWhenOutOfRange, - } + }, ); return { ...release, changelog, }; - }) + }), ).catch((e) => { console.error( - "The following error was encountered while generating changelog entries" + "The following error was encountered while generating changelog entries", ); console.error( - "We have escaped applying the changesets, and no files should have been affected" + "We have escaped applying the changesets, and no files should have been affected", ); throw e; }); @@ -277,73 +294,46 @@ async function updateChangelog( changelogPath: string, changelog: string, name: string, - prettierInstance: typeof prettier | undefined ) { - let templateString = `\n\n${changelog.trim()}\n`; + const templateString = `\n\n${changelog.trim()}\n`; + let fileData; try { - if (fs.existsSync(changelogPath)) { - await prependFile(changelogPath, templateString, name, prettierInstance); - } else { - await writeFormattedMarkdownFile( - changelogPath, - `# ${name}${templateString}`, - prettierInstance - ); + fileData = (await fs.readFile(changelogPath)).toString(); + } catch (err: any) { + if (err?.code !== "ENOENT") { + throw err; } - } catch (e) { - console.warn(e); + await fs.writeFile(changelogPath, `# ${name}${templateString}`); + return; } -} - -async function updatePackageJson( - pkgJsonPath: string, - pkgJson: any -): Promise { - const pkgRaw = await fs.readFile(pkgJsonPath, "utf-8"); - const indent = detectIndent(pkgRaw).indent || " "; - const stringified = - JSON.stringify(pkgJson, null, indent) + (pkgRaw.endsWith("\n") ? "\n" : ""); - return fs.writeFile(pkgJsonPath, stringified); -} - -async function prependFile( - filePath: string, - data: string, - name: string, - prettierInstance: typeof prettier | undefined -) { - const fileData = fs.readFileSync(filePath).toString(); // if the file exists but doesn't have the header, we'll add it in if (!fileData) { - const completelyNewChangelog = `# ${name}${data}`; - await writeFormattedMarkdownFile( - filePath, - completelyNewChangelog, - prettierInstance - ); + const completelyNewChangelog = `# ${name}${templateString}`; + await fs.writeFile(changelogPath, completelyNewChangelog); return; } - const newChangelog = fileData.replace("\n", data); - await writeFormattedMarkdownFile(filePath, newChangelog, prettierInstance); -} + // Require just 2 version numbers here, assuming `## 1.1` is a valid version heading. + // Our version headings start with ##, we are more permissive here though. + // Note: we also need to handle prerelease versions here but that's already covered by the regex. + const isVersionHeading = /^#{1,6}\s+\d+\.\d+/.test(fileData); -async function writeFormattedMarkdownFile( - filePath: string, - content: string, - prettierInstance: typeof prettier | undefined -) { - await fs.writeFile( - filePath, - prettierInstance - ? // Prettier v3 returns a promise - await prettierInstance.format(content, { - ...(await prettierInstance.resolveConfig(filePath)), - filepath: filePath, - parser: "markdown", - }) - : content - ); + let newChangelog: string; + if (isVersionHeading) { + newChangelog = templateString.trimStart() + fileData; + } else { + const index = fileData.indexOf("\n"); + newChangelog = + index === -1 + ? fileData + templateString // treat the whole file as header + : fileData.slice(0, index) + templateString + fileData.slice(index + 1); + } + + await fs.writeFile(changelogPath, newChangelog); } + +/** @deprecated Use named export `applyReleasePlan` instead */ +const applyReleasePlanDefault = applyReleasePlan; +export default applyReleasePlanDefault; diff --git a/packages/apply-release-plan/src/test-utils/failing-functions.ts b/packages/apply-release-plan/src/test-utils/failing-functions.ts index f208a4be3..cb3e1068a 100644 --- a/packages/apply-release-plan/src/test-utils/failing-functions.ts +++ b/packages/apply-release-plan/src/test-utils/failing-functions.ts @@ -1,3 +1,8 @@ +// plugin, must have a default export +/* eslint-disable import-lite/no-default-export */ + +import type { ChangelogFunctions } from "@changesets/types"; + export default { getReleaseLine: () => { throw new Error("no chance"); @@ -5,4 +10,4 @@ export default { getDependencyReleaseLine: () => { throw new Error("no chance"); }, -}; +} satisfies ChangelogFunctions; diff --git a/packages/apply-release-plan/src/test-utils/get-changelog-dep-updated.ts b/packages/apply-release-plan/src/test-utils/get-changelog-dep-updated.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/apply-release-plan/src/test-utils/get-changelog-entry-with-git-hash.ts b/packages/apply-release-plan/src/test-utils/get-changelog-entry-with-git-hash.ts deleted file mode 100644 index d9bb2f0cb..000000000 --- a/packages/apply-release-plan/src/test-utils/get-changelog-entry-with-git-hash.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import startCase from "lodash.startcase"; -import { getCommitsThatAddFiles } from "@changesets/git"; -import { ComprehensiveRelease, NewChangeset } from "@changesets/types"; - -import { RelevantChangesets } from "../types"; - -async function getReleaseLine(changeset: NewChangeset, cwd: string) { - const [firstLine, ...futureLines] = changeset.summary - .split("\n") - .map((l) => l.trimRight()); - - const [commitThatAddsFile] = await getCommitsThatAddFiles( - [`.changeset/${changeset.id}.md`], - { cwd } - ); - - return `- [${commitThatAddsFile}] ${firstLine}\n${futureLines - .map((l) => ` ${l}`) - .join("\n")}`; -} - -async function getReleaseLines( - obj: RelevantChangesets, - type: keyof RelevantChangesets, - cwd: string -) { - const releaseLines = obj[type].map((changeset) => - getReleaseLine(changeset, cwd) - ); - if (!releaseLines.length) return ""; - const resolvedLines = await Promise.all(releaseLines); - - return `### ${startCase(type)} Changes\n\n${resolvedLines.join("")}`; -} - -export default async function defaultChangelogGetter( - release: ComprehensiveRelease, - relevantChangesets: RelevantChangesets, - options: { cwd: string } -) { - let { cwd } = options; - - // First, we construct the release lines, summaries of changesets that caused us to be released - let majorReleaseLines = await getReleaseLines( - relevantChangesets, - "major", - cwd - ); - let minorReleaseLines = await getReleaseLines( - relevantChangesets, - "minor", - cwd - ); - let patchReleaseLines = await getReleaseLines( - relevantChangesets, - "patch", - cwd - ); - - return [ - `## ${release.newVersion}`, - majorReleaseLines, - minorReleaseLines, - patchReleaseLines, - ] - .filter((line) => line) - .join("\n"); -} diff --git a/packages/apply-release-plan/src/test-utils/simple-get-changelog-entry.ts b/packages/apply-release-plan/src/test-utils/simple-get-changelog-entry.ts deleted file mode 100644 index 14631450d..000000000 --- a/packages/apply-release-plan/src/test-utils/simple-get-changelog-entry.ts +++ /dev/null @@ -1,4 +0,0 @@ -// We are doing it here to avoide adding a circular dependency and as this is only used in testing. -// This is wicked, and please don't copy us. -// eslint-disable-next-line import/no-extraneous-dependencies -export { default } from "@changesets/cli/changelog"; diff --git a/packages/apply-release-plan/src/test-utils/simple-get-commit-entry.ts b/packages/apply-release-plan/src/test-utils/simple-get-commit-entry.ts deleted file mode 100644 index e02e5e364..000000000 --- a/packages/apply-release-plan/src/test-utils/simple-get-commit-entry.ts +++ /dev/null @@ -1,4 +0,0 @@ -// We are doing it here to avoide adding a circular dependency and as this is only used in testing. -// This is wicked, and please don't copy us. -// eslint-disable-next-line import/no-extraneous-dependencies -export { default } from "@changesets/cli/commit"; diff --git a/packages/apply-release-plan/src/types.ts b/packages/apply-release-plan/src/types.ts deleted file mode 100644 index d87214bbc..000000000 --- a/packages/apply-release-plan/src/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NewChangeset } from "@changesets/types"; - -export type RelevantChangesets = { - major: NewChangeset[]; - minor: NewChangeset[]; - patch: NewChangeset[]; -}; diff --git a/packages/apply-release-plan/src/utils.ts b/packages/apply-release-plan/src/utils.ts index d796c206e..281c25130 100644 --- a/packages/apply-release-plan/src/utils.ts +++ b/packages/apply-release-plan/src/utils.ts @@ -1,8 +1,10 @@ +import path from "node:path"; +import type { VersionType } from "@changesets/types"; /** * Shared utility functions and business logic */ -import semverSatisfies from "semver/functions/satisfies"; -import { VersionType } from "@changesets/types"; +import semverSatisfies from "semver/functions/satisfies.js"; +import validRange from "semver/ranges/valid.js"; const bumpTypes = ["none", "patch", "minor", "major"]; @@ -16,7 +18,13 @@ function getBumpLevel(type: VersionType) { } export function shouldUpdateDependencyBasedOnConfig( - release: { version: string; type: VersionType }, + cwd: string, + release: { + version: string; + oldVersion: string; + type: VersionType; + dir: string; + }, { depVersionRange, depType, @@ -34,8 +42,30 @@ export function shouldUpdateDependencyBasedOnConfig( }: { minReleaseType: "patch" | "minor"; onlyUpdatePeerDependentsWhenOutOfRange: boolean; - } + }, ): boolean { + const usesWorkspaceRange = depVersionRange.startsWith("workspace:"); + if (usesWorkspaceRange) { + depVersionRange = depVersionRange.replace(/^workspace:/, ""); + switch (depVersionRange) { + case "*": + // given the old range was exact, we can short circuit and return true + return true; + case "^": + case "~": + depVersionRange = `${depVersionRange}${release.oldVersion}`; + break; + default: { + if (!validRange(depVersionRange)) { + return ( + path.posix.normalize(depVersionRange) === + path.relative(cwd, release.dir).replace(/\\/g, "/") + ); + } + // fallthrough + } + } + } if (!semverSatisfies(release.version, depVersionRange)) { // Dependencies leaving semver range should always be updated return true; @@ -49,3 +79,7 @@ export function shouldUpdateDependencyBasedOnConfig( } return shouldUpdate; } + +export function capitalize(str: string) { + return str[0].toUpperCase() + str.slice(1); +} diff --git a/packages/apply-release-plan/src/version-package.ts b/packages/apply-release-plan/src/version-package.ts index d5230fb06..83acd17c6 100644 --- a/packages/apply-release-plan/src/version-package.ts +++ b/packages/apply-release-plan/src/version-package.ts @@ -1,13 +1,9 @@ -import { - ComprehensiveRelease, - PackageJSON, - VersionType, -} from "@changesets/types"; -import getVersionRangeType from "@changesets/get-version-range-type"; -import Range from "semver/classes/range"; -import semverPrerelease from "semver/functions/prerelease"; -import validRange from "semver/ranges/valid"; -import { shouldUpdateDependencyBasedOnConfig } from "./utils"; +import type { ModCompWithPackage, VersionType } from "@changesets/types"; +import Range from "semver/classes/range.js"; +import semverPrerelease from "semver/functions/prerelease.js"; +import validRange from "semver/ranges/valid.js"; +import type { EditJsonOperation } from "./edit-json.ts"; +import { shouldUpdateDependencyBasedOnConfig } from "./utils.ts"; const DEPENDENCY_TYPES = [ "dependencies", @@ -16,40 +12,54 @@ const DEPENDENCY_TYPES = [ "optionalDependencies", ] as const; -export default function versionPackage( - release: ComprehensiveRelease & { - changelog: string | null; - packageJson: PackageJSON; +export type ModCompWithPackageAndChangelog = ModCompWithPackage & { + changelog: string | null; +}; + +type ModCompWithPackageAndChangelogAndEdits = ModCompWithPackageAndChangelog & { + pkgJsonEdits: EditJsonOperation[]; +}; + +export function versionPackage( + release: ModCompWithPackageAndChangelog, + versionsToUpdate: Array<{ + name: string; + version: string; + oldVersion: string; + type: VersionType; dir: string; - }, - versionsToUpdate: Array<{ name: string; version: string; type: VersionType }>, + }>, { + cwd, updateInternalDependencies, onlyUpdatePeerDependentsWhenOutOfRange, bumpVersionsWithWorkspaceProtocolOnly, snapshot, }: { + cwd: string; updateInternalDependencies: "patch" | "minor"; onlyUpdatePeerDependentsWhenOutOfRange: boolean; bumpVersionsWithWorkspaceProtocolOnly?: boolean; snapshot?: string | boolean | undefined; - } -) { - let { newVersion, packageJson } = release; + }, +): ModCompWithPackageAndChangelogAndEdits { + const pkgJsonEdits: EditJsonOperation[] = []; + const { newVersion, packageJson } = release; - packageJson.version = newVersion; + pkgJsonEdits.push({ keys: ["version"], value: newVersion }); - for (let depType of DEPENDENCY_TYPES) { - let deps = packageJson[depType]; + for (const depType of DEPENDENCY_TYPES) { + const deps = packageJson[depType]; if (deps) { - for (let { name, version, type } of versionsToUpdate) { + for (const { name, version, oldVersion, type, dir } of versionsToUpdate) { let depCurrentVersion = deps[name]; if ( !depCurrentVersion || depCurrentVersion.startsWith("file:") || depCurrentVersion.startsWith("link:") || !shouldUpdateDependencyBasedOnConfig( - { version, type }, + cwd, + { version, oldVersion, type, dir }, { depVersionRange: depCurrentVersion, depType, @@ -57,7 +67,7 @@ export default function versionPackage( { minReleaseType: updateInternalDependencies, onlyUpdatePeerDependentsWhenOutOfRange, - } + }, ) ) { continue; @@ -67,7 +77,7 @@ export default function versionPackage( if ( !usesWorkspaceRange && (bumpVersionsWithWorkspaceProtocolOnly || - validRange(depCurrentVersion) === null) + validRange(depCurrentVersion) == null) ) { continue; } @@ -75,12 +85,13 @@ export default function versionPackage( if (usesWorkspaceRange) { const workspaceDepVersion = depCurrentVersion.replace( /^workspace:/, - "" + "", ); if ( workspaceDepVersion === "*" || workspaceDepVersion === "^" || - workspaceDepVersion === "~" + workspaceDepVersion === "~" || + validRange(workspaceDepVersion) == null ) { continue; } @@ -94,17 +105,28 @@ export default function versionPackage( new Range(depCurrentVersion).range !== "" || // ...unless the current version of a dependency is a prerelease (which doesn't satisfy x/X/*) // leaving those as is would leave the package in a non-installable state (wrong dep versions would get installed) - semverPrerelease(version) !== null + semverPrerelease(version) != null ) { let newNewRange = snapshot ? version : `${getVersionRangeType(depCurrentVersion)}${version}`; if (usesWorkspaceRange) newNewRange = `workspace:${newNewRange}`; - deps[name] = newNewRange; + pkgJsonEdits.push({ keys: [depType, name], value: newNewRange }); } } } } - return { ...release, packageJson }; + return { ...release, pkgJsonEdits }; +} + +function getVersionRangeType( + versionRange: string, +): "^" | "~" | ">=" | "<=" | ">" | "" { + if (versionRange.charAt(0) === "^") return "^"; + if (versionRange.charAt(0) === "~") return "~"; + if (versionRange.startsWith(">=")) return ">="; + if (versionRange.startsWith("<=")) return "<="; + if (versionRange.charAt(0) === ">") return ">"; + return ""; } diff --git a/packages/apply-release-plan/tsdown.config.ts b/packages/apply-release-plan/tsdown.config.ts new file mode 100644 index 000000000..8eddd7f1b --- /dev/null +++ b/packages/apply-release-plan/tsdown.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsdown/config"; +import { baseConfig } from "../../tsdown.config.ts"; + +export default defineConfig(baseConfig); diff --git a/packages/assemble-release-plan/CHANGELOG.md b/packages/assemble-release-plan/CHANGELOG.md index 05347726a..e80c27646 100644 --- a/packages/assemble-release-plan/CHANGELOG.md +++ b/packages/assemble-release-plan/CHANGELOG.md @@ -1,5 +1,90 @@ # @changesets/assemble-release-plan +## 7.0.0-next.6 + +### Minor Changes + +- [#2068](https://github.com/changesets/changesets/pull/2068) [`d03ffc1`](https://github.com/changesets/changesets/commit/d03ffc1d11fb486328734e52767379646062f5c1) Thanks [@bluwy](https://github.com/bluwy)! - Support `{commit-short}` placeholder for the `snapshot.prereleaseTemplate` config, which is a 7 character variant of `{commit}` + +## 7.0.0-next.5 + +### Patch Changes + +- Updated dependencies [[`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf)]: + - @changesets/types@7.0.0-next.5 + - @changesets/errors@1.0.0-next.3 + - @changesets/get-dependents-graph@3.0.0-next.5 + - @changesets/should-skip-package@1.0.0-next.5 + +## 7.0.0-next.4 + +### Patch Changes + +- Updated dependencies [[`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8), [`169b128`](https://github.com/changesets/changesets/commit/169b128522f0e53ef228f3acd8118709b0f72156)]: + - @changesets/types@7.0.0-next.4 + - @changesets/get-dependents-graph@3.0.0-next.4 + - @changesets/should-skip-package@1.0.0-next.4 + +## 7.0.0-next.3 + +### Major Changes + +- [#1954](https://github.com/changesets/changesets/pull/1954) [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped supported Node versions to `^22.11 || ^24 || >=26` + +- [#1956](https://github.com/changesets/changesets/pull/1956) [`03d4479`](https://github.com/changesets/changesets/commit/03d44794fedd24ae9eb053b28624c1fd8fe6fe6f) Thanks [@Andarist](https://github.com/Andarist)! - Drop the legacy compatibility shim in `assembleReleasePlan` that accepted older `config` and `snapshot` argument shapes. + +- [#1652](https://github.com/changesets/changesets/pull/1652) [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7) Thanks [@bluwy](https://github.com/bluwy)! - Remove support for the deprecated `___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.useCalculatedVersionForSnapshots` config. The `snapshot.useCalculatedVersion` config should be used instead. + +### Minor Changes + +- [#1969](https://github.com/changesets/changesets/pull/1969) [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Add a named export that mirrors the current `default` export + + The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. + +### Patch Changes + +- Updated dependencies [[`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069), [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625), [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7)]: + - @changesets/get-dependents-graph@3.0.0-next.3 + - @changesets/should-skip-package@1.0.0-next.3 + - @changesets/errors@1.0.0-next.2 + - @changesets/types@7.0.0-next.3 + +## 7.0.0-next.2 + +### Major Changes + +- [#1655](https://github.com/changesets/changesets/pull/1655) [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2) Thanks [@bluwy](https://github.com/bluwy)! - Update `@manypkg/get-packages` which drops support for detecting packages in Bolt monorepos and adds support for npm monorepos + +### Patch Changes + +- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c), [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2)]: + - @changesets/types@7.0.0-next.2 + - @changesets/get-dependents-graph@3.0.0-next.2 + - @changesets/should-skip-package@1.0.0-next.2 + +## 7.0.0-next.1 + +### Major Changes + +- [#1656](https://github.com/changesets/changesets/pull/1656) [`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d) Thanks [@bluwy](https://github.com/bluwy)! - Bumps minimum node version to `>=20.0.0` + +### Patch Changes + +- Updated dependencies [[`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d)]: + - @changesets/get-dependents-graph@3.0.0-next.1 + - @changesets/should-skip-package@1.0.0-next.1 + - @changesets/errors@1.0.0-next.1 + - @changesets/types@7.0.0-next.1 + +## 6.0.10 + +### Patch Changes + +- [#1888](https://github.com/changesets/changesets/pull/1888) [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix dependent bump detection for workspace path references. Dependencies declared with specifiers like `workspace:packages/pkg` are now resolved correctly when deciding whether dependents need a release. + +- Updated dependencies [[`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464)]: + - @changesets/get-dependents-graph@2.1.4 + ## 6.0.9 ### Patch Changes @@ -18,6 +103,22 @@ - [#1589](https://github.com/changesets/changesets/pull/1589) [`de8bebc`](https://github.com/changesets/changesets/commit/de8bebc93b81cb333c3c7e1ed8a3687926b7fcd8) Thanks [@remorses](https://github.com/remorses), [@vzt7](https://github.com/vzt7)! - Fixed a crash in prerelease mode when a package misses the version field in its `package.json` +## 7.0.0-next.0 + +### Major Changes + +- [#1479](https://github.com/changesets/changesets/pull/1479) [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5) Thanks [@bluwy](https://github.com/bluwy)! - Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. + +- [#1482](https://github.com/changesets/changesets/pull/1482) [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7) Thanks [@Andarist](https://github.com/Andarist)! - From now on this package is going to be published as ES module. + +### Patch Changes + +- Updated dependencies [[`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5), [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7)]: + - @changesets/get-dependents-graph@3.0.0-next.0 + - @changesets/should-skip-package@1.0.0-next.0 + - @changesets/errors@1.0.0-next.0 + - @changesets/types@7.0.0-next.0 + ## 6.0.6 ### Patch Changes diff --git a/packages/assemble-release-plan/README.md b/packages/assemble-release-plan/README.md index 241b2053b..99902047e 100644 --- a/packages/assemble-release-plan/README.md +++ b/packages/assemble-release-plan/README.md @@ -1,7 +1,7 @@ # Assemble Release Plan -[![npm package](https://img.shields.io/npm/v/@changesets/assemble-release-plan)](https://npmjs.com/package/@changesets/assemble-release-plan) -[![View changelog](https://img.shields.io/badge/Explore%20Changelog-brightgreen)](./CHANGELOG.md) +[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/@changesets/assemble-release-plan?name=true)](https://npmx.dev/package/@changesets/assemble-release-plan) +[![View changelog](https://npmx.dev/api/registry/badge/version/@changesets/cli?color=229fe4&value=View+changelog&label=+)](./CHANGELOG.md) Assemble a release plan for changesets from data about a repository. @@ -9,29 +9,38 @@ Usage ```ts import assembleReleasePlan from "@changesets/assemble-release-plan"; -import readChangesets from "@changesets/read"; -import { read } from "@changesets/config"; +import { readChangesets } from "@changesets/read"; +import { readConfig } from "@changesets/config"; import { getPackages } from "@manypkg/get-packages"; import { readPreState } from "@changesets/pre"; const packages = await getPackages(cwd); const preState = await readPreState(cwd); -const config = await read(cwd, packages); +const { config } = await readConfig(cwd, packages); const changesets = await readChangesets(cwd, sinceRef); -const releasePlan = assembleReleasePlan(changesets, packages, config, preState); +const releasePlan = assembleReleasePlan( + changesets, + packages, + config!, + preState, +); ``` Signature ```ts -import { NewChangeset, Config, ReleasePlan } from "@changesets/types"; -import { Packages } from "@manypkg/get-packages"; +import type { + NewChangeset, + Config, + Packages, + ReleasePlan, +} from "@changesets/types"; assembleReleasePlan = ( changesets: NewChangeset[], packages: Packages, - config: Config + config: Config, ) => ReleasePlan; ``` diff --git a/packages/assemble-release-plan/package.json b/packages/assemble-release-plan/package.json index 3d29bc8aa..1482d455b 100644 --- a/packages/assemble-release-plan/package.json +++ b/packages/assemble-release-plan/package.json @@ -1,32 +1,29 @@ { "name": "@changesets/assemble-release-plan", - "version": "6.0.9", + "version": "7.0.0-next.6", "description": "Reads changesets and adds information on dependents that need bumping", - "main": "dist/changesets-assemble-release-plan.cjs.js", - "module": "dist/changesets-assemble-release-plan.esm.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/changesets/changesets.git", + "directory": "packages/assemble-release-plan" + }, + "type": "module", "exports": { - ".": { - "types": { - "import": "./dist/changesets-assemble-release-plan.cjs.mjs", - "default": "./dist/changesets-assemble-release-plan.cjs.js" - }, - "module": "./dist/changesets-assemble-release-plan.esm.js", - "import": "./dist/changesets-assemble-release-plan.cjs.mjs", - "default": "./dist/changesets-assemble-release-plan.cjs.js" - }, + ".": "./dist/index.mjs", "./package.json": "./package.json" }, - "license": "MIT", - "repository": "https://github.com/changesets/changesets/tree/main/packages/assemble-release-plan", "dependencies": { - "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "semver": "^7.5.3" + "@changesets/errors": "workspace:^", + "@changesets/get-dependents-graph": "workspace:^", + "@changesets/should-skip-package": "workspace:^", + "@changesets/types": "workspace:^", + "semver": "^7.8.1" }, "devDependencies": { - "@changesets/config": "*" + "@changesets/config": "workspace:*" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } } diff --git a/packages/assemble-release-plan/src/apply-links.ts b/packages/assemble-release-plan/src/apply-links.ts index e954f65ee..1aaefe85e 100644 --- a/packages/assemble-release-plan/src/apply-links.ts +++ b/packages/assemble-release-plan/src/apply-links.ts @@ -1,7 +1,6 @@ -import { Linked } from "@changesets/types"; -import { Package } from "@manypkg/get-packages"; -import { InternalRelease } from "./types"; -import { getCurrentHighestVersion, getHighestReleaseType } from "./utils"; +import type { Linked, Package } from "@changesets/types"; +import type { InternalRelease } from "./types.ts"; +import { getCurrentHighestVersion, getHighestReleaseType } from "./utils.ts"; /* WARNING: @@ -15,33 +14,33 @@ import { getCurrentHighestVersion, getHighestReleaseType } from "./utils"; We could solve this by inlining this function, or by returning a deep-cloned then modified array, but we decided both of those are worse than this solution. */ -export default function applyLinks( +export function applyLinks( releases: Map, packagesByName: Map, - linked: Linked + linked: Linked, ): boolean { let updated = false; // We do this for each set of linked packages - for (let linkedPackages of linked) { + for (const linkedPackages of linked) { // First we filter down to all the relevant releases for one set of linked packages - let releasingLinkedPackages = [...releases.values()].filter( + const releasingLinkedPackages = [...releases.values()].filter( (release) => - linkedPackages.includes(release.name) && release.type !== "none" + linkedPackages.includes(release.name) && release.type !== "none", ); // If we proceed any further we do extra work with calculating highestVersion for things that might // not need one, as they only have workspace based packages if (releasingLinkedPackages.length === 0) continue; - let highestReleaseType = getHighestReleaseType(releasingLinkedPackages); - let highestVersion = getCurrentHighestVersion( + const highestReleaseType = getHighestReleaseType(releasingLinkedPackages); + const highestVersion = getCurrentHighestVersion( linkedPackages, - packagesByName + packagesByName, ); // Finally, we update the packages so all of them are on the highest version - for (let linkedPackage of releasingLinkedPackages) { + for (const linkedPackage of releasingLinkedPackages) { if (linkedPackage.type !== highestReleaseType) { updated = true; linkedPackage.type = highestReleaseType; diff --git a/packages/assemble-release-plan/src/determine-dependents.ts b/packages/assemble-release-plan/src/determine-dependents.ts index 47e32b6f0..4fa28ac00 100644 --- a/packages/assemble-release-plan/src/determine-dependents.ts +++ b/packages/assemble-release-plan/src/determine-dependents.ts @@ -1,14 +1,17 @@ +import path from "node:path"; import { shouldSkipPackage } from "@changesets/should-skip-package"; -import { +import type { Config, DependencyType, PackageJSON, VersionType, + Package, } from "@changesets/types"; -import { Package } from "@manypkg/get-packages"; -import semverSatisfies from "semver/functions/satisfies"; -import { incrementVersion } from "./increment"; -import { InternalRelease, PreInfo } from "./types"; +import semverSatisfies from "semver/functions/satisfies.js"; +import validRange from "semver/ranges/valid.js"; +import { incrementVersion } from "./increment.ts"; +import type { InternalRelease, PreInfo } from "./types.ts"; +import { mapGetOrThrowInternal } from "./utils.ts"; /* WARNING: @@ -22,40 +25,44 @@ import { InternalRelease, PreInfo } from "./types"; We could solve this by inlining this function, or by returning a deep-cloned then modified array, but we decided both of those are worse than this solution. */ -export default function determineDependents({ +export function determineDependents({ releases, packagesByName, + rootDir, dependencyGraph, preInfo, config, }: { releases: Map; packagesByName: Map; + rootDir: string; dependencyGraph: Map; preInfo: PreInfo | undefined; config: Config; }): boolean { let updated = false; // NOTE this is intended to be called recursively - let pkgsToSearch = [...releases.values()]; + const pkgsToSearch = [...releases.values()]; while (pkgsToSearch.length > 0) { // nextRelease is our dependency, think of it as "avatar" const nextRelease = pkgsToSearch.shift(); if (!nextRelease) continue; // pkgDependents will be a list of packages that depend on nextRelease ie. ['avatar-group', 'comment'] - const pkgDependents = dependencyGraph.get(nextRelease.name); - if (!pkgDependents) { - throw new Error( - `Error in determining dependents - could not find package in repository: ${nextRelease.name}` - ); - } + const pkgDependents = mapGetOrThrowInternal( + dependencyGraph, + nextRelease.name, + `Error in determining dependents - could not find package in repository: ${nextRelease.name}`, + ); pkgDependents .map((dependent) => { let type: VersionType | undefined; - const dependentPackage = packagesByName.get(dependent); - if (!dependentPackage) throw new Error("Dependency map is incorrect"); + const dependentPackage = mapGetOrThrowInternal( + packagesByName, + dependent, + "Dependency map is incorrect", + ); if ( shouldSkipPackage(dependentPackage, { @@ -65,9 +72,16 @@ export default function determineDependents({ ) { type = "none"; } else { + const dependencyPackage = mapGetOrThrowInternal( + packagesByName, + nextRelease.name, + "Dependency map is incorrect", + ); const dependencyVersionRanges = getDependencyVersionRanges( + rootDir, dependentPackage.packageJson, - nextRelease + nextRelease, + dependencyPackage, ); for (const { depType, versionRange } of dependencyVersionRanges) { @@ -94,7 +108,7 @@ export default function determineDependents({ .updateInternalDependents === "always" || !semverSatisfies( incrementVersion(nextRelease, preInfo), - versionRange + versionRange, )) ) { switch (depType) { @@ -130,9 +144,9 @@ export default function determineDependents({ }) .filter( ( - dependentItem + dependentItem, ): dependentItem is typeof dependentItem & { type: VersionType } => - !!dependentItem.type + !!dependentItem.type, ) .forEach(({ name, type, pkgJSON }) => { // At this point, we know if we are making a change @@ -149,7 +163,7 @@ export default function determineDependents({ pkgsToSearch.push(existing); } else { - let newDependent: InternalRelease = { + const newDependent: InternalRelease = { name, type, oldVersion: pkgJSON.version, @@ -171,8 +185,10 @@ export default function determineDependents({ dependency lists. For example, a package that is both a peerDepenency and a devDependency. */ function getDependencyVersionRanges( + rootDir: string, dependentPkgJSON: PackageJSON, - dependencyRelease: InternalRelease + dependencyRelease: InternalRelease, + dependencyPackage: Package, ): { depType: DependencyType; versionRange: string; @@ -202,7 +218,19 @@ function getDependencyVersionRanges( case "~": versionRange = `${versionRange}${dependencyRelease.oldVersion}`; break; - // default: keep the stripped range as is + default: { + if (!validRange(versionRange)) { + if ( + path.posix.normalize(versionRange) === + path.relative(rootDir, dependencyPackage.dir).replace(/\\/g, "/") + ) { + versionRange = dependencyRelease.oldVersion; + } else { + continue; + } + } + // fallthrough: keep the stripped range as is + } } } dependencyVersionRanges.push({ diff --git a/packages/assemble-release-plan/src/flatten-releases.ts b/packages/assemble-release-plan/src/flatten-releases.ts index b24af22e4..4e3e4247d 100644 --- a/packages/assemble-release-plan/src/flatten-releases.ts +++ b/packages/assemble-release-plan/src/flatten-releases.ts @@ -1,61 +1,63 @@ -// This function takes in changesets and returns one release per -// package listed in the changesets - import { shouldSkipPackage } from "@changesets/should-skip-package"; -import { Config, NewChangeset } from "@changesets/types"; -import { Package } from "@manypkg/get-packages"; -import { InternalRelease } from "./types"; +import type { Config, NewChangeset, Package } from "@changesets/types"; +import type { InternalRelease } from "./types.ts"; +import { mapGetOrThrowInternal } from "./utils.ts"; + +const changeTypes = { + major: 3, + minor: 2, + patch: 1, + none: 0, +} as const; -export default function flattenReleases( +/** + * Flattens a list of changesets into a package->release-type map + */ +export function flattenReleases( changesets: NewChangeset[], packagesByName: Map, - config: Config + config: Config, ): Map { - let releases: Map = new Map(); + const releases: Map = new Map(); + + // Iterate each changeset and its affected packages (`releases`) + for (const changeset of changesets) { + for (const { name, type } of changeset.releases) { + const pkg = mapGetOrThrowInternal( + packagesByName, + name, + `Couldn't find package named "${name}" listed in changeset "${changeset.id}"`, + ); - changesets.forEach((changeset) => { - changeset.releases // Filter out skipped packages because they should not trigger a release // If their dependencies need updates, they will be added to releases by `determineDependents()` with release type `none` - .filter( - ({ name }) => - !shouldSkipPackage(packagesByName.get(name)!, { - ignore: config.ignore, - allowPrivatePackages: config.privatePackages.version, - }) - ) - .forEach(({ name, type }) => { - let release = releases.get(name); - let pkg = packagesByName.get(name); - if (!pkg) { - throw new Error( - `"${changeset.id}" changeset mentions a release for a package "${name}" but such a package could not be found.` - ); - } - if (!release) { - release = { - name, - type, - oldVersion: pkg.packageJson.version, - changesets: [changeset.id], - }; - } else { - if ( - type === "major" || - ((release.type === "patch" || release.type === "none") && - (type === "minor" || type === "patch")) - ) { - release.type = type; - } - // Check whether the bumpType will change - // If the bumpType has changed recalc newVersion - // push new changeset to releases - release.changesets.push(changeset.id); - } - - releases.set(name, release); + const isSkipped = shouldSkipPackage(pkg, { + ignore: config.ignore, + allowPrivatePackages: config.privatePackages.version, }); - }); + if (isSkipped) continue; + + const release = releases.get(name); + + if (release == null) { + releases.set(name, { + name, + type, + oldVersion: pkg.packageJson.version, + changesets: [changeset.id], + }); + + continue; + } + + // if this changeset's type overrides the previous one + if (changeTypes[type] > changeTypes[release.type]) { + release.type = type; + } + + release.changesets.push(changeset.id); + } + } return releases; } diff --git a/packages/assemble-release-plan/src/increment.test.ts b/packages/assemble-release-plan/src/increment.test.ts index 130878111..c01934f5c 100644 --- a/packages/assemble-release-plan/src/increment.test.ts +++ b/packages/assemble-release-plan/src/increment.test.ts @@ -1,5 +1,6 @@ -import { incrementVersion } from "./increment"; -import { InternalRelease, PreInfo } from "./types"; +import { describe, expect, it } from "vitest"; +import { incrementVersion } from "./increment.ts"; +import type { InternalRelease, PreInfo } from "./types.ts"; describe("incrementVersion", () => { describe("pre mode", () => { diff --git a/packages/assemble-release-plan/src/increment.ts b/packages/assemble-release-plan/src/increment.ts index 3c1c735ac..ec8579c48 100644 --- a/packages/assemble-release-plan/src/increment.ts +++ b/packages/assemble-release-plan/src/increment.ts @@ -1,23 +1,22 @@ -import semverInc from "semver/functions/inc"; -import { InternalRelease, PreInfo } from "./types"; -import { InternalError } from "@changesets/errors"; +import semverInc from "semver/functions/inc.js"; +import type { InternalRelease, PreInfo } from "./types.ts"; +import { mapGetOrThrowInternal } from "./utils.ts"; export function incrementVersion( release: InternalRelease, - preInfo: PreInfo | undefined + preInfo: PreInfo | undefined, ) { if (release.type === "none") { return release.oldVersion; } let version = semverInc(release.oldVersion, release.type)!; - if (preInfo !== undefined && preInfo.state.mode !== "exit") { - let preVersion = preInfo.preVersions.get(release.name); - if (preVersion === undefined) { - throw new InternalError( - `preVersion for ${release.name} does not exist when preState is defined` - ); - } + if (preInfo != null && preInfo.state.mode !== "exit") { + const preVersion = mapGetOrThrowInternal( + preInfo.preVersions, + release.name, + `preVersion for ${release.name} does not exist when preState is defined`, + ); // why are we adding this ourselves rather than passing 'pre' + versionType to semver.inc? // because semver.inc with prereleases is confusing and this seems easier version += `-${preInfo.state.tag}.${preVersion}`; diff --git a/packages/assemble-release-plan/src/index.test.ts b/packages/assemble-release-plan/src/index.test.ts index 6ef1f9578..5b741de63 100644 --- a/packages/assemble-release-plan/src/index.test.ts +++ b/packages/assemble-release-plan/src/index.test.ts @@ -1,8 +1,12 @@ +import { randomUUID } from "node:crypto"; import { defaultConfig } from "@changesets/config"; -import assembleReleasePlan from "./"; -import FakeFullState from "./test-utils"; +import type { Config, VersionType } from "@changesets/types"; +import { inc } from "semver"; +import { beforeEach, describe, expect, it } from "vitest"; +import { assembleReleasePlan } from "./index.ts"; +import { FakeFullState } from "./test-utils.ts"; -describe("assemble-release-plan", () => { +describe("assembleReleasePlan", () => { let setup: FakeFullState; beforeEach(() => { @@ -13,12 +17,12 @@ describe("assemble-release-plan", () => { setup.addPackage("pkg-d", "1.0.0"); }); - it("should assemble release plan for basic setup", () => { - let { releases } = assembleReleasePlan( + it("should assemble plan for basic setup", () => { + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toBe(1); @@ -31,37 +35,37 @@ describe("assemble-release-plan", () => { }); }); - it("should assemble release plan for basic setup with snapshot", () => { - let { releases } = assembleReleasePlan( + it("should assemble plan for basic setup with snapshot", () => { + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, undefined, { tag: undefined, - } + }, ); expect(releases.length).toBe(1); expect(/0\.0\.0-\d{14}/.test(releases[0].newVersion)).toBeTruthy(); }); - it("should assemble release plan for basic setup with snapshot and tag", () => { - let { releases } = assembleReleasePlan( + it("should assemble plan for basic setup with snapshot and tag", () => { + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, undefined, { tag: "foo", - } + }, ); expect(releases.length).toBe(1); expect(/0\.0\.0-foo-\d{14}/.test(releases[0].newVersion)).toBeTruthy(); }); - it("should assemble release plan with multiple packages", () => { + it("should assemble plan with multiple packages", () => { setup.addChangeset({ id: "big-cats-delight", releases: [ @@ -71,11 +75,11 @@ describe("assemble-release-plan", () => { ], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toBe(4); @@ -88,17 +92,18 @@ describe("assemble-release-plan", () => { expect(releases[3].name).toBe("pkg-d"); expect(releases[3].newVersion).toBe("2.0.0"); }); + it("should handle two changesets for a package", () => { setup.addChangeset({ id: "big-cats-delight", releases: [{ name: "pkg-a", type: "major" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toEqual(1); @@ -106,7 +111,8 @@ describe("assemble-release-plan", () => { expect(releases[0].type).toEqual("major"); expect(releases[0].newVersion).toEqual("2.0.0"); }); - it("`none` changeset should not override other release types", () => { + + it("none should not override any other release types", () => { setup.addChangeset({ id: "big-cats-delight", releases: [ @@ -132,11 +138,11 @@ describe("assemble-release-plan", () => { ], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toEqual(3); @@ -153,36 +159,16 @@ describe("assemble-release-plan", () => { expect(releases[2].type).toEqual("major"); expect(releases[2].newVersion).toEqual("2.0.0"); }); - it("should assemble release plan with dependents", () => { - setup.updateDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - expect(releases[1].changesets).toEqual([]); - }); it("should update multiple dependents of a single package", () => { setup.updateDependency("pkg-b", "pkg-a", "1.0.0"); setup.updateDependency("pkg-c", "pkg-a", "1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toEqual(3); @@ -193,37 +179,42 @@ describe("assemble-release-plan", () => { expect(releases[2].name).toEqual("pkg-c"); expect(releases[2].newVersion).toEqual("1.0.1"); }); - it("should update a second dependent based on updating a first dependent", () => { + + it("should update dependents all the way down the dep tree", () => { setup.updateDependency("pkg-b", "pkg-a", "1.0.0"); setup.updateDependency("pkg-c", "pkg-b", "1.0.0"); + setup.updateDependency("pkg-d", "pkg-c", "1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); - expect(releases.length).toEqual(3); + expect(releases.length).toEqual(4); expect(releases[0].name).toEqual("pkg-a"); expect(releases[0].newVersion).toEqual("1.0.1"); expect(releases[1].name).toEqual("pkg-b"); expect(releases[1].newVersion).toEqual("1.0.1"); expect(releases[2].name).toEqual("pkg-c"); expect(releases[2].newVersion).toEqual("1.0.1"); + expect(releases[3].name).toEqual("pkg-d"); + expect(releases[3].newVersion).toEqual("1.0.1"); }); - it("should assemble release plan with without a wildcard dependent", () => { + + it("should not bump packages with a wildcard dependency", () => { setup.updateDependency("pkg-b", "pkg-a", "*"); setup.addChangeset({ id: "big-cats-delight", releases: [{ name: "pkg-a", type: "major" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toEqual(1); @@ -231,379 +222,230 @@ describe("assemble-release-plan", () => { expect(releases[0].newVersion).toEqual("2.0.0"); }); - it("should assemble the release plan only with workspace protocol dependents when using bumpVersionsWithWorkspaceProtocolOnly", () => { - setup.updateDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.updateDependency("pkg-c", "pkg-a", "workspace:^1.0.0"); + it("throws error when changeset contains package that is not in workspace", () => { setup.addChangeset({ id: "big-cats-delight", releases: [{ name: "pkg-a", type: "major" }], }); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - bumpVersionsWithWorkspaceProtocolOnly: true, - }, - undefined - ); - - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-c"); - expect(releases[1].newVersion).toEqual("1.0.1"); - expect(releases[1].changesets).toEqual([]); - }); - it("should assemble the release plan with workspace:^ and workspace:~ dependents", () => { - setup.updateDependency("pkg-b", "pkg-a", "workspace:~"); - setup.updateDependency("pkg-c", "pkg-a", "workspace:^"); setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], + id: "small-dogs-sad", + releases: [{ name: "pkg-z", type: "minor" }], }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - bumpVersionsWithWorkspaceProtocolOnly: true, - }, - undefined + expect(() => + assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ), + ).toThrow( + "Found changeset small-dogs-sad for package pkg-z which is not in the workspace", ); + }); - expect(releases.length).toEqual(3); - - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); + describe("link: protocol", () => { + it("should not touch link: ranges", () => { + setup.updateDevDependency("pkg-b", "pkg-a", "link:../pkg-a"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - expect(releases[1].changesets).toEqual([]); + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ); - expect(releases[2].name).toEqual("pkg-c"); - expect(releases[2].newVersion).toEqual("1.0.1"); - expect(releases[2].changesets).toEqual([]); - }); - it("should assemble release plan without dependent through dev dependency", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], + expect(releases.length).toEqual(1); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); }); + }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + describe("file: protocol", () => { + it("should not touch file: ranges", () => { + setup.updateDevDependency("pkg-b", "pkg-a", "file:../pkg-a"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - it("should assemble release plan with dependent when the dependent has both a changed prod and dev dependency", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.updateDependency("pkg-b", "pkg-c", "^1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [ - { name: "pkg-a", type: "major" }, - { name: "pkg-c", type: "major" }, - ], + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ); + + expect(releases.length).toEqual(1); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); }); + }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + describe("ignored packages", () => { + it("does not touch ignored packages with changesets", () => { + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); + setup.addChangeset({ + id: "small-dogs-sad", + releases: [{ name: "pkg-b", type: "minor" }], + }); + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + { + ...defaultConfig, + ignore: ["pkg-b"], + }, + undefined, + ); - expect(releases.length).toEqual(3); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-c"); - expect(releases[1].newVersion).toEqual("2.0.0"); - expect(releases[2].name).toEqual("pkg-b"); - expect(releases[2].oldVersion).toEqual("1.0.0"); - expect(releases[2].newVersion).toEqual("1.0.1"); - }); - it("should assemble release plan without dependencies when the dependent has a changeset type of none", () => { - setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-b", type: "none" }], + expect(releases.length).toEqual(1); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + it("creates 'none' releases for ignored dependencies", () => { + setup.updateDependency("pkg-b", "pkg-a", "1.0.0"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); + setup.addChangeset({ + id: "small-dogs-sad", + releases: [{ name: "pkg-b", type: "minor" }], + }); + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + { + ...defaultConfig, + ignore: ["pkg-b"], + }, + undefined, + ); - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].oldVersion).toEqual("1.0.0"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - it("should assemble release plan without dependent through the link protocol", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "link:../pkg-a"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], + expect(releases.length).toEqual(2); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-b"); + expect(releases[1].type).toEqual("none"); + expect(releases[1].newVersion).toEqual("1.0.0"); }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + it("creates 'none' releases for ignored peerDependencies", () => { + setup.updatePeerDependency("pkg-b", "pkg-a", "1.0.0"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); + setup.addChangeset({ + id: "small-dogs-sad", + releases: [{ name: "pkg-b", type: "minor" }], + }); + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + { + ...defaultConfig, + ignore: ["pkg-b"], + }, + undefined, + ); - expect(releases.length).toEqual(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - }); - it("should assemble release plan without dependent through the file protocol", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "file:../pkg-a"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], + expect(releases.length).toEqual(2); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-b"); + expect(releases[1].type).toEqual("none"); + expect(releases[1].newVersion).toEqual("1.0.0"); }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + it("creates 'none' releases for ignored devDependencies", () => { + setup.updateDevDependency("pkg-b", "pkg-a", "1.0.0"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); + setup.addChangeset({ + id: "small-dogs-sad", + releases: [{ name: "pkg-b", type: "minor" }], + }); + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + { + ...defaultConfig, + ignore: ["pkg-b"], + }, + undefined, + ); - expect(releases.length).toEqual(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - }); - it("should update a peerDep by a major bump", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); - setup.addChangeset({ - id: "nonsense-words-combine", - releases: [{ name: "pkg-a", type: "minor" }], + expect(releases.length).toEqual(2); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-b"); + expect(releases[1].type).toEqual("none"); + expect(releases[1].newVersion).toEqual("1.0.0"); }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + it("should throw if changeset includes both ignored and non-ignored packages", () => { + setup.addChangeset({ + id: "big-cats-delight", + releases: [ + { name: "pkg-a", type: "major" }, + { name: "pkg-b", type: "minor" }, + ], + }); - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("2.0.0"); - }); - it("should assemble release plan without ignored packages", () => { - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - setup.addChangeset({ - id: "small-dogs-sad", - releases: [{ name: "pkg-b", type: "minor" }], - }); - const { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ignore: ["pkg-b"], - }, - undefined - ); - - expect(releases.length).toEqual(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - }); - it("should generate releases with 'none' release type for ignored packages through dependencies", () => { - setup.updateDependency("pkg-b", "pkg-a", "1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - setup.addChangeset({ - id: "small-dogs-sad", - releases: [{ name: "pkg-b", type: "minor" }], - }); - const { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ignore: ["pkg-b"], - }, - undefined - ); - - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].type).toEqual("none"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - it("should generate releases with 'none' release type for ignored packages through peerDependencies", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - setup.addChangeset({ - id: "small-dogs-sad", - releases: [{ name: "pkg-b", type: "minor" }], - }); - const { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ignore: ["pkg-b"], - }, - undefined - ); - - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].type).toEqual("none"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - it("should generate releases with 'none' release type for ignored packages through devDependencies", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - setup.addChangeset({ - id: "small-dogs-sad", - releases: [{ name: "pkg-b", type: "minor" }], - }); - const { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ignore: ["pkg-b"], - }, - undefined - ); - - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].type).toEqual("none"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - // Mixed changesets are the ones that contains both ignored packages and not ignored packages - it("should throw for mixed changesets", () => { - setup.addChangeset({ - id: "big-cats-delight", - releases: [ - { name: "pkg-a", type: "major" }, - { name: "pkg-b", type: "minor" }, - ], - }); - - expect(() => - assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ignore: ["pkg-b"], - }, - undefined - ) - ).toThrowErrorMatchingInlineSnapshot(` -"Found mixed changeset big-cats-delight -Found ignored packages: pkg-b -Found not ignored packages: pkg-a -Mixed changesets that contain both ignored and not ignored packages are not allowed" -`); - }); - - it("should not bump a dev dependent nor its dependent when a package gets bumped", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "1.0.0"); - setup.updateDependency("pkg-c", "pkg-b", "1.0.0"); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - - it("should throw an error when a changeset contains a package that is not in the workspace", () => { - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-a", type: "major" }], - }); - setup.addChangeset({ - id: "small-dogs-sad", - releases: [{ name: "pkg-z", type: "minor" }], + expect(() => + assembleReleasePlan( + setup.changesets, + setup.packages, + { + ...defaultConfig, + ignore: ["pkg-b"], + }, + undefined, + ), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: Found mixed changeset big-cats-delight + Found ignored packages: pkg-b + Found not ignored packages: pkg-a + Mixed changesets that contain both ignored and not ignored packages are not allowed] + `); }); - - expect(() => - assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ) - ).toThrow( - "Found changeset small-dogs-sad for package pkg-z which is not in the workspace" - ); }); describe("fixed packages", () => { - it("should assemble release plan for fixed packages", () => { + it("should bump all fixed packages together", () => { setup.addChangeset({ id: "just-some-umbrellas", releases: [{ name: "pkg-a", type: "minor" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, fixed: [["pkg-a", "pkg-b"]], }, - undefined + undefined, ); expect(releases.length).toEqual(2); expect(releases[0].newVersion).toEqual("1.1.0"); expect(releases[1].newVersion).toEqual("1.1.0"); }); - it("should assemble a release plan where new highest version is set by an unreleased package", () => { + + it("should bump versions when the version is determined by an unreleased package", () => { setup.addChangeset({ id: "just-some-umbrellas", releases: [ @@ -614,14 +456,14 @@ Mixed changesets that contain both ignored and not ignored packages are not allo setup.updatePackage("pkg-c", "2.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, fixed: [["pkg-a", "pkg-b", "pkg-c"]], }, - undefined + undefined, ); expect(releases.length).toEqual(3); @@ -630,11 +472,11 @@ Mixed changesets that contain both ignored and not ignored packages are not allo expect(releases[2].newVersion).toEqual("2.1.0"); }); - it("should assemble release plan where a fixed constraint causes a dependency to need changing which causes a second fixed group to update", () => { + it("should bump multiple fixed groups in a chain when one depends on another", () => { // Expected events: // - dependencies are checked, nothing leaves semver, nothing changes // - fixed are checked, pkg-a is aligned with pkg-b - // - depencencies are checked, in pkg-c the dependency range for pkg-a is not satisfied, so a patch bump is given to it + // - dependencies are checked, in pkg-c the dependency range for pkg-a is not satisfied, so a patch bump is given to it // - fixed are checked, pkg-c is aligned with pkg-d setup.addChangeset({ id: "just-some-umbrellas", @@ -647,7 +489,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo setup.updateDependency("pkg-c", "pkg-a", "^1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { @@ -657,7 +499,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo ["pkg-c", "pkg-d"], ], }, - undefined + undefined, ); expect(releases.length).toEqual(4); @@ -670,7 +512,8 @@ Mixed changesets that contain both ignored and not ignored packages are not allo expect(releases[3].name).toEqual("pkg-c"); expect(releases[3].newVersion).toEqual("1.1.0"); }); - it("should assemble release plan where a fixed constraint causes a dependency to need changing which causes a second fixed group to update 2", () => { + + it("should bump multiple fixed groups in a chain when one depends on another 2", () => { setup.addChangeset({ id: "just-some-umbrellas", releases: [{ name: "pkg-a", type: "major" }], @@ -682,7 +525,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { @@ -692,7 +535,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo ["pkg-c", "pkg-d"], ], }, - undefined + undefined, ); expect(releases.length).toEqual(4); @@ -705,8 +548,9 @@ Mixed changesets that contain both ignored and not ignored packages are not allo expect(releases[3].name).toEqual("pkg-c"); expect(releases[3].newVersion).toEqual("1.1.0"); }); + it("should return an empty release array when no changes will occur", () => { - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( [], setup.packages, { @@ -716,7 +560,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo ["pkg-c", "pkg-d"], ], }, - undefined + undefined, ); expect(releases).toEqual([]); @@ -730,14 +574,14 @@ Mixed changesets that contain both ignored and not ignored packages are not allo releases: [{ type: "minor", name: "pkg-a" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, fixed: [["pkg-a", "pkg-c"]], }, - undefined + undefined, ); expect(releases).toMatchObject([ @@ -758,27 +602,28 @@ Mixed changesets that contain both ignored and not ignored packages are not allo }); describe("linked packages", () => { - it("should assemble release plan for linked packages", () => { + it("should bump linked packages together", () => { setup.addChangeset({ id: "just-some-umbrellas", releases: [{ name: "pkg-b", type: "major" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, linked: [["pkg-a", "pkg-b"]], }, - undefined + undefined, ); expect(releases.length).toEqual(2); expect(releases[0].newVersion).toEqual("2.0.0"); expect(releases[1].newVersion).toEqual("2.0.0"); }); - it("should assemble a release plan where new highest version is set by an unreleased package", () => { + + it("should bump versions when the version is determined by an unreleased package", () => { setup.addChangeset({ id: "just-some-umbrellas", releases: [ @@ -789,21 +634,22 @@ Mixed changesets that contain both ignored and not ignored packages are not allo setup.updatePackage("pkg-c", "2.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, linked: [["pkg-a", "pkg-b", "pkg-c"]], }, - undefined + undefined, ); expect(releases.length).toEqual(2); expect(releases[0].newVersion).toEqual("2.1.0"); expect(releases[1].newVersion).toEqual("2.1.0"); }); - it("should assemble release plan where a link causes a dependency to need changing which causes a second link to update", () => { + + it("should bump multiple linked groups in a chain when one depends on another", () => { /* Expected events: - dependencies are checked, nothing leaves semver, nothing changes @@ -822,7 +668,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo setup.updateDependency("pkg-c", "pkg-a", "^1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { @@ -832,7 +678,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo ["pkg-c", "pkg-d"], ], }, - undefined + undefined, ); expect(releases.length).toEqual(4); @@ -841,8 +687,9 @@ Mixed changesets that contain both ignored and not ignored packages are not allo expect(releases[2].newVersion).toEqual("1.1.0"); expect(releases[3].newVersion).toEqual("1.1.0"); }); + it("should return an empty release array when no changes will occur", () => { - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( [], setup.packages, { @@ -852,11 +699,12 @@ Mixed changesets that contain both ignored and not ignored packages are not allo ["pkg-c", "pkg-d"], ], }, - undefined + undefined, ); expect(releases).toEqual([]); }); + it("should bump peer dependents where the version is updated because of linked", () => { setup.updatePeerDependency("pkg-b", "pkg-a", "1.0.0"); @@ -865,14 +713,14 @@ Mixed changesets that contain both ignored and not ignored packages are not allo releases: [{ type: "minor", name: "pkg-c" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, linked: [["pkg-a", "pkg-c"]], }, - undefined + undefined, ); expect(releases).toMatchObject([ @@ -905,7 +753,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo tag: "next", initialVersions: {}, mode: "exit", - } + }, ); expect(releases.length).toEqual(1); @@ -932,7 +780,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo "pkg-b": "1.0.0", }, mode: "exit", - } + }, ); expect(releases.length).toEqual(2); @@ -962,7 +810,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo "pkg-b": "1.0.0", }, mode: "exit", - } + }, ); expect(releases.length).toEqual(2); @@ -1010,7 +858,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo "pkg-c": "1.0.0", }, mode: "pre", - } + }, ); expect(releases.length).toEqual(1); @@ -1021,93 +869,91 @@ Mixed changesets that contain both ignored and not ignored packages are not allo }); describe("workspace protocol", () => { - it("should assemble release plan without workspace dependencies when the dependent has a changeset type of none", () => { - setup.updateDependency("pkg-c", "pkg-b", "workspace:^1.0.0"); - setup.addChangeset({ - id: "big-cats-delight", - releases: [{ name: "pkg-b", type: "none" }], - }); + // (workspace:path patch) => 1.0.1 + it("should assemble plan with workspace:path dependencies", () => { + setup.updateDependency("pkg-b", "pkg-a", "workspace:packages/pkg-a"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); expect(releases.length).toEqual(2); expect(releases[0].name).toEqual("pkg-a"); expect(releases[1].name).toEqual("pkg-b"); expect(releases[1].oldVersion).toEqual("1.0.0"); - expect(releases[1].newVersion).toEqual("1.0.0"); + expect(releases[1].newVersion).toEqual("1.0.1"); }); - it("should assemble release plan without workspace:* dependencies when the dependent has a changeset type of none", () => { - setup.updateDependency("pkg-c", "pkg-b", "workspace:*"); + }); + + describe("bumpVersionsWithWorkspaceProtocolOnly", () => { + it("should only bump packages with workspace protocol", () => { + setup.updateDependency("pkg-b", "pkg-a", "^1.0.0"); + setup.updateDependency("pkg-c", "pkg-a", "workspace:^1.0.0"); setup.addChangeset({ id: "big-cats-delight", - releases: [{ name: "pkg-b", type: "none" }], + releases: [{ name: "pkg-a", type: "major" }], }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].oldVersion).toEqual("1.0.0"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); - it("should assemble release plan with workspace:* dependencies", () => { - setup.updateDependency("pkg-b", "pkg-a", "workspace:*"); - - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, - defaultConfig, - undefined + { + ...defaultConfig, + bumpVersionsWithWorkspaceProtocolOnly: true, + }, + undefined, ); expect(releases.length).toEqual(2); expect(releases[0].name).toEqual("pkg-a"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].oldVersion).toEqual("1.0.0"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-c"); expect(releases[1].newVersion).toEqual("1.0.1"); + expect(releases[1].changesets).toEqual([]); }); - }); - describe("updateInternalDependents: always", () => { - it("should bump a direct dependent when a dependency package gets bumped", () => { - setup.updateDependency("pkg-b", "pkg-a", "^1.0.0"); + it("should bump packages with workspace:^ and workspace:~ ranges", () => { + setup.updateDependency("pkg-b", "pkg-a", "workspace:~"); + setup.updateDependency("pkg-c", "pkg-a", "workspace:^"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-a", type: "major" }], + }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { ...defaultConfig, - ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { - ...defaultConfig.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, - updateInternalDependents: "always", - }, + bumpVersionsWithWorkspaceProtocolOnly: true, }, - undefined + undefined, ); - expect(releases.length).toBe(2); + expect(releases.length).toEqual(3); + expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-b"); expect(releases[1].newVersion).toEqual("1.0.1"); + expect(releases[1].changesets).toEqual([]); + + expect(releases[2].name).toEqual("pkg-c"); + expect(releases[2].newVersion).toEqual("1.0.1"); + expect(releases[2].changesets).toEqual([]); }); + }); + describe("updateInternalDependents: always", () => { it("should bump a transitive dependent when a dependency package gets bumped", () => { setup.updateDependency("pkg-b", "pkg-a", "^1.0.0"); setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { @@ -1117,7 +963,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo updateInternalDependents: "always", }, }, - undefined + undefined, ); expect(releases.length).toBe(3); @@ -1136,7 +982,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo releases: [{ name: "pkg-c", type: "none" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, { @@ -1146,7 +992,7 @@ Mixed changesets that contain both ignored and not ignored packages are not allo updateInternalDependents: "always", }, }, - undefined + undefined, ); expect(releases.length).toBe(2); @@ -1155,362 +1001,448 @@ Mixed changesets that contain both ignored and not ignored packages are not allo expect(releases[1].name).toEqual("pkg-c"); expect(releases[1].newVersion).toEqual("1.0.0"); }); - - it("should not bump a dev dependent nor its dependent when a package gets bumped", () => { - setup.updateDevDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - { - ...defaultConfig, - ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { - ...defaultConfig.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, - updateInternalDependents: "always", - }, - }, - undefined - ); - - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.0"); - }); }); }); -describe("version update thoroughness", () => { - let setup: FakeFullState; - beforeEach(() => { - setup = new FakeFullState(); - - setup.addPackage("pkg-b", "1.0.0"); - setup.addPackage("pkg-c", "1.0.0"); - setup.addPackage("pkg-d", "1.0.0"); - setup.updateDependency("pkg-b", "pkg-a", "1.0.0"); - setup.updateDependency("pkg-c", "pkg-a", "~1.0.0"); - setup.updateDependency("pkg-d", "pkg-a", "^1.0.0"); - }); - - it("should patch a single pinned dependent", () => { - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - expect(releases.length).toEqual(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - }); - it("should path a pinned and tilde dependents when minor versioning", () => { - setup.addChangeset({ - id: "stuff-and-nonsense", - releases: [{ name: "pkg-a", type: "minor" }], - }); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined +describe("dependent bumping", () => { + type DeepPartial = T extends object + ? { [P in keyof T]?: DeepPartial } + : T; + + /** Semver range prefix written on the dependency. */ + type Range = "^" | "~" | "="; + /** Which field the dependency lives in. */ + type DepKind = "dep" | "dev" | "peer"; + + const RANGES = ["^", "~", "="] as const satisfies readonly Range[]; + const DEP_KINDS = [ + "dep", + "dev", + "peer", + ] as const satisfies readonly DepKind[]; + const BUMPS = [ + "none", + "patch", + "minor", + "major", + ] as const satisfies readonly VersionType[]; + const BASE_VERSION = "1.0.0"; + + // ---- Expectation tables ---- + // + // The whole matrix lives in one readable table, indexed `expected[dep][bump][range]`. + // The value is the resulting version of the *dependent* (`pkg-a`); + + type ExpectationTable = Record< + DepKind, + Record> + >; + + // oxfmt-ignore + const baseExpectations: ExpectationTable = { + // direct dependent has to be bumped only when dependency falls out of allowed range + // otherwise, installing the latest versions of dependent and dependency would result in duplicate dependency package in the tree + dep: { + none: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + patch: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.1" }, + minor: { "^": "1.0.0", "~": "1.0.1", "=": "1.0.1" }, + major: { "^": "1.0.1", "~": "1.0.1", "=": "1.0.1" }, + }, + // devDependent should stay untouched, given the dev dependency doesn't affect production installations + dev: { + none: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + patch: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + minor: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + major: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + }, + peer: { + none: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.0" }, + patch: { "^": "1.0.0", "~": "1.0.0", "=": "1.0.1" }, + minor: { "^": "2.0.0", "~": "2.0.0", "=": "2.0.0" }, + major: { "^": "2.0.0", "~": "2.0.0", "=": "2.0.0" }, + }, + }; + + // ---- Test cases ---- + + type Case = { + range: Range; + dep: DepKind; + bump: VersionType; + /** Expected resulting version of the dependent (`pkg-a`). */ + expected: string; + /** Set when an override changed the baseline expectation (for the title). */ + overriddenFrom?: string; + }; + + /** Flattens the expectation table into one `Case` per cell. */ + function casesFromTable(table: ExpectationTable): Case[] { + return DEP_KINDS.flatMap((dep) => + BUMPS.flatMap((bump) => + RANGES.map((range) => ({ + dep, + bump, + range, + expected: table[dep][bump][range], + })), + ), ); + } + + /** + * Applies a partial expectation table on top of the baseline cases. + * Only the cells you list change; everything else stays at the baseline. + * Invalid keys are caught by the type checker, so there's no runtime + * "no matching case" guard to maintain. + */ + function applyOverrides( + cases: Case[], + overrides: DeepPartial, + ): Case[] { + return cases.map((c) => { + const expected = overrides[c.dep]?.[c.bump]?.[c.range]; + if (expected == null) return c; + + if (expected === c.expected) { + throw new Error( + `Override for ${c.range}${c.dep}:${c.bump} is invalid. ${expected} is same as original value.`, + ); + } - expect(releases.length).toEqual(3); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - expect(releases[2].name).toEqual("pkg-c"); - expect(releases[2].newVersion).toEqual("1.0.1"); - }); - it("should patch pinned, tilde and caret dependents when a major versioning", () => { + return { ...c, expected, overriddenFrom: c.expected }; + }); + } + + // ---- Execution ---- + + /** Turns a canonical range into the string written to package.json. */ + type RangeRenderer = (range: Range) => string; + + const defaultRange: RangeRenderer = (range) => + range === "=" ? BASE_VERSION : `${range}${BASE_VERSION}`; + + // oxfmt-ignore + const writeDependency: Record void> = { + dep: (setup, range) => + setup.updateDependency("pkg-a", "pkg-a-b", range), + dev: (setup, range) => + setup.updateDevDependency("pkg-a", "pkg-a-b", range), + peer: (setup, range) => + setup.updatePeerDependency("pkg-a", "pkg-a-b", range), + }; + + function runCase(c: Case, config: Config, renderRange: RangeRenderer) { + /* + * Set up the test "workspace": + * - `pkg-a` depends on `pkg-a-b` via dependency kind `dep` + * using the range produced by `renderRange(range)` + * - `pkg-a-b` is bumped by `bump` + */ + const setup = new FakeFullState({ changesets: [] }); + setup.addPackage("pkg-a-b", BASE_VERSION); setup.addChangeset({ - id: "stuff-and-nonsense", - releases: [{ name: "pkg-a", type: "major" }], + id: randomUUID(), + releases: [{ name: "pkg-a-b", type: c.bump }], }); + writeDependency[c.dep](setup, renderRange(c.range)); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, - defaultConfig, - undefined + config, + undefined, ); - expect(releases.length).toEqual(4); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - expect(releases[2].name).toEqual("pkg-c"); - expect(releases[2].newVersion).toEqual("1.0.1"); - expect(releases[3].name).toEqual("pkg-d"); - expect(releases[3].newVersion).toEqual("1.0.1"); - }); -}); - -describe("bumping peerDeps", () => { - let setup: FakeFullState; - beforeEach(() => { - setup = new FakeFullState(); - setup.addPackage("pkg-b", "1.0.0"); - }); - - it("should patch a pinned peerDep", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "1.0.0"); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined + // Sanity check: the dependency itself bumped as requested. + const dependency = releases.find((r) => r.name === "pkg-a-b"); + expect(dependency).toBeDefined(); + expect(dependency!.newVersion).toEqual( + c.bump !== "none" ? inc(BASE_VERSION, c.bump) : BASE_VERSION, ); - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("1.0.1"); - }); - it("should not bump the dependent when bumping a tilde peerDep by none", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); - setup.changesets = []; - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "none" }], + // The dependent got bumped (or not) as expected. + const dependent = releases.find((r) => r.name === "pkg-a"); + if (c.expected === BASE_VERSION) { + expect(dependent?.newVersion).toBeOneOf(["1.0.0", undefined]); + } else { + expect(dependent?.newVersion).toEqual(c.expected); + } + } + + // ---- Suite builder ---- + + function fieldWidths(cases: Case[]) { + return { + range: Math.max(...cases.map((c) => c.range.length)), + dep: Math.max(...cases.map((c) => c.dep.length)), + bump: Math.max(...cases.map((c) => c.bump.length)), + }; + } + + function caseTitle(c: Case, widths: ReturnType): string { + const range = c.range.padEnd(widths.range); + const dep = c.dep.padStart(widths.dep); + const bump = c.bump.padEnd(widths.bump); + + let title = `(${range} ${dep} ${bump}) => ${c.expected}`; + if (c.overriddenFrom != null) { + title += ` (overridden from ${c.overriddenFrom})`; + } + return title; + } + + type SuiteOptions = { + config?: DeepPartial; + overrides?: DeepPartial; + /** Override how the range is written (e.g. the `workspace:` protocol). */ + renderRange?: RangeRenderer; + }; + + function describeDependentBumping( + name: string, + { + config = {}, + overrides = {}, + renderRange = defaultRange, + }: SuiteOptions = {}, + ) { + const mergedConfig = { + ...defaultConfig, + ...config, + ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { + ...defaultConfig?.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, + ...config?.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, + }, + } as Config; + const cases = applyOverrides(casesFromTable(baseExpectations), overrides); + const widths = fieldWidths(cases); + + // eslint-disable-next-line vitest/valid-title + describe(name, () => { + for (const testCase of cases) { + // eslint-disable-next-line vitest/valid-title, vitest/expect-expect + it(caseTitle(testCase, widths), () => { + runCase(testCase, mergedConfig, renderRange); + }); + } }); + } - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - - expect(releases.length).toBe(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.0"); - }); - it("should not bump the dependent when bumping a tilde peerDep by a patch", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); + // ---- Suites ---- - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + describeDependentBumping("default config"); - expect(releases.length).toBe(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); + // makes all non-none bumps result in a patch bump of a dependent + describeDependentBumping("updateInternalDependents: always", { + config: { + ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { + updateInternalDependents: "always", + }, + }, + overrides: { + dep: { + patch: { "^": "1.0.1", "~": "1.0.1" }, + minor: { "^": "1.0.1" }, + }, + peer: { + patch: { "^": "1.0.1", "~": "1.0.1" }, + }, + }, }); - it("should major bump dependent when bumping a tilde peerDep by minor", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "minor" }], - }); - - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("2.0.0"); + describeDependentBumping("onlyUpdatePeerDependentsWhenOutOfRange: true", { + config: { + ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { + onlyUpdatePeerDependentsWhenOutOfRange: true, + }, + }, + overrides: { + peer: { minor: { "^": "1.0.0" } }, + }, }); - it("should major bump dependent when bumping a tilde peerDep by major", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "major" }], - }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + // weird case, should probably not be allowed + describeDependentBumping( + "onlyUpdatePeerDependentsWhenOutOfRange + updateInternalDependents combined", + { + config: { + ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { + onlyUpdatePeerDependentsWhenOutOfRange: true, + updateInternalDependents: "always", + }, + }, + overrides: { + dep: { + patch: { "^": "1.0.1", "~": "1.0.1" }, + minor: { "^": "1.0.1" }, + }, + peer: { + patch: { "^": "1.0.1", "~": "1.0.1" }, + minor: { "^": "1.0.1" }, + }, + }, + }, + ); - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("2.0.0"); - }); - it("should not bump dependent when bumping caret peerDep by none", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.changesets = []; - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "none" }], + describe("workspace: protocol works the same as without it", () => { + describeDependentBumping("modifier only", { + // render workspace:*, workspace:^, workspace:~ + renderRange: (range) => `workspace:${range !== "=" ? range : "*"}`, }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - - expect(releases.length).toBe(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.0"); + describeDependentBumping("modifier+version", { + // render workspace:1.0.0, workspace:^1.0.0, workspace:~1.0.0 + renderRange: (range) => `workspace:${range !== "=" ? range : ""}1.0.0`, + }); }); - it("should not bump dependent when bumping caret peerDep by patch", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); - - expect(releases.length).toBe(1); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.0.1"); - }); - it("should major bump dependent when bumping caret peerDep by minor", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); + it("should assemble plan when dependent has both a changed prod and dev dependency", () => { + const setup = new FakeFullState({ changesets: [] }); + setup.addPackage("pkg-b", "1.0.0"); + setup.addPackage("pkg-c", "1.0.0"); + setup.updateDevDependency("pkg-b", "pkg-a", "^1.0.0"); + setup.updateDependency("pkg-b", "pkg-c", "^1.0.0"); setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "minor" }], + id: "big-cats-delight", + releases: [ + { name: "pkg-a", type: "major" }, + { name: "pkg-c", type: "major" }, + ], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, defaultConfig, - undefined + undefined, ); - expect(releases.length).toBe(2); + expect(releases.length).toEqual(3); expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); - expect(releases[1].name).toEqual("pkg-b"); + expect(releases[0].newVersion).toEqual("2.0.0"); + expect(releases[1].name).toEqual("pkg-c"); expect(releases[1].newVersion).toEqual("2.0.0"); + expect(releases[2].name).toEqual("pkg-b"); + expect(releases[2].oldVersion).toEqual("1.0.0"); + expect(releases[2].newVersion).toEqual("1.0.1"); }); - it("should major bump dependent when bumping caret peerDep by major", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "major" }], - }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + describe("changetype none", () => { + it("should assemble no-op plan when dependent has a changeset type of none", () => { + const setup = new FakeFullState({ changesets: [] }); + setup.addPackage("pkg-b", "1.0.0"); + setup.addPackage("pkg-c", "1.0.0"); + setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-b", type: "none" }], + }); - expect(releases.length).toBe(2); - expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("2.0.0"); - expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("2.0.0"); - }); - it("should patch bump transitive dep that is only affected by peerDep bump", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); - setup.addPackage("pkg-c", "1.0.0"); - setup.updateDependency("pkg-c", "pkg-b", "^1.0.0"); - setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "minor" }], + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ); + + expect(releases.length).toEqual(1); + expect(releases[0].name).toEqual("pkg-b"); + expect(releases[0].oldVersion).toEqual("1.0.0"); + expect(releases[0].newVersion).toEqual("1.0.0"); }); - let { releases } = assembleReleasePlan( - setup.changesets, - setup.packages, - defaultConfig, - undefined - ); + // (~ peer | none) => none + it("should not bump dependent when bumping peer:~ by none", () => { + const setup = new FakeFullState({ changesets: [] }); + setup.addPackage("pkg-b", "1.0.0"); + setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); + setup.addChangeset({ + id: "anyway-the-windblows", + releases: [{ name: "pkg-a", type: "none" }], + }); - expect(releases.length).toBe(3); - expect(releases[0]).toMatchObject({ - name: "pkg-a", - newVersion: "1.1.0", - }); - expect(releases[1]).toMatchObject({ - name: "pkg-b", - newVersion: "2.0.0", - }); - expect(releases[2]).toMatchObject({ - name: "pkg-c", - newVersion: "1.0.1", + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ); + + expect(releases.length).toBe(1); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[0].newVersion).toEqual("1.0.0"); }); - }); - describe("onlyUpdatePeerDependentsWhenOutOfRange: true", () => { - it("should not bump dependent when still in range", () => { + // (^ peer none) => none + it("should not bump dependent when bumping peer:^ by none", () => { + const setup = new FakeFullState({ changesets: [] }); + setup.addPackage("pkg-b", "1.0.0"); setup.updatePeerDependency("pkg-b", "pkg-a", "^1.0.0"); setup.addChangeset({ id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "minor" }], + releases: [{ name: "pkg-a", type: "none" }], }); - let { releases } = assembleReleasePlan( + + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, - { - ...defaultConfig, - ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { - ...defaultConfig.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, - onlyUpdatePeerDependentsWhenOutOfRange: true, - }, - }, - undefined + defaultConfig, + undefined, ); + expect(releases.length).toBe(1); expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); + expect(releases[0].newVersion).toEqual("1.0.0"); }); - it("should major bump dependent when leaving range", () => { - setup.updatePeerDependency("pkg-b", "pkg-a", "~1.0.0"); + // (workspace:^ direct none) => same + it("should not bump dependent when bumping dep:workspace:^ by none", () => { + const setup = new FakeFullState(); + setup.addPackage("pkg-b", "1.0.0"); + setup.addPackage("pkg-c", "1.0.0"); + setup.updateDependency("pkg-c", "pkg-b", "workspace:*"); setup.addChangeset({ - id: "anyway-the-windblows", - releases: [{ name: "pkg-a", type: "minor" }], + id: "big-cats-delight", + releases: [{ name: "pkg-b", type: "none" }], }); - let { releases } = assembleReleasePlan( + const { releases } = assembleReleasePlan( setup.changesets, setup.packages, - { - ...defaultConfig, - ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { - ...defaultConfig.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH, - onlyUpdatePeerDependentsWhenOutOfRange: true, - }, - }, - undefined + defaultConfig, + undefined, ); - expect(releases.length).toBe(2); + expect(releases.length).toEqual(2); expect(releases[0].name).toEqual("pkg-a"); - expect(releases[0].newVersion).toEqual("1.1.0"); expect(releases[1].name).toEqual("pkg-b"); - expect(releases[1].newVersion).toEqual("2.0.0"); + expect(releases[1].oldVersion).toEqual("1.0.0"); + expect(releases[1].newVersion).toEqual("1.0.0"); + }); + + // (workspace:path direct none) => same + it("should not bump dependent when bumping dep:workspace:path by none", () => { + const setup = new FakeFullState(); + setup.addPackage("pkg-b", "1.0.0"); + setup.addPackage("pkg-c", "1.0.0"); + setup.updateDependency("pkg-c", "pkg-b", "workspace:packages/pkg-b"); + setup.addChangeset({ + id: "big-cats-delight", + releases: [{ name: "pkg-b", type: "none" }], + }); + + const { releases } = assembleReleasePlan( + setup.changesets, + setup.packages, + defaultConfig, + undefined, + ); + + expect(releases.length).toEqual(2); + expect(releases[0].name).toEqual("pkg-a"); + expect(releases[1].name).toEqual("pkg-b"); + expect(releases[1].oldVersion).toEqual("1.0.0"); + expect(releases[1].newVersion).toEqual("1.0.0"); }); }); }); - -/* - Bumping peerDeps is a tricky issue, so we are testing every single combination here so that - we can have absolute certainty when changing anything to do with them. - In general the rule for bumping peerDeps is that: - * All MINOR or MAJOR peerDep bumps must MAJOR bump all dependents - regardless of ranges - * Otherwise - normal patching rules apply - */ diff --git a/packages/assemble-release-plan/src/index.ts b/packages/assemble-release-plan/src/index.ts index c8038f0b2..8a7774041 100644 --- a/packages/assemble-release-plan/src/index.ts +++ b/packages/assemble-release-plan/src/index.ts @@ -1,21 +1,23 @@ import { InternalError } from "@changesets/errors"; import { getDependentsGraph } from "@changesets/get-dependents-graph"; import { shouldSkipPackage } from "@changesets/should-skip-package"; -import { +import type { Config, NewChangeset, PackageGroup, PreState, ReleasePlan, + Package, + Packages, } from "@changesets/types"; -import { Package, Packages } from "@manypkg/get-packages"; -import semverParse from "semver/functions/parse"; -import applyLinks from "./apply-links"; -import determineDependents from "./determine-dependents"; -import flattenReleases from "./flatten-releases"; -import { incrementVersion } from "./increment"; -import matchFixedConstraint from "./match-fixed-constraint"; -import { InternalRelease, PreInfo } from "./types"; +import semverParse from "semver/functions/parse.js"; +import { applyLinks } from "./apply-links.ts"; +import { determineDependents } from "./determine-dependents.ts"; +import { flattenReleases } from "./flatten-releases.ts"; +import { incrementVersion } from "./increment.ts"; +import { matchFixedConstraint } from "./match-fixed-constraint.ts"; +import type { InternalRelease, PreInfo } from "./types.ts"; +import { mapGetOrThrow, mapGetOrThrowInternal } from "./utils.ts"; type SnapshotReleaseParameters = { tag?: string | undefined; @@ -23,9 +25,8 @@ type SnapshotReleaseParameters = { }; function getPreVersion(version: string) { - let parsed = semverParse(version)!; - let preVersion = - parsed.prerelease[1] === undefined ? -1 : parsed.prerelease[1]; + const parsed = semverParse(version)!; + let preVersion = parsed.prerelease[1] ?? -1; if (typeof preVersion !== "number") { throw new InternalError("preVersion is not a number"); } @@ -35,12 +36,13 @@ function getPreVersion(version: string) { function getSnapshotSuffix( template: Config["snapshot"]["prereleaseTemplate"], - snapshotParameters: SnapshotReleaseParameters + snapshotParameters: SnapshotReleaseParameters, ): string { - let snapshotRefDate = new Date(); + const snapshotRefDate = new Date(); const placeholderValues = { commit: snapshotParameters.commit, + "commit-short": snapshotParameters.commit?.slice(0, 7), tag: snapshotParameters.tag, timestamp: snapshotRefDate.getTime().toString(), datetime: snapshotRefDate @@ -61,18 +63,18 @@ function getSnapshotSuffix( keyof typeof placeholderValues >; - if (!template.includes(`{tag}`) && placeholderValues.tag !== undefined) { + if (!template.includes(`{tag}`) && placeholderValues.tag != null) { throw new Error( - `Failed to compose snapshot version: "{tag}" placeholder is missing, but the snapshot parameter is defined (value: '${placeholderValues.tag}')` + `Failed to compose snapshot version: "{tag}" placeholder is missing, but the snapshot parameter is defined (value: '${placeholderValues.tag}')`, ); } return placeholders.reduce((prev, key) => { return prev.replace(new RegExp(`\\{${key}\\}`, "g"), () => { const value = placeholderValues[key]; - if (value === undefined) { + if (value == null) { throw new Error( - `Failed to compose snapshot version: "{${key}}" placeholder is used without having a value defined!` + `Failed to compose snapshot version: "{${key}}" placeholder is used without having a value defined!`, ); } @@ -85,7 +87,7 @@ function getSnapshotVersion( release: InternalRelease, preInfo: PreInfo | undefined, useCalculatedVersion: boolean, - snapshotSuffix: string + snapshotSuffix: string, ): string { if (release.type === "none") { return release.oldVersion; @@ -109,7 +111,7 @@ function getSnapshotVersion( function getNewVersion( release: InternalRelease, - preInfo: PreInfo | undefined + preInfo: PreInfo | undefined, ): string { if (release.type === "none") { return release.oldVersion; @@ -118,100 +120,70 @@ function getNewVersion( return incrementVersion(release, preInfo); } -type OptionalProp = Omit & Partial>; - -function assembleReleasePlan( +export function assembleReleasePlan( changesets: NewChangeset[], packages: Packages, - config: OptionalProp, + config: Config, // intentionally not using an optional parameter here so the result of `readPreState` has to be passed in here preState: PreState | undefined, - // snapshot: undefined -> not using snaphot + // snapshot: undefined -> not using snapshot // snapshot: { tag: undefined } -> --snapshot (empty tag) // snapshot: { tag: "canary" } -> --snapshot canary - snapshot?: SnapshotReleaseParameters | string | boolean + snapshot?: SnapshotReleaseParameters, ): ReleasePlan { - // TODO: remove `refined*` in the next major version of this package - // just use `config` and `snapshot` parameters directly, typed as: `config: Config, snapshot?: SnapshotReleaseParameters` - const refinedConfig: Config = config.snapshot - ? (config as Config) - : { - ...config, - snapshot: { - prereleaseTemplate: null, - useCalculatedVersion: ( - config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH as any - ).useCalculatedVersionForSnapshots, - }, - }; - const refinedSnapshot: SnapshotReleaseParameters | undefined = - typeof snapshot === "string" - ? { tag: snapshot } - : typeof snapshot === "boolean" - ? { tag: undefined } - : snapshot; - - let packagesByName = new Map( - packages.packages.map((x) => [x.packageJson.name, x]) + const packagesByName = new Map( + packages.packages.map((x) => [x.packageJson.name, x]), ); const relevantChangesets = getRelevantChangesets( changesets, packagesByName, - refinedConfig, - preState + config, + preState, ); - const preInfo = getPreInfo( - changesets, - packagesByName, - refinedConfig, - preState - ); + const preInfo = getPreInfo(changesets, packagesByName, config, preState); // releases is, at this point a list of all packages we are going to releases, // flattened down to one release per package, having a reference back to their // changesets, and with a calculated new versions - let releases = flattenReleases( - relevantChangesets, - packagesByName, - refinedConfig - ); + const releases = flattenReleases(relevantChangesets, packagesByName, config); - let dependencyGraph = getDependentsGraph(packages, { + // Unlike the config/CLI validation graphs, this graph intentionally includes + // devDependencies. While devDeps don't cause version bumps (determineDependents + // assigns type "none"), they must appear in the release plan so that + // apply-release-plan can update their version ranges in package.json. + const dependencyGraph = getDependentsGraph(packages, { bumpVersionsWithWorkspaceProtocolOnly: - refinedConfig.bumpVersionsWithWorkspaceProtocolOnly, + config.bumpVersionsWithWorkspaceProtocolOnly, }); let releasesValidated = false; while (releasesValidated === false) { // The map passed in to determineDependents will be mutated - let dependentAdded = determineDependents({ + const dependentAdded = determineDependents({ releases, packagesByName, + rootDir: packages.rootDir, dependencyGraph, preInfo, - config: refinedConfig, + config, }); // `releases` might get mutated here - let fixedConstraintUpdated = matchFixedConstraint( + const fixedConstraintUpdated = matchFixedConstraint( releases, packagesByName, - refinedConfig - ); - let linksUpdated = applyLinks( - releases, - packagesByName, - refinedConfig.linked + config, ); + const linksUpdated = applyLinks(releases, packagesByName, config.linked); releasesValidated = !linksUpdated && !dependentAdded && !fixedConstraintUpdated; } if (preInfo?.state.mode === "exit") { - for (let pkg of packages.packages) { + for (const pkg of packages.packages) { // If a package had a prerelease, but didn't trigger a version bump in the regular release, // we want to give it a patch release. // Detailed explanation at https://github.com/changesets/changesets/pull/382#discussion_r434434182 @@ -227,8 +199,8 @@ function assembleReleasePlan( } else if ( existingRelease.type === "none" && !shouldSkipPackage(pkg, { - ignore: refinedConfig.ignore, - allowPrivatePackages: refinedConfig.privatePackages.version, + ignore: config.ignore, + allowPrivatePackages: config.privatePackages.version, }) ) { existingRelease.type = "patch"; @@ -239,23 +211,19 @@ function assembleReleasePlan( // Caching the snapshot version here and use this if it is snapshot release const snapshotSuffix = - refinedSnapshot && - getSnapshotSuffix( - refinedConfig.snapshot.prereleaseTemplate, - refinedSnapshot - ); + snapshot && getSnapshotSuffix(config.snapshot.prereleaseTemplate, snapshot); return { changesets: relevantChangesets, - releases: [...releases.values()].map((incompleteRelease) => { + releases: Array.from(releases.values(), (incompleteRelease) => { return { ...incompleteRelease, newVersion: snapshotSuffix ? getSnapshotVersion( incompleteRelease, preInfo, - refinedConfig.snapshot.useCalculatedVersion, - snapshotSuffix + config.snapshot.useCalculatedVersion, + snapshotSuffix, ) : getNewVersion(incompleteRelease, preInfo), }; @@ -268,7 +236,7 @@ function getRelevantChangesets( changesets: NewChangeset[], packagesByName: Map, config: Config, - preState: PreState | undefined + preState: PreState | undefined, ): NewChangeset[] { for (const changeset of changesets) { // Using the following 2 arrays to decide whether a changeset @@ -276,13 +244,12 @@ function getRelevantChangesets( const skippedPackages = []; const notSkippedPackages = []; for (const release of changeset.releases) { - const packageByName = packagesByName.get(release.name); - - if (!packageByName) { - throw new Error( - `Found changeset ${changeset.id} for package ${release.name} which is not in the workspace` - ); - } + // this acts as an early validation in this package so we don't throw an internal error here + const packageByName = mapGetOrThrow( + packagesByName, + release.name, + `Found changeset ${changeset.id} for package ${release.name} which is not in the workspace`, + ); if ( shouldSkipPackage(packageByName, { @@ -301,15 +268,15 @@ function getRelevantChangesets( `Found mixed changeset ${changeset.id}\n` + `Found ignored packages: ${skippedPackages.join(" ")}\n` + `Found not ignored packages: ${notSkippedPackages.join(" ")}\n` + - "Mixed changesets that contain both ignored and not ignored packages are not allowed" + "Mixed changesets that contain both ignored and not ignored packages are not allowed", ); } } if (preState && preState.mode !== "exit") { - let usedChangesetIds = new Set(preState.changesets); + const usedChangesetIds = new Set(preState.changesets); return changesets.filter( - (changeset) => !usedChangesetIds.has(changeset.id) + (changeset) => !usedChangesetIds.has(changeset.id), ); } @@ -317,14 +284,22 @@ function getRelevantChangesets( } function getHighestPreVersion( + groupKind: "linked" | "fixed", packageGroup: PackageGroup, - packagesByName: Map + packagesByName: Map, ): number { let highestPreVersion = 0; - for (let pkg of packageGroup) { + for (const pkgName of packageGroup) { + const pkg = mapGetOrThrowInternal( + packagesByName, + pkgName, + `Could not find package named "${pkgName}" listed in ${groupKind} group ${JSON.stringify( + packageGroup, + )}`, + ); highestPreVersion = Math.max( - getPreVersion(packagesByName.get(pkg)!.packageJson.version), - highestPreVersion + getPreVersion(pkg.packageJson.version), + highestPreVersion, ); } return highestPreVersion; @@ -334,13 +309,13 @@ function getPreInfo( changesets: NewChangeset[], packagesByName: Map, config: Config, - preState: PreState | undefined + preState: PreState | undefined, ): PreInfo | undefined { - if (preState === undefined) { + if (preState == null) { return; } - let updatedPreState = { + const updatedPreState = { ...preState, changesets: changesets.map((changeset) => changeset.id), initialVersions: { @@ -349,14 +324,12 @@ function getPreInfo( }; for (const [, pkg] of packagesByName) { - if (updatedPreState.initialVersions[pkg.packageJson.name] === undefined) { - updatedPreState.initialVersions[pkg.packageJson.name] = - pkg.packageJson.version; - } + updatedPreState.initialVersions[pkg.packageJson.name] ??= + pkg.packageJson.version; } // Populate preVersion // preVersion is the map between package name and its next pre version number. - let preVersions = new Map(); + const preVersions = new Map(); for (const [, pkg] of packagesByName) { if ( shouldSkipPackage(pkg, { @@ -368,18 +341,26 @@ function getPreInfo( } preVersions.set( pkg.packageJson.name, - getPreVersion(pkg.packageJson.version) + getPreVersion(pkg.packageJson.version), ); } - for (let fixedGroup of config.fixed) { - let highestPreVersion = getHighestPreVersion(fixedGroup, packagesByName); - for (let fixedPackage of fixedGroup) { + for (const fixedGroup of config.fixed) { + const highestPreVersion = getHighestPreVersion( + "fixed", + fixedGroup, + packagesByName, + ); + for (const fixedPackage of fixedGroup) { preVersions.set(fixedPackage, highestPreVersion); } } - for (let linkedGroup of config.linked) { - let highestPreVersion = getHighestPreVersion(linkedGroup, packagesByName); - for (let linkedPackage of linkedGroup) { + for (const linkedGroup of config.linked) { + const highestPreVersion = getHighestPreVersion( + "linked", + linkedGroup, + packagesByName, + ); + for (const linkedPackage of linkedGroup) { preVersions.set(linkedPackage, highestPreVersion); } } @@ -390,4 +371,6 @@ function getPreInfo( }; } -export default assembleReleasePlan; +/** @deprecated Use named export `assembleReleasePlan` instead */ +const assembleReleasePlanDefault = assembleReleasePlan; +export default assembleReleasePlanDefault; diff --git a/packages/assemble-release-plan/src/match-fixed-constraint.ts b/packages/assemble-release-plan/src/match-fixed-constraint.ts index 963d609f5..8a0b1d247 100644 --- a/packages/assemble-release-plan/src/match-fixed-constraint.ts +++ b/packages/assemble-release-plan/src/match-fixed-constraint.ts @@ -1,41 +1,51 @@ import { shouldSkipPackage } from "@changesets/should-skip-package"; -import { Config } from "@changesets/types"; -import { Package } from "@manypkg/get-packages"; -import { InternalRelease } from "./types"; -import { getCurrentHighestVersion, getHighestReleaseType } from "./utils"; +import type { Config, Package } from "@changesets/types"; +import type { InternalRelease } from "./types.ts"; +import { + getCurrentHighestVersion, + getHighestReleaseType, + mapGetOrThrowInternal, +} from "./utils.ts"; -export default function matchFixedConstraint( +export function matchFixedConstraint( releases: Map, packagesByName: Map, - config: Config + config: Config, ): boolean { let updated = false; - for (let fixedPackages of config.fixed) { - let releasingFixedPackages = [...releases.values()].filter( + for (const fixedPackages of config.fixed) { + const releasingFixedPackages = [...releases.values()].filter( (release) => - fixedPackages.includes(release.name) && release.type !== "none" + fixedPackages.includes(release.name) && release.type !== "none", ); if (releasingFixedPackages.length === 0) continue; - let highestReleaseType = getHighestReleaseType(releasingFixedPackages); - let highestVersion = getCurrentHighestVersion( + const highestReleaseType = getHighestReleaseType(releasingFixedPackages); + const highestVersion = getCurrentHighestVersion( fixedPackages, - packagesByName + packagesByName, ); // Finally, we update the packages so all of them are on the highest version - for (let pkgName of fixedPackages) { + for (const pkgName of fixedPackages) { + const pkg = mapGetOrThrowInternal( + packagesByName, + pkgName, + `Could not find package named "${pkgName}" listed in fixed group ${JSON.stringify( + fixedPackages, + )}`, + ); if ( - shouldSkipPackage(packagesByName.get(pkgName)!, { + shouldSkipPackage(pkg, { ignore: config.ignore, allowPrivatePackages: config.privatePackages.version, }) ) { continue; } - let release = releases.get(pkgName); + const release = releases.get(pkgName); if (!release) { updated = true; diff --git a/packages/assemble-release-plan/src/test-utils.ts b/packages/assemble-release-plan/src/test-utils.ts index 2bd16710e..1732f18f8 100644 --- a/packages/assemble-release-plan/src/test-utils.ts +++ b/packages/assemble-release-plan/src/test-utils.ts @@ -1,5 +1,10 @@ -import { NewChangeset, Release, VersionType } from "@changesets/types"; -import { Package, Packages } from "@manypkg/get-packages"; +import type { + NewChangeset, + Release, + VersionType, + Packages, + Package, +} from "@changesets/types"; function getPackage({ name, @@ -13,7 +18,7 @@ function getPackage({ name, version, }, - dir: "this-shouldn't-matter", + dir: `/packages/${name.replace(/^@/, "").replace(/\//g, "-")}`, }; } @@ -22,11 +27,11 @@ function getChangeset( id?: string; summary?: string; releases?: Array; - } = {} + } = {}, ): NewChangeset { - let id = data.id || "strange-words-combine"; - let summary = data.summary || "base summary whatever"; - let releases = data.releases || []; + const id = data.id || "strange-words-combine"; + const summary = data.summary || "base summary whatever"; + const releases = data.releases || []; return { id, summary, @@ -44,29 +49,30 @@ function getRelease({ return { name, type }; } -let getSimpleSetup = () => ({ +const getSimpleSetup = () => ({ packages: { - root: { + rootPackage: { packageJson: { name: "root", version: "0.0.0", }, dir: "/", }, + rootDir: "/", packages: [getPackage({ name: "pkg-a", version: "1.0.0" })], - tool: "yarn" as const, - }, + tool: { type: "yarn" }, + } satisfies Packages, changesets: [ getChangeset({ releases: [getRelease({ name: "pkg-a", type: "patch" })] }), - ], + ] satisfies Array, }); -class FakeFullState { +export class FakeFullState { packages: Packages; changesets: NewChangeset[]; constructor(custom?: { packages?: Packages; changesets?: NewChangeset[] }) { - let { packages, changesets } = { ...getSimpleSetup(), ...custom }; + const { packages, changesets } = { ...getSimpleSetup(), ...custom }; this.packages = packages; this.changesets = changesets; } @@ -76,64 +82,80 @@ class FakeFullState { id?: string; summary?: string; releases?: Array; - } = {} + } = {}, ) { - let changeset = getChangeset(data); - if (this.changesets.find((c) => c.id === changeset.id)) { + const changeset = getChangeset(data); + if (this.changesets.some((c) => c.id === changeset.id)) { throw new Error( - `tried to add a second changeset with same id: ${changeset.id}` + `tried to add a second changeset with same id: ${changeset.id}`, ); } this.changesets.push(changeset); } - updateDependency(pkgA: string, pkgB: string, versionRange: string) { - let pkg = this.packages.packages.find((a) => a.packageJson.name === pkgA); - if (!pkg) throw new Error(`No "${pkgA}" package`); + updateDependency( + dependent: string, + dependency: string, + versionRange: string, + ) { + const pkg = this.packages.packages.find( + (a) => a.packageJson.name === dependent, + ); + if (!pkg) throw new Error(`No "${dependent}" package`); if (!pkg.packageJson.dependencies) { pkg.packageJson.dependencies = {}; } - pkg.packageJson.dependencies[pkgB] = versionRange; + pkg.packageJson.dependencies[dependency] = versionRange; } - updateDevDependency(pkgA: string, pkgB: string, versionRange: string) { - let pkg = this.packages.packages.find((a) => a.packageJson.name === pkgA); - if (!pkg) throw new Error(`No "${pkgA}" package`); + updateDevDependency( + dependent: string, + dependency: string, + versionRange: string, + ) { + const pkg = this.packages.packages.find( + (a) => a.packageJson.name === dependent, + ); + if (!pkg) throw new Error(`No "${dependent}" package`); if (!pkg.packageJson.devDependencies) { pkg.packageJson.devDependencies = {}; } - pkg.packageJson.devDependencies[pkgB] = versionRange; + pkg.packageJson.devDependencies[dependency] = versionRange; } - updatePeerDependency(pkgA: string, pkgB: string, versionRange: string) { - let pkg = this.packages.packages.find((a) => a.packageJson.name === pkgA); - if (!pkg) throw new Error(`No "${pkgA}" package`); + updatePeerDependency( + dependent: string, + dependency: string, + versionRange: string, + ) { + const pkg = this.packages.packages.find( + (a) => a.packageJson.name === dependent, + ); + if (!pkg) throw new Error(`No "${dependent}" package`); if (!pkg.packageJson.peerDependencies) { pkg.packageJson.peerDependencies = {}; } - pkg.packageJson.peerDependencies[pkgB] = versionRange; + pkg.packageJson.peerDependencies[dependency] = versionRange; } addPackage(name: string, version: string) { - let pkg = getPackage({ name, version }); + const pkg = getPackage({ name, version }); if ( - this.packages.packages.find( - (c) => c.packageJson.name === pkg.packageJson.name + this.packages.packages.some( + (c) => c.packageJson.name === pkg.packageJson.name, ) ) { throw new Error( - `tried to add a second package with same name': ${pkg.packageJson.name}` + `tried to add a second package with same name': ${pkg.packageJson.name}`, ); } this.packages.packages.push(pkg); } updatePackage(name: string, version: string) { - let pkg = this.packages.packages.find((c) => c.packageJson.name === name); + const pkg = this.packages.packages.find((c) => c.packageJson.name === name); if (!pkg) { throw new Error( - `could not update package ${name} because it doesn't exist - try addWorskpace` + `could not update package ${name} because it doesn't exist - try addWorskpace`, ); } pkg.packageJson.version = version; } } - -export default FakeFullState; diff --git a/packages/assemble-release-plan/src/types.ts b/packages/assemble-release-plan/src/types.ts index 83826c2af..deae5194e 100644 --- a/packages/assemble-release-plan/src/types.ts +++ b/packages/assemble-release-plan/src/types.ts @@ -1,4 +1,4 @@ -import { VersionType, PreState } from "@changesets/types"; +import type { VersionType, PreState } from "@changesets/types"; export type InternalRelease = { name: string; diff --git a/packages/assemble-release-plan/src/utils.ts b/packages/assemble-release-plan/src/utils.ts index 8784f8606..88f37855e 100644 --- a/packages/assemble-release-plan/src/utils.ts +++ b/packages/assemble-release-plan/src/utils.ts @@ -1,20 +1,20 @@ -import { PackageGroup, VersionType } from "@changesets/types"; -import { Package } from "@manypkg/get-packages"; -import semverGt from "semver/functions/gt"; -import { InternalRelease } from "./types"; +import { InternalError } from "@changesets/errors"; +import type { PackageGroup, VersionType, Package } from "@changesets/types"; +import semverGt from "semver/functions/gt.js"; +import type { InternalRelease } from "./types.ts"; export function getHighestReleaseType( - releases: InternalRelease[] + releases: InternalRelease[], ): VersionType { if (releases.length === 0) { throw new Error( - `Large internal Changesets error when calculating highest release type in the set of releases. Please contact the maintainers` + `Large internal Changesets error when calculating highest release type in the set of releases. Please contact the maintainers`, ); } let highestReleaseType: VersionType = "none"; - for (let release of releases) { + for (const release of releases) { switch (release.type) { case "major": return "major"; @@ -34,22 +34,19 @@ export function getHighestReleaseType( export function getCurrentHighestVersion( packageGroup: PackageGroup, - packagesByName: Map + packagesByName: Map, ): string { let highestVersion: string | undefined; - for (let pkgName of packageGroup) { - let pkg = packagesByName.get(pkgName); - - if (!pkg) { - console.error( - `FATAL ERROR IN CHANGESETS! We were unable to version for package group: ${pkgName} in package group: ${packageGroup.toString()}` - ); - throw new Error(`fatal: could not resolve linked packages`); - } + for (const pkgName of packageGroup) { + const pkg = mapGetOrThrowInternal( + packagesByName, + pkgName, + `We were unable to version for package group: ${pkgName} in package group: ${packageGroup.toString()}`, + ); if ( - highestVersion === undefined || + highestVersion == null || semverGt(pkg.packageJson.version, highestVersion) ) { highestVersion = pkg.packageJson.version; @@ -58,3 +55,27 @@ export function getCurrentHighestVersion( return highestVersion!; } + +export function mapGetOrThrow( + map: Map, + key: string, + errorMessage: string, +): V { + const value = map.get(key); + if (value == null) { + throw new Error(errorMessage); + } + return value; +} + +export function mapGetOrThrowInternal( + map: Map, + key: string, + errorMessage: string, +): V { + const value = map.get(key); + if (value == null) { + throw new InternalError(errorMessage); + } + return value; +} diff --git a/packages/assemble-release-plan/tsdown.config.ts b/packages/assemble-release-plan/tsdown.config.ts new file mode 100644 index 000000000..8eddd7f1b --- /dev/null +++ b/packages/assemble-release-plan/tsdown.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsdown/config"; +import { baseConfig } from "../../tsdown.config.ts"; + +export default defineConfig(baseConfig); diff --git a/packages/changelog-git/CHANGELOG.md b/packages/changelog-git/CHANGELOG.md index 2d381bf91..fc41121b1 100644 --- a/packages/changelog-git/CHANGELOG.md +++ b/packages/changelog-git/CHANGELOG.md @@ -1,5 +1,71 @@ # @changesets/changelog-git +## 1.0.0-next.5 + +### Patch Changes + +- Updated dependencies [[`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f)]: + - @changesets/types@7.0.0-next.5 + +## 1.0.0-next.4 + +### Patch Changes + +- Updated dependencies [[`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8)]: + - @changesets/types@7.0.0-next.4 + +## 1.0.0-next.3 + +### Major Changes + +- [#1954](https://github.com/changesets/changesets/pull/1954) [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped supported Node versions to `^22.11 || ^24 || >=26` + +- [#1961](https://github.com/changesets/changesets/pull/1961) [`07278a7`](https://github.com/changesets/changesets/commit/07278a726343388eb6dfc56e7a8213872d4c8857) Thanks [@beeequeue](https://github.com/beeequeue)! - `ChangelogFunctions` can now be both sync and async, and the `defaultChangelogFunctions` are now sync. + +### Minor Changes + +- [#1969](https://github.com/changesets/changesets/pull/1969) [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Add a named export that mirrors the current `default` export + + The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. + +### Patch Changes + +- Updated dependencies [[`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069), [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7)]: + - @changesets/types@7.0.0-next.3 + +## 1.0.0-next.2 + +### Patch Changes + +- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c)]: + - @changesets/types@7.0.0-next.2 + +## 1.0.0-next.1 + +### Minor Changes + +- [#1656](https://github.com/changesets/changesets/pull/1656) [`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d) Thanks [@bluwy](https://github.com/bluwy)! - Bumps minimum node version to `>=20.0.0` + +### Patch Changes + +- Updated dependencies [[`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d)]: + - @changesets/types@7.0.0-next.1 + +## 1.0.0-next.0 + +### Major Changes + +- [#1482](https://github.com/changesets/changesets/pull/1482) [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7) Thanks [@Andarist](https://github.com/Andarist)! - From now on this package is going to be published as ES module. + +### Minor Changes + +- [#1479](https://github.com/changesets/changesets/pull/1479) [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5) Thanks [@bluwy](https://github.com/bluwy)! - Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. + +### Patch Changes + +- Updated dependencies [[`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5), [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7)]: + - @changesets/types@7.0.0-next.0 + ## 0.2.1 ### Patch Changes diff --git a/packages/changelog-git/package.json b/packages/changelog-git/package.json index 12f372dfc..f1225bce8 100644 --- a/packages/changelog-git/package.json +++ b/packages/changelog-git/package.json @@ -1,24 +1,22 @@ { "name": "@changesets/changelog-git", - "version": "0.2.1", + "version": "1.0.0-next.5", "description": "A changelog entry generator for git that writes hashes", - "main": "dist/changesets-changelog-git.cjs.js", - "module": "dist/changesets-changelog-git.esm.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/changesets/changesets.git", + "directory": "packages/changelog-git" + }, + "type": "module", "exports": { - ".": { - "types": { - "import": "./dist/changesets-changelog-git.cjs.mjs", - "default": "./dist/changesets-changelog-git.cjs.js" - }, - "module": "./dist/changesets-changelog-git.esm.js", - "import": "./dist/changesets-changelog-git.cjs.mjs", - "default": "./dist/changesets-changelog-git.cjs.js" - }, + ".": "./dist/index.mjs", "./package.json": "./package.json" }, - "license": "MIT", - "repository": "https://github.com/changesets/changesets/tree/main/packages/changelog-git", "dependencies": { - "@changesets/types": "^6.1.0" + "@changesets/types": "workspace:^" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } } diff --git a/packages/changelog-git/src/index.ts b/packages/changelog-git/src/index.ts index f09d521c4..f3a50ef53 100644 --- a/packages/changelog-git/src/index.ts +++ b/packages/changelog-git/src/index.ts @@ -1,52 +1,38 @@ -import { - NewChangesetWithCommit, - VersionType, - ChangelogFunctions, - ModCompWithPackage, -} from "@changesets/types"; - -const getReleaseLine = async ( - changeset: NewChangesetWithCommit, - _type: VersionType -) => { - const [firstLine, ...futureLines] = changeset.summary - .split("\n") - .map((l) => l.trimRight()); - - let returnVal = `- ${ - changeset.commit ? `${changeset.commit.slice(0, 7)}: ` : "" - }${firstLine}`; - - if (futureLines.length > 0) { - returnVal += `\n${futureLines.map((l) => ` ${l}`).join("\n")}`; - } - - return returnVal; +import type { ChangelogFunctions } from "@changesets/types"; + +const changelogFunctions: ChangelogFunctions = { + getReleaseLine: (changeset) => { + const [firstLine, ...futureLines] = changeset.summary + .split("\n") + .map((l) => l.trimEnd()); + + let returnVal = `- ${ + changeset.commit ? `${changeset.commit.slice(0, 7)}: ` : "" + }${firstLine}`; + + if (futureLines.length > 0) { + returnVal += `\n${futureLines.map((l) => ` ${l}`).join("\n")}`; + } + + return returnVal; + }, + getDependencyReleaseLine: (changesets, dependenciesUpdated) => { + if (dependenciesUpdated.length === 0) return ""; + + const changesetLinks = changesets.map( + (changeset) => + `- Updated dependencies${ + changeset.commit ? ` [${changeset.commit.slice(0, 7)}]` : "" + }`, + ); + + const updatedDependenciesList = dependenciesUpdated.map( + (dependency) => ` - ${dependency.name}@${dependency.newVersion}`, + ); + + return [...changesetLinks, ...updatedDependenciesList].join("\n"); + }, }; -const getDependencyReleaseLine = async ( - changesets: NewChangesetWithCommit[], - dependenciesUpdated: ModCompWithPackage[] -) => { - if (dependenciesUpdated.length === 0) return ""; - - const changesetLinks = changesets.map( - (changeset) => - `- Updated dependencies${ - changeset.commit ? ` [${changeset.commit.slice(0, 7)}]` : "" - }` - ); - - const updatedDependenciesList = dependenciesUpdated.map( - (dependency) => ` - ${dependency.name}@${dependency.newVersion}` - ); - - return [...changesetLinks, ...updatedDependenciesList].join("\n"); -}; - -const defaultChangelogFunctions: ChangelogFunctions = { - getReleaseLine, - getDependencyReleaseLine, -}; - -export default defaultChangelogFunctions; +// ChangelogFunctions require a default export +export default changelogFunctions; diff --git a/packages/changelog-git/tsdown.config.ts b/packages/changelog-git/tsdown.config.ts new file mode 100644 index 000000000..8eddd7f1b --- /dev/null +++ b/packages/changelog-git/tsdown.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsdown/config"; +import { baseConfig } from "../../tsdown.config.ts"; + +export default defineConfig(baseConfig); diff --git a/packages/changelog-github/CHANGELOG.md b/packages/changelog-github/CHANGELOG.md index a71bc0ba1..8e0c90151 100644 --- a/packages/changelog-github/CHANGELOG.md +++ b/packages/changelog-github/CHANGELOG.md @@ -1,5 +1,106 @@ # @changesets/changelog-github +## 1.0.0-next.5 + +### Patch Changes + +- [#2040](https://github.com/changesets/changesets/pull/2040) [`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f) Thanks [@bluwy](https://github.com/bluwy)! - Improve type-check for options object + +- Updated dependencies [[`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f)]: + - @changesets/types@7.0.0-next.5 + +## 1.0.0-next.4 + +### Patch Changes + +- Updated dependencies [[`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8)]: + - @changesets/types@7.0.0-next.4 + +## 1.0.0-next.3 + +### Major Changes + +- [#1954](https://github.com/changesets/changesets/pull/1954) [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped supported Node versions to `^22.11 || ^24 || >=26` + +### Minor Changes + +- [#1969](https://github.com/changesets/changesets/pull/1969) [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Add a named export that mirrors the current `default` export + + The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. + +### Patch Changes + +- [#1947](https://github.com/changesets/changesets/pull/1947) [`492b0ca`](https://github.com/changesets/changesets/commit/492b0caa1a076551cf4bdca13d83dee4c485c9c8) Thanks [@bluwy](https://github.com/bluwy)! - Use `parseEnv` instead of `dotenv` to load the `.env` file and avoid loading them to `process.env` + +- Updated dependencies [[`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069), [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7), [`492b0ca`](https://github.com/changesets/changesets/commit/492b0caa1a076551cf4bdca13d83dee4c485c9c8)]: + - @changesets/get-github-info@1.0.0-next.2 + - @changesets/types@7.0.0-next.3 + +## 0.7.0 + +### Minor Changes + +- [#1255](https://github.com/changesets/changesets/pull/1255) [`94578cf`](https://github.com/changesets/changesets/commit/94578cf164aa7abcb12b97dd3a55d12a324f4fe8) Thanks [@Kauhsa](https://github.com/Kauhsa)! - Added `disableThanks` option + +## 1.0.0-next.2 + +### Patch Changes + +- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c)]: + - @changesets/types@7.0.0-next.2 + +## 0.6.0 + +### Minor Changes + +- [#1850](https://github.com/changesets/changesets/pull/1850) [`fd0bc2e`](https://github.com/changesets/changesets/commit/fd0bc2e997a7bf603415489d10fcac0ca129badf) Thanks [@mixelburg](https://github.com/mixelburg)! - Linkify issue references in changelog entries. + +### Patch Changes + +- [#1810](https://github.com/changesets/changesets/pull/1810) [`27fd8f4`](https://github.com/changesets/changesets/commit/27fd8f41dddafcc2e96e7df39dca04d92f916a0a) Thanks [@hirasso](https://github.com/hirasso)! - Replace deprecated `String.prototype.trimRight` with [`String.prototype.trimEnd`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd) + +- Updated dependencies [[`d4b8ad8`](https://github.com/changesets/changesets/commit/d4b8ad8158fe4d900abc5052dacaa8be1f41a232), [`e462d89`](https://github.com/changesets/changesets/commit/e462d892af560d0e3cf5d2f04da230751fbd05ca)]: + - @changesets/get-github-info@0.8.0 + +## 0.5.2 + +### Patch Changes + +- [#1783](https://github.com/changesets/changesets/pull/1783) [`398b3fe`](https://github.com/changesets/changesets/commit/398b3fe1cbce3c8a02f4d6a568f6cb724acffa5a) Thanks [@mrginglymus](https://github.com/mrginglymus)! - Respect `GITHUB_SERVER_URL` environment variable when constructing URLs + +- Updated dependencies [[`398b3fe`](https://github.com/changesets/changesets/commit/398b3fe1cbce3c8a02f4d6a568f6cb724acffa5a)]: + - @changesets/get-github-info@0.7.0 + +## 1.0.0-next.1 + +### Minor Changes + +- [#1656](https://github.com/changesets/changesets/pull/1656) [`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d) Thanks [@bluwy](https://github.com/bluwy)! - Bumps minimum node version to `>=20.0.0` + +### Patch Changes + +- Updated dependencies [[`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d)]: + - @changesets/get-github-info@1.0.0-next.1 + - @changesets/types@7.0.0-next.1 + +## 1.0.0-next.0 + +### Major Changes + +- [#1482](https://github.com/changesets/changesets/pull/1482) [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7) Thanks [@Andarist](https://github.com/Andarist)! - From now on this package is going to be published as ES module. + +### Minor Changes + +- [#1479](https://github.com/changesets/changesets/pull/1479) [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5) Thanks [@bluwy](https://github.com/bluwy)! - Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. + +- [#1615](https://github.com/changesets/changesets/pull/1615) [`4b962cc`](https://github.com/changesets/changesets/commit/4b962cc6e5c56ca519c3d5c00bdec59c754a43cc) Thanks [@bluwy](https://github.com/bluwy)! - Bump `dotenv` dependency to v16 + +### Patch Changes + +- Updated dependencies [[`5b02e2f`](https://github.com/changesets/changesets/commit/5b02e2f61d2a1335293016f81efb0386a0ed7967), [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5), [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7)]: + - @changesets/get-github-info@1.0.0-next.0 + - @changesets/types@7.0.0-next.0 + ## 0.5.1 ### Patch Changes diff --git a/packages/changelog-github/package.json b/packages/changelog-github/package.json index 57ab72b3f..288f4271b 100644 --- a/packages/changelog-github/package.json +++ b/packages/changelog-github/package.json @@ -1,29 +1,26 @@ { "name": "@changesets/changelog-github", - "version": "0.5.1", + "version": "1.0.0-next.5", "description": "A changelog entry generator for GitHub that links to commits, PRs and users", - "main": "dist/changesets-changelog-github.cjs.js", - "module": "dist/changesets-changelog-github.esm.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/changesets/changesets.git", + "directory": "packages/changelog-github" + }, + "type": "module", "exports": { - ".": { - "types": { - "import": "./dist/changesets-changelog-github.cjs.mjs", - "default": "./dist/changesets-changelog-github.cjs.js" - }, - "module": "./dist/changesets-changelog-github.esm.js", - "import": "./dist/changesets-changelog-github.cjs.mjs", - "default": "./dist/changesets-changelog-github.cjs.js" - }, + ".": "./dist/index.mjs", "./package.json": "./package.json" }, - "license": "MIT", - "repository": "https://github.com/changesets/changesets/tree/main/packages/changelog-github", "dependencies": { - "@changesets/get-github-info": "^0.6.0", - "@changesets/types": "^6.1.0", - "dotenv": "^8.1.0" + "@changesets/get-github-info": "workspace:^", + "@changesets/types": "workspace:^" }, "devDependencies": { - "@changesets/parse": "*" + "@changesets/parse": "workspace:*" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } } diff --git a/packages/changelog-github/src/index.test.ts b/packages/changelog-github/src/index.test.ts index a5904a22d..9033aad7f 100644 --- a/packages/changelog-github/src/index.test.ts +++ b/packages/changelog-github/src/index.test.ts @@ -1,12 +1,12 @@ -import changelogFunctions from "./index"; -import parse from "@changesets/parse"; +import { parseChangesetFile as parse } from "@changesets/parse"; +import { describe, expect, it, test, vi } from "vitest"; +import changelogFunctions from "./index.ts"; const getReleaseLine = changelogFunctions.getReleaseLine; -jest.mock( +vi.mock( "@changesets/get-github-info", (): typeof import("@changesets/get-github-info") => { - // this is duplicated because jest.mock reordering things const data = { commit: "a085003", user: "Andarist", @@ -19,6 +19,7 @@ jest.mock( commit: `[\`${data.commit}\`](https://github.com/${data.repo}/commit/${data.commit})`, }; return { + /* eslint-disable vitest/no-standalone-expect */ async getInfo({ commit, repo }) { expect(commit).toBe(data.commit); expect(repo).toBe(data.repo); @@ -37,8 +38,9 @@ jest.mock( links, }; }, + /* eslint-enable vitest/no-standalone-expect */ }; - } + }, ); const getChangeset = (content: string, commit: string | undefined) => { @@ -51,7 +53,7 @@ const getChangeset = (content: string, commit: string | undefined) => { something ${content} - ` + `, ), id: "some-id", commit, @@ -79,25 +81,25 @@ describe.each([data.commit, "wrongcommit", undefined])( await getReleaseLine( ...getChangeset( `${keyword}: ${kind === "with #" ? "#" : ""}${data.pull}`, - commitFromChangeset - ) - ) + commitFromChangeset, + ), + ), ).toEqual( - `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something\n` + `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something\n`, ); }); - } + }, ); test("override commit with commit keyword", async () => { expect( await getReleaseLine( - ...getChangeset(`commit: ${data.commit}`, commitFromChangeset) - ) + ...getChangeset(`commit: ${data.commit}`, commitFromChangeset), + ), ).toEqual( - `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something\n` + `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something\n`, ); }); - } + }, ); describe.each(["author", "user"])( @@ -108,24 +110,130 @@ describe.each(["author", "user"])( await getReleaseLine( ...getChangeset( `${keyword}: ${kind === "with @" ? "@" : ""}other`, - data.commit - ) - ) + data.commit, + ), + ), ).toEqual( - `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@other](https://github.com/other)! - something\n` + `\n\n- [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@other](https://github.com/other)! - something\n`, ); }); - } + }, ); +it("linkifies bare issue references", async () => { + expect( + await getReleaseLine(...getChangeset("fixes #1234 and #5678", data.commit)), + ).toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + fixes [#1234](https://github.com/emotion-js/emotion/issues/1234) and [#5678](https://github.com/emotion-js/emotion/issues/5678)" + `); +}); + +it("does not double-linkify existing markdown links", async () => { + expect( + await getReleaseLine( + ...getChangeset( + "see [#1234](https://github.com/emotion-js/emotion/issues/1234)", + data.commit, + ), + ), + ).toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + see [#1234](https://github.com/emotion-js/emotion/issues/1234)" + `); +}); + +it("does not linkify issue-like refs inside link text", async () => { + expect( + await getReleaseLine( + ...getChangeset("see [fix for #99](https://example.com)", data.commit), + ), + ).toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + see [fix for #99](https://example.com)" + `); +}); + +it("does not linkify when preceded by a word character", async () => { + expect(await getReleaseLine(...getChangeset("foo#123", data.commit))) + .toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + foo#123" + `); +}); + +it("does not linkify #0", async () => { + expect(await getReleaseLine(...getChangeset("see #0", data.commit))) + .toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + see #0" + `); +}); + +it("linkifies issue ref at the start of a line", async () => { + expect(await getReleaseLine(...getChangeset("#42 was fixed", data.commit))) + .toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + [#42](https://github.com/emotion-js/emotion/issues/42) was fixed" + `); +}); + +it("linkifies issue ref after punctuation", async () => { + expect(await getReleaseLine(...getChangeset("fixed (#99)", data.commit))) + .toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + fixed ([#99](https://github.com/emotion-js/emotion/issues/99))" + `); +}); + +it("handles mixed linked and bare refs", async () => { + expect( + await getReleaseLine( + ...getChangeset( + "fixes [#1](https://github.com/emotion-js/emotion/issues/1) and #2", + data.commit, + ), + ), + ).toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + fixes [#1](https://github.com/emotion-js/emotion/issues/1) and [#2](https://github.com/emotion-js/emotion/issues/2)" + `); +}); + +it("linkifies issue ref followed by a dot", async () => { + expect(await getReleaseLine(...getChangeset("this fixes #42.", data.commit))) + .toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) Thanks [@Andarist](https://github.com/Andarist)! - something + this fixes [#42](https://github.com/emotion-js/emotion/issues/42)." + `); +}); + it("with multiple authors", async () => { expect( await getReleaseLine( ...getChangeset( ["author: @Andarist", "author: @mitchellhamilton"].join("\n"), - data.commit - ) - ) + data.commit, + ), + ), ).toMatchInlineSnapshot(` " @@ -133,3 +241,21 @@ it("with multiple authors", async () => { " `); }); + +it("disables thanks if disableThanks is enabled", async () => { + const [changeset, releaseType, options] = getChangeset( + "author: @Andarist", + data.commit, + ); + expect( + await getReleaseLine(changeset, releaseType, { + ...options, + disableThanks: true, + }), + ).toMatchInlineSnapshot(` + " + + - [#1613](https://github.com/emotion-js/emotion/pull/1613) [\`a085003\`](https://github.com/emotion-js/emotion/commit/a085003) - something + " + `); +}); diff --git a/packages/changelog-github/src/index.ts b/packages/changelog-github/src/index.ts index 15618c47d..c471dc1eb 100644 --- a/packages/changelog-github/src/index.ts +++ b/packages/changelog-github/src/index.ts @@ -1,19 +1,57 @@ -import { ChangelogFunctions } from "@changesets/types"; -// @ts-ignore -import { config } from "dotenv"; +import fs from "node:fs/promises"; +import path from "node:path"; +import util from "node:util"; import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info"; +import type { ChangelogFunctions } from "@changesets/types"; -config(); +// "match what you skip, capture what you want": the left alternative +// consumes markdown links so the right alternative only matches bare refs +function linkifyIssueRefs( + line: string, + { serverUrl, repo }: { serverUrl: string; repo: string }, +): string { + return line.replace(/\[.*?\]\(.*?\)|\B#([1-9]\d*)\b/g, (match, issue) => + // PRs and issues are the same thing on GitHub (to some extent, of course) + // this relies on GitHub redirecting from /issues/1234 to /pull/1234 when necessary + issue ? `[#${issue}](${serverUrl}/${repo}/issues/${issue})` : match, + ); +} + +async function readEnvFile() { + const envFile = path.resolve(process.cwd(), ".env"); + let content: string | undefined; + try { + content = await fs.readFile(envFile, "utf-8"); + } catch { + return {}; + } + return util.parseEnv(content); +} + +let cachedEnv: ReturnType | undefined; +function readEnvFileCached() { + cachedEnv ??= readEnvFile(); + return cachedEnv; +} + +async function readEnv() { + const GITHUB_SERVER_URL = + process.env.GITHUB_SERVER_URL || + (await readEnvFileCached()).GITHUB_SERVER_URL || + "https://github.com"; + return { GITHUB_SERVER_URL }; +} const changelogFunctions: ChangelogFunctions = { getDependencyReleaseLine: async ( changesets, dependenciesUpdated, - options + options, ) => { - if (!options.repo) { + const repo = options?.repo; + if (!repo || typeof repo !== "string") { throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]' + 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]', ); } if (dependenciesUpdated.length === 0) return ""; @@ -22,38 +60,41 @@ const changelogFunctions: ChangelogFunctions = { await Promise.all( changesets.map(async (cs) => { if (cs.commit) { - let { links } = await getInfo({ - repo: options.repo, + const { links } = await getInfo({ + repo, commit: cs.commit, }); return links.commit; } - }) + }), ) ) .filter((_) => _) .join(", ")}]:`; const updatedDepenenciesList = dependenciesUpdated.map( - (dependency) => ` - ${dependency.name}@${dependency.newVersion}` + (dependency) => ` - ${dependency.name}@${dependency.newVersion}`, ); return [changesetLink, ...updatedDepenenciesList].join("\n"); }, getReleaseLine: async (changeset, type, options) => { - if (!options || !options.repo) { + const repo = options?.repo; + if (!repo || typeof repo !== "string") { throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]' + 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]', ); } + const { GITHUB_SERVER_URL } = await readEnv(); + let prFromSummary: number | undefined; let commitFromSummary: string | undefined; - let usersFromSummary: string[] = []; + const usersFromSummary: string[] = []; const replacedChangelog = changeset.summary .replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => { - let num = Number(pr); + const num = Number(pr); if (!isNaN(num)) prFromSummary = num; return ""; }) @@ -69,27 +110,27 @@ const changelogFunctions: ChangelogFunctions = { const [firstLine, ...futureLines] = replacedChangelog .split("\n") - .map((l) => l.trimRight()); + .map((l) => l.trimEnd()); const links = await (async () => { - if (prFromSummary !== undefined) { + if (prFromSummary != null) { let { links } = await getInfoFromPullRequest({ - repo: options.repo, + repo, pull: prFromSummary, }); if (commitFromSummary) { const shortCommitId = commitFromSummary.slice(0, 7); links = { ...links, - commit: `[\`${shortCommitId}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`, + commit: `[\`${shortCommitId}\`](${GITHUB_SERVER_URL}/${repo}/commit/${commitFromSummary})`, }; } return links; } const commitToFetchFrom = commitFromSummary || changeset.commit; if (commitToFetchFrom) { - let { links } = await getInfo({ - repo: options.repo, + const { links } = await getInfo({ + repo, commit: commitToFetchFrom, }); return links; @@ -101,25 +142,37 @@ const changelogFunctions: ChangelogFunctions = { }; })(); - const users = usersFromSummary.length - ? usersFromSummary - .map( - (userFromSummary) => - `[@${userFromSummary}](https://github.com/${userFromSummary})` - ) - .join(", ") - : links.user; + const users = options.disableThanks + ? null + : usersFromSummary.length + ? usersFromSummary + .map( + (userFromSummary) => + `[@${userFromSummary}](${GITHUB_SERVER_URL}/${userFromSummary})`, + ) + .join(", ") + : links.user; const prefix = [ - links.pull === null ? "" : ` ${links.pull}`, - links.commit === null ? "" : ` ${links.commit}`, - users === null ? "" : ` Thanks ${users}!`, + links.pull == null ? "" : ` ${links.pull}`, + links.commit == null ? "" : ` ${links.commit}`, + users == null ? "" : ` Thanks ${users}!`, ].join(""); - return `\n\n-${prefix ? `${prefix} -` : ""} ${firstLine}\n${futureLines - .map((l) => ` ${l}`) + return `\n\n-${prefix ? `${prefix} -` : ""} ${linkifyIssueRefs(firstLine, { + serverUrl: GITHUB_SERVER_URL, + repo, + })}\n${futureLines + .map( + (l) => + ` ${linkifyIssueRefs(l, { + serverUrl: GITHUB_SERVER_URL, + repo, + })}`, + ) .join("\n")}`; }, }; +// ChangelogFunctions require a default export export default changelogFunctions; diff --git a/packages/changelog-github/tsdown.config.ts b/packages/changelog-github/tsdown.config.ts new file mode 100644 index 000000000..8eddd7f1b --- /dev/null +++ b/packages/changelog-github/tsdown.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsdown/config"; +import { baseConfig } from "../../tsdown.config.ts"; + +export default defineConfig(baseConfig); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index bce151664..652892695 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,281 @@ # @changesets/cli +## 3.0.0-next.6 + +### Major Changes + +- [#2074](https://github.com/changesets/changesets/pull/2074) [`3599e47`](https://github.com/changesets/changesets/commit/3599e47217d2ed2dd60da628fe4d1d3fc4b849c7) Thanks [@bluwy](https://github.com/bluwy)! - Set supported package manager versions in `"engines"` field, including npm >=10.9.0, pnpm >=10.0.0, and yarn >=4.5.2. + +### Minor Changes + +- [#2068](https://github.com/changesets/changesets/pull/2068) [`d03ffc1`](https://github.com/changesets/changesets/commit/d03ffc1d11fb486328734e52767379646062f5c1) Thanks [@bluwy](https://github.com/bluwy)! - Support `{commit-short}` placeholder for the `snapshot.prereleaseTemplate` config, which is a 7 character variant of `{commit}` + +- [#2061](https://github.com/changesets/changesets/pull/2061) [`c2db1dd`](https://github.com/changesets/changesets/commit/c2db1dd5d2da6c6eb514d86bbe05cbb7227b067f) Thanks [@Andarist](https://github.com/Andarist)! - Added a `changeset publish-plan` command to inspect which packages would be published or tagged, with optional JSON output. + +- [#2100](https://github.com/changesets/changesets/pull/2100) [`90b4ad0`](https://github.com/changesets/changesets/commit/90b4ad0d1e0c41fab982f065f9f9ab522838499b) Thanks [@Andarist](https://github.com/Andarist)! - Order releases into dependency-aware chunks so packages are grouped in publish order. + +- [#2063](https://github.com/changesets/changesets/pull/2063) [`ed77176`](https://github.com/changesets/changesets/commit/ed771766183df240ff1dbedc6eaf6c0064b0c850) Thanks [@Andarist](https://github.com/Andarist)! - Added `changeset publish --from-pack-dir ` to publish packages from a previously created pack output directory. + +- [#2062](https://github.com/changesets/changesets/pull/2062) [`830443c`](https://github.com/changesets/changesets/commit/830443c757d2a685cc76f36ebebb081cb531b3c2) Thanks [@Andarist](https://github.com/Andarist)! - Added a `changeset pack` command that requires `--out-dir` and writes publishable package tarballs plus an enriched `publish-plan.json` into that directory, either from the current workspace or from a saved publish plan via `--from-plan`. + +- [#2073](https://github.com/changesets/changesets/pull/2073) [`b9cbd80`](https://github.com/changesets/changesets/commit/b9cbd804d68ac43af3b3ada32bed6217da0af81c) Thanks [@bluwy](https://github.com/bluwy)! - Show if a package is private when selecting packages in `changeset add` + +### Patch Changes + +- [#2060](https://github.com/changesets/changesets/pull/2060) [`11bded4`](https://github.com/changesets/changesets/commit/11bded4bd38e4ced3dfa4c428c50e2284c458ae3) Thanks [@Andarist](https://github.com/Andarist)! - Fixed `changeset publish` to respect ignored packages for both publishing and private package tagging. + +- [#2064](https://github.com/changesets/changesets/pull/2064) [`ffd65fc`](https://github.com/changesets/changesets/commit/ffd65fc6110a5ac6dfb27eac7a28a8b26751acc7) Thanks [@Andarist](https://github.com/Andarist)! - For pnpm projects, Changesets now match pnpm's native registry behavior more closely during unpublished package checks. Both scope-based `publishConfig` registry overrides and `publishConfig.registry` are now ignored. + +- [#2113](https://github.com/changesets/changesets/pull/2113) [`b8222e6`](https://github.com/changesets/changesets/commit/b8222e688b11e53c2e6b3fab811ccfb50038007b) Thanks [@Andarist](https://github.com/Andarist)! - Fixed publish error printing for pnpm 11. + +- [#2111](https://github.com/changesets/changesets/pull/2111) [`124ad07`](https://github.com/changesets/changesets/commit/124ad077c1361b00efa838c0400dc8a835645036) Thanks [@Andarist](https://github.com/Andarist)! - Auto-create the directory for the target publish plan file when executing `changeset publish-plan --output ` + +- [#2113](https://github.com/changesets/changesets/pull/2113) [`b8222e6`](https://github.com/changesets/changesets/commit/b8222e688b11e53c2e6b3fab811ccfb50038007b) Thanks [@Andarist](https://github.com/Andarist)! - Fixed accidental success logs on failed npm publishes + +- [#2091](https://github.com/changesets/changesets/pull/2091) [`3918fe5`](https://github.com/changesets/changesets/commit/3918fe56a32b13c00d76e21e96f6280527a1871c) Thanks [@bluwy](https://github.com/bluwy)! - Log "New tag: ..." messages when running `changeset publish` to fix compatibility with the Changesets release GitHub action to create GitHub releases and push the new tags +- Updated dependencies [[`694396c`](https://github.com/changesets/changesets/commit/694396ce49f0d7e2200c119b360e60e6bd11265f), [`d03ffc1`](https://github.com/changesets/changesets/commit/d03ffc1d11fb486328734e52767379646062f5c1), [`01f4da4`](https://github.com/changesets/changesets/commit/01f4da4e30aa90391def46b84b986fa223a055f5), [`c2348fc`](https://github.com/changesets/changesets/commit/c2348fcb9eba443fde1460b595651ce040f40a08)]: + - @changesets/apply-release-plan@8.0.0-next.6 + - @changesets/assemble-release-plan@7.0.0-next.6 + - @changesets/read@1.0.0-next.6 + +## 3.0.0-next.5 + +### Patch Changes + +- [#2041](https://github.com/changesets/changesets/pull/2041) [`ce39c72`](https://github.com/changesets/changesets/commit/ce39c725afba19588c261cc521ef4fd55f72ef19) Thanks [@bluwy](https://github.com/bluwy)! - Enable guide line for `add` command and use box design for dependent patch bump note + +- [#2009](https://github.com/changesets/changesets/pull/2009) [`44df27d`](https://github.com/changesets/changesets/commit/44df27d25745609bcf0c66ae244de2b0464a1b2d) Thanks [@bluwy](https://github.com/bluwy)! - Use `cac` for CLI arg parsing and handling + +- Updated dependencies [[`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf), [`88f2abb`](https://github.com/changesets/changesets/commit/88f2abb5e14748b08e3441fd871df60dd1c4737f), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf), [`6a05002`](https://github.com/changesets/changesets/commit/6a05002228a06807b1a95da841d1809ae07441bf)]: + - @changesets/config@4.0.0-next.5 + - @changesets/types@7.0.0-next.5 + - @changesets/errors@1.0.0-next.3 + - @changesets/apply-release-plan@8.0.0-next.5 + - @changesets/assemble-release-plan@7.0.0-next.5 + - @changesets/changelog-git@1.0.0-next.5 + - @changesets/get-dependents-graph@3.0.0-next.5 + - @changesets/git@4.0.0-next.5 + - @changesets/pre@3.0.0-next.5 + - @changesets/read@1.0.0-next.5 + - @changesets/should-skip-package@1.0.0-next.5 + - @changesets/write@1.0.0-next.5 + +## 3.0.0-next.4 + +### Major Changes + +- [#1994](https://github.com/changesets/changesets/pull/1994) [`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8) Thanks [@bluwy](https://github.com/bluwy)! - The `prettier` option in `.changeset/config.json` has been removed in favor of `format`. `format` supports `"auto"`, `"prettier"`, `"oxfmt"`, `"deno"`, and `"dprint"`, and `false` disables formatting. If you previously used `prettier: false`, migrate to `format: false` or remove the option to use automatic formatter detection. + +- [#1879](https://github.com/changesets/changesets/pull/1879) [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc) Thanks [@beeequeue](https://github.com/beeequeue)! - Removed warning messages about using v1 configs. They will now be silently ignored. + +- [#1879](https://github.com/changesets/changesets/pull/1879) [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc) Thanks [@beeequeue](https://github.com/beeequeue)! - Migrated from `enquirer` + `@inquirer/launch-editor` to `@clack/prompts` + `launch-editor`. + + This means the CLI flows will have minor changes, but they are largely the same. + + This change also fixes various issues related to `enquirer` like cancelling prompts crashing the CLI. + +### Minor Changes + +- [#1879](https://github.com/changesets/changesets/pull/1879) [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc) Thanks [@beeequeue](https://github.com/beeequeue)! - Packages are now listed in alphabetical order when possible. + +- [#1879](https://github.com/changesets/changesets/pull/1879) [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc) Thanks [@beeequeue](https://github.com/beeequeue)! - Choosing a change type now shows a preview of which part of the version it affects. + + > Which packages should have a major (**X**.X.X) bump? + +### Patch Changes + +- [#2002](https://github.com/changesets/changesets/pull/2002) [`6db2c21`](https://github.com/changesets/changesets/commit/6db2c2160ee8f7a2401faeeb1477a98ff47fab12) Thanks [@Andarist](https://github.com/Andarist)! - Lazy-load CLI commands so `changeset` only loads the code needed for the command being run. + +- [#2004](https://github.com/changesets/changesets/pull/2004) [`169b128`](https://github.com/changesets/changesets/commit/169b128522f0e53ef228f3acd8118709b0f72156) Thanks [@ghostdevv](https://github.com/ghostdevv)! - Replace `picocolors` with `node:util`'s `styleText` + +- Updated dependencies [[`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8), [`ee10723`](https://github.com/changesets/changesets/commit/ee10723dde491ba6632da74d10876dfa2e67d0d2), [`c76b232`](https://github.com/changesets/changesets/commit/c76b232abc76f73592a21f0d5df9cc89406a31dc), [`fc42514`](https://github.com/changesets/changesets/commit/fc425143294e63ba254ddbe8c2ea026b55a05991), [`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8), [`169b128`](https://github.com/changesets/changesets/commit/169b128522f0e53ef228f3acd8118709b0f72156), [`062530b`](https://github.com/changesets/changesets/commit/062530b825d53abc9d8934f3a50cc61ff3ff82b8)]: + - @changesets/config@4.0.0-next.4 + - @changesets/types@7.0.0-next.4 + - @changesets/apply-release-plan@8.0.0-next.4 + - @changesets/write@1.0.0-next.4 + - @changesets/get-dependents-graph@3.0.0-next.4 + - @changesets/read@1.0.0-next.4 + - @changesets/assemble-release-plan@7.0.0-next.4 + - @changesets/get-release-plan@5.0.0-next.4 + - @changesets/changelog-git@1.0.0-next.4 + - @changesets/git@4.0.0-next.4 + - @changesets/pre@3.0.0-next.4 + - @changesets/should-skip-package@1.0.0-next.4 + +## 3.0.0-next.3 + +### Major Changes + +- [#1860](https://github.com/changesets/changesets/pull/1860) [`92b1c1b`](https://github.com/changesets/changesets/commit/92b1c1b6b1733b1f1799a73828d378646b068798) Thanks [@mixelburg](https://github.com/mixelburg)! - `changeset version` now exits with code 1 when there are no unreleased changesets, instead of silently exiting with code 0. + + This makes it easier to detect when a version step is a no-op — for example, to prevent accidentally publishing packages with incorrect version tags when using `--snapshot` mode. + +- [#1954](https://github.com/changesets/changesets/pull/1954) [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped supported Node versions to `^22.11 || ^24 || >=26` + +- [#1961](https://github.com/changesets/changesets/pull/1961) [`07278a7`](https://github.com/changesets/changesets/commit/07278a726343388eb6dfc56e7a8213872d4c8857) Thanks [@beeequeue](https://github.com/beeequeue)! - `CommitFunctions` can now be both sync and async, and the `defaultCommitFunctions` are now sync. + +- [#1652](https://github.com/changesets/changesets/pull/1652) [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7) Thanks [@bluwy](https://github.com/bluwy)! - Remove support for the deprecated `___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.useCalculatedVersionForSnapshots` config. The `snapshot.useCalculatedVersion` config should be used instead. + +### Minor Changes + +- [#1969](https://github.com/changesets/changesets/pull/1969) [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Add a named export that mirrors the current `default` export + + The `default` export is slated for removal in the next major release, so this ensures a smoother transition path. + +### Patch Changes + +- [#1667](https://github.com/changesets/changesets/pull/1667) [`81832f8`](https://github.com/changesets/changesets/commit/81832f855029f4093b2142ba22b747ba0de92425) Thanks [@Andarist](https://github.com/Andarist)! - Fixed resolution of changelog and commit generator modules so built-in modules can still be loaded when they are not installed in the target project. + +- Updated dependencies [[`81832f8`](https://github.com/changesets/changesets/commit/81832f855029f4093b2142ba22b747ba0de92425), [`ed6728c`](https://github.com/changesets/changesets/commit/ed6728ce3c089caaee19f71194a0cd7029480069), [`03d4479`](https://github.com/changesets/changesets/commit/03d44794fedd24ae9eb053b28624c1fd8fe6fe6f), [`b9407b3`](https://github.com/changesets/changesets/commit/b9407b39a458bab106d0e23a3afab01d07d8482f), [`2c7c043`](https://github.com/changesets/changesets/commit/2c7c043d7071440009f8a69eff0b0c6746ac7625), [`07278a7`](https://github.com/changesets/changesets/commit/07278a726343388eb6dfc56e7a8213872d4c8857), [`ad3edbd`](https://github.com/changesets/changesets/commit/ad3edbdc78c7b2ba451577969b6137df275ec430), [`a0b5326`](https://github.com/changesets/changesets/commit/a0b5326570e8e7bf5e35c1cefe8f70d9a51a5cd7)]: + - @changesets/apply-release-plan@8.0.0-next.3 + - @changesets/assemble-release-plan@7.0.0-next.3 + - @changesets/get-dependents-graph@3.0.0-next.3 + - @changesets/should-skip-package@1.0.0-next.3 + - @changesets/get-release-plan@5.0.0-next.3 + - @changesets/changelog-git@1.0.0-next.3 + - @changesets/config@4.0.0-next.3 + - @changesets/errors@1.0.0-next.2 + - @changesets/logger@1.0.0-next.2 + - @changesets/types@7.0.0-next.3 + - @changesets/write@1.0.0-next.3 + - @changesets/read@1.0.0-next.3 + - @changesets/git@4.0.0-next.3 + - @changesets/pre@3.0.0-next.3 + +## 3.0.0-next.2 + +### Major Changes + +- [#1655](https://github.com/changesets/changesets/pull/1655) [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2) Thanks [@bluwy](https://github.com/bluwy)! - Update `@manypkg/get-packages` which drops support for detecting packages in Bolt monorepos and adds support for npm monorepos + +### Minor Changes + +- [#1744](https://github.com/changesets/changesets/pull/1744) [`303cacd`](https://github.com/changesets/changesets/commit/303cacdde85c94f2ef4d1408b401165ff25d263d) Thanks [@beeequeue](https://github.com/beeequeue)! - Bumped the default Prettier version used in the absence of the local installation to v3 + +### Patch Changes + +- [#1875](https://github.com/changesets/changesets/pull/1875) [`12f20ea`](https://github.com/changesets/changesets/commit/12f20ea75fb5a440a378bd2bf6072a6bd749fd57) Thanks [@beeequeue](https://github.com/beeequeue)! - Replaced `spawndamnit` with `tinyexec` + +- Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c), [`db46911`](https://github.com/changesets/changesets/commit/db46911e57603f20a158a47bbbebd112272c84e2), [`303cacd`](https://github.com/changesets/changesets/commit/303cacdde85c94f2ef4d1408b401165ff25d263d), [`12f20ea`](https://github.com/changesets/changesets/commit/12f20ea75fb5a440a378bd2bf6072a6bd749fd57)]: + - @changesets/types@7.0.0-next.2 + - @changesets/assemble-release-plan@7.0.0-next.2 + - @changesets/get-dependents-graph@3.0.0-next.2 + - @changesets/apply-release-plan@8.0.0-next.2 + - @changesets/get-release-plan@5.0.0-next.2 + - @changesets/config@4.0.0-next.2 + - @changesets/git@4.0.0-next.2 + - @changesets/pre@3.0.0-next.2 + - @changesets/changelog-git@1.0.0-next.2 + - @changesets/read@1.0.0-next.2 + - @changesets/should-skip-package@1.0.0-next.2 + - @changesets/write@1.0.0-next.2 + +## 2.31.0 + +### Minor Changes + +- [#1889](https://github.com/changesets/changesets/pull/1889) [`96ca062`](https://github.com/changesets/changesets/commit/96ca062272605c14f77a64043f50a0a3a278c57f) Thanks [@mixelburg](https://github.com/mixelburg)! - Error on unsupported flags for individual CLI commands and print the matching command usage to make mistakes easier to spot. + +- [#1873](https://github.com/changesets/changesets/pull/1873) [`42943b7`](https://github.com/changesets/changesets/commit/42943b74d7a455ed03b93dd85e1c0a15f45db37f) Thanks [@mixelburg](https://github.com/mixelburg)! - Respond to `--help` on all subcommands. Previously, `--help` was only handled when it was the sole argument; passing it alongside a subcommand (e.g. `changeset version --help`) would silently execute the command instead. Now `--help` always exits early and prints per-command usage when a known subcommand is provided, or the general help text otherwise. + +### Patch Changes + +- [`d2121dc`](https://github.com/changesets/changesets/commit/d2121dc3d86b55f76de6022ccfcde843ed4b884a) Thanks [@Andarist](https://github.com/Andarist)! - Fix npm auth for path-based registries during publish by preserving configured registry URLs instead of normalizing them. + +- [#1888](https://github.com/changesets/changesets/pull/1888) [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix several `changeset version` issues with workspace protocol dependencies. Valid explicit `workspace:` ranges and aliases are no longer rewritten unnecessarily, and workspace path references are handled correctly during versioning. + +- [#1903](https://github.com/changesets/changesets/pull/1903) [`5c4731f`](https://github.com/changesets/changesets/commit/5c4731fea82ce880500ac5e1c55ff372f7a4efe2) Thanks [@Andarist](https://github.com/Andarist)! - Gracefully handle stale `npm info` data leading to duplicate publish attempts. + +- [#1867](https://github.com/changesets/changesets/pull/1867) [`f61e716`](https://github.com/changesets/changesets/commit/f61e7166c349d4934e4acc9b47f3d028c212ecc1) Thanks [@Andarist](https://github.com/Andarist)! - Improved detection for `published` state of prerelease-only packages without `latest` dist-tag on GitHub Packages registry. + +- Updated dependencies [[`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464), [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464), [`036fdd4`](https://github.com/changesets/changesets/commit/036fdd451367226d0f2cd8af1e0a7f37a65e3464)]: + - @changesets/assemble-release-plan@6.0.10 + - @changesets/get-dependents-graph@2.1.4 + - @changesets/apply-release-plan@7.1.1 + - @changesets/get-release-plan@4.0.16 + - @changesets/config@3.1.4 + +## 2.30.0 + +### Minor Changes + +- [#1840](https://github.com/changesets/changesets/pull/1840) [`057cca2`](https://github.com/changesets/changesets/commit/057cca222321816b6c8c6f6c52130185b364de36) Thanks [@wotan-allfather](https://github.com/wotan-allfather)! - Add `--since` flag to `add` command + + The `add` command now supports a `--since` flag that allows you to specify which branch, tag, or git ref to use when detecting changed packages. This is useful for gitflow workflows where you have multiple target branches and the `baseBranch` config option doesn't cover all use cases. + + Example: `changeset add --since=develop` + + If not provided, the command falls back to the `baseBranch` value in your `.changeset/config.json`. + +- [#1845](https://github.com/changesets/changesets/pull/1845) [`2b4a66a`](https://github.com/changesets/changesets/commit/2b4a66a36497fd5504186dcc6ae9e287c8403de6) Thanks [@Andarist](https://github.com/Andarist)! - Delegate OTP prompting to the package manager instead of handling it in-process. This allows Changesets to use the package manager's native web auth support. + +- [#1774](https://github.com/changesets/changesets/pull/1774) [`667fe5a`](https://github.com/changesets/changesets/commit/667fe5aacf04dbefcf2532584ff2753b8417855a) Thanks [@bluwy](https://github.com/bluwy)! - Support importing custom `commit` option ES module. Previously, it used `require()` which only worked for CJS modules, however now it uses `import()` which supports both CJS and ES modules. + +- [#1839](https://github.com/changesets/changesets/pull/1839) [`73b1809`](https://github.com/changesets/changesets/commit/73b18099517b00a3c7b70c417b7f7f1bfaa24931) Thanks [@leochiu-a](https://github.com/leochiu-a)! - Add a `--message` (`-m`) flag to `changeset add` (and default `changeset`) so the changeset summary can be provided from the command line. When `--message` is present, the summary prompt is skipped while the final confirmation step is kept. + +- [#1806](https://github.com/changesets/changesets/pull/1806) [`0e8e01e`](https://github.com/changesets/changesets/commit/0e8e01e93358bdc8c318c608dd3b0e4af8219049) Thanks [@luisadame](https://github.com/luisadame)! - Changeset CLI can now be run from the nested directories in the project, where the `.changeset` directory has to be found in one of the parent directories + +### Patch Changes + +- [#1849](https://github.com/changesets/changesets/pull/1849) [`9dc3230`](https://github.com/changesets/changesets/commit/9dc32308e4d208964b648a788ba4eee1003c273c) Thanks [@Andarist](https://github.com/Andarist)! - Compute the terminal's size lazily to avoid spurious stderr output in non-interactive mode + +- [#1857](https://github.com/changesets/changesets/pull/1857) [`2a73025`](https://github.com/changesets/changesets/commit/2a7302577d2923dc7db5025003d8aa58fb627ff9) Thanks [@mixelburg](https://github.com/mixelburg)! - Fix confusing prompt labels when entering changeset summary after external editor fallback + +- [#1842](https://github.com/changesets/changesets/pull/1842) [`6df3a5e`](https://github.com/changesets/changesets/commit/6df3a5e95522a0210cb2b5619588a75f32b502c6) Thanks [@RodrigoHamuy](https://github.com/RodrigoHamuy)! - Allow private packages to depend on skipped packages without requiring them to also be skipped. Private packages are not published to npm, so it is safe for them to have dependencies on ignored or unversioned packages. + +- [#1776](https://github.com/changesets/changesets/pull/1776) [`503fcaa`](https://github.com/changesets/changesets/commit/503fcaae57c397e14a52da7700dc5cb8e7cbd551) Thanks [@bluwy](https://github.com/bluwy)! - Support absolute paths in `changeset status --output ` + +- Updated dependencies [[`667fe5a`](https://github.com/changesets/changesets/commit/667fe5aacf04dbefcf2532584ff2753b8417855a), [`1772598`](https://github.com/changesets/changesets/commit/1772598270a59ba1fa7b0ef7e675fce6a575f850), [`b6f4c74`](https://github.com/changesets/changesets/commit/b6f4c748c4ba50b5ac608f3ce41229526d1bfe94), [`6df3a5e`](https://github.com/changesets/changesets/commit/6df3a5e95522a0210cb2b5619588a75f32b502c6), [`6df3a5e`](https://github.com/changesets/changesets/commit/6df3a5e95522a0210cb2b5619588a75f32b502c6), [`27fd8f4`](https://github.com/changesets/changesets/commit/27fd8f41dddafcc2e96e7df39dca04d92f916a0a)]: + - @changesets/apply-release-plan@7.1.0 + - @changesets/config@3.1.3 + - @changesets/get-release-plan@4.0.15 + - @changesets/read@0.6.7 + +## 2.29.8 + +### Patch Changes + +- [#1437](https://github.com/changesets/changesets/pull/1437) [`aa68d54`](https://github.com/changesets/changesets/commit/aa68d54faa17f825345ef3732d20e4a74423e5fd) Thanks [@with-heart](https://github.com/with-heart)! - Tweaked a hint text printed when one confirms an empty set of packages to be released + +- Updated dependencies [[`cc28222`](https://github.com/changesets/changesets/commit/cc28222ee892b3a078fa02ee26e1cef98c171532), [`e520bf5`](https://github.com/changesets/changesets/commit/e520bf5d4dbfe96f59ca28008e87bffaf3c9dfea), [`13dace8`](https://github.com/changesets/changesets/commit/13dace895017fa351014bc9e13b544d33f8b4bbe)]: + - @changesets/config@3.1.2 + - @changesets/apply-release-plan@7.0.14 + - @changesets/get-release-plan@4.0.14 + - @changesets/read@0.6.6 + +## 2.29.7 + +### Patch Changes + +- Updated dependencies [[`957f24e`](https://github.com/changesets/changesets/commit/957f24ed0446494c5709189ae57583f72c716d43)]: + - @changesets/apply-release-plan@7.0.13 + +## 3.0.0-next.1 + +### Major Changes + +- [#1656](https://github.com/changesets/changesets/pull/1656) [`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d) Thanks [@bluwy](https://github.com/bluwy)! - Bumps minimum node version to `>=20.0.0` + +- [#1651](https://github.com/changesets/changesets/pull/1651) [`e1df862`](https://github.com/changesets/changesets/commit/e1df8625800a1b3c4f66474a0e3c01b08214465c) Thanks [@bluwy](https://github.com/bluwy)! - Remove support for the `--sinceMaster` flag for `changeset status`. Use `--since=master` or `--since=main` instead. + +### Patch Changes + +- Updated dependencies [[`268a29f`](https://github.com/changesets/changesets/commit/268a29fedc948f22c672a3b1e3e51df4427f478d), [`b83787f`](https://github.com/changesets/changesets/commit/b83787fb090dc03ad566a7d8b7e286dbe93e2301)]: + - @changesets/assemble-release-plan@7.0.0-next.1 + - @changesets/get-dependents-graph@3.0.0-next.1 + - @changesets/should-skip-package@1.0.0-next.1 + - @changesets/apply-release-plan@8.0.0-next.1 + - @changesets/get-release-plan@5.0.0-next.1 + - @changesets/changelog-git@1.0.0-next.1 + - @changesets/config@4.0.0-next.1 + - @changesets/errors@1.0.0-next.1 + - @changesets/logger@1.0.0-next.1 + - @changesets/types@7.0.0-next.1 + - @changesets/write@1.0.0-next.1 + - @changesets/read@1.0.0-next.1 + - @changesets/git@4.0.0-next.1 + - @changesets/pre@3.0.0-next.1 + ## 2.29.6 ### Patch Changes @@ -38,6 +314,40 @@ - @changesets/assemble-release-plan@6.0.7 - @changesets/get-release-plan@4.0.11 +## 3.0.0-next.0 + +### Major Changes + +- [#1479](https://github.com/changesets/changesets/pull/1479) [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5) Thanks [@bluwy](https://github.com/bluwy)! - Add `"engines"` field for explicit node version support. The supported node versions are `>=18.0.0`. + +- [#1482](https://github.com/changesets/changesets/pull/1482) [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7) Thanks [@Andarist](https://github.com/Andarist)! - From now on this package is going to be published as ES module. + +### Patch Changes + +- [#1476](https://github.com/changesets/changesets/pull/1476) [`e0e1748`](https://github.com/changesets/changesets/commit/e0e1748369b1f936c665b62590a76a0d57d1545e) Thanks [@pralkarz](https://github.com/pralkarz)! - Replace `fs-extra` usage with `node:fs` + +- [#1570](https://github.com/changesets/changesets/pull/1570) [`d099e43`](https://github.com/changesets/changesets/commit/d099e43300c590c86740a638330bed03511c7c77) Thanks [@pralkarz](https://github.com/pralkarz)! - Removed extra leftover code related to Changesets v1 + +- [#1616](https://github.com/changesets/changesets/pull/1616) [`609046c`](https://github.com/changesets/changesets/commit/609046c353caa08473b86e7c194e256e1cecacaa) Thanks [@bluwy](https://github.com/bluwy)! - Remove `term-size` dependency + +- [#1640](https://github.com/changesets/changesets/pull/1640) [`2ad65e0`](https://github.com/changesets/changesets/commit/2ad65e062ec882dcde461dbcd80a3e1d4ee98c96) Thanks [@bluwy](https://github.com/bluwy)! - Remove deprecated flag warnings, including `--updateChangelog`, `--isPublic`, `--skipCI`, and `--commit` + +- Updated dependencies [[`e0e1748`](https://github.com/changesets/changesets/commit/e0e1748369b1f936c665b62590a76a0d57d1545e), [`6d1f384`](https://github.com/changesets/changesets/commit/6d1f384c8feab091f58443f6f7ee2ada64e0e7cc), [`7f34a00`](https://github.com/changesets/changesets/commit/7f34a00aab779a941a406b17f5a85895144fc0a5), [`3628cab`](https://github.com/changesets/changesets/commit/3628cab6cbfd931b7f2a909b38b66c1aa794d4bf), [`8f7b607`](https://github.com/changesets/changesets/commit/8f7b607b486e299e038bf8e257d28f0193ac4412), [`df424a4`](https://github.com/changesets/changesets/commit/df424a4a09eea15b0fa9159ee0b98af0d95f58a7)]: + - @changesets/apply-release-plan@8.0.0-next.0 + - @changesets/config@4.0.0-next.0 + - @changesets/write@1.0.0-next.0 + - @changesets/read@1.0.0-next.0 + - @changesets/git@4.0.0-next.0 + - @changesets/pre@3.0.0-next.0 + - @changesets/assemble-release-plan@7.0.0-next.0 + - @changesets/get-dependents-graph@3.0.0-next.0 + - @changesets/should-skip-package@1.0.0-next.0 + - @changesets/get-release-plan@5.0.0-next.0 + - @changesets/changelog-git@1.0.0-next.0 + - @changesets/errors@1.0.0-next.0 + - @changesets/logger@1.0.0-next.0 + - @changesets/types@7.0.0-next.0 + ## 2.29.2 ### Patch Changes @@ -229,7 +539,6 @@ ### Patch Changes - [#1354](https://github.com/changesets/changesets/pull/1354) [`69be7dc`](https://github.com/changesets/changesets/commit/69be7dc7195011ac6dbd00b24ea923f02adcf69c) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with `changeset status` incorrectly returning an error status in two cases: - - for changed ignored packages - for changed private packages when `privatePackage.version` was set to `false` @@ -1187,7 +1496,6 @@ - [`a679b1d`](https://github.com/changesets/changesets/commit/a679b1dcdcb56652d31536e2d6326ba02a9dfe62) [#204](https://github.com/changesets/changesets/pull/204) Thanks [@Andarist](https://github.com/Andarist)! - Correctly handle the 'access' flag for packages Previously, we had access as "public" or "private", access "private" isn't valid. This was a confusing because there are three states for publishing a package: - - `private: true` - the package will not be published to npm (worked) - `access: public` - the package will be publicly published to npm (even if it uses a scope) (worked) - `access: restricted` - the package will be published to npm, but only visible/accessible by those who are part of the scope. This technically worked, but we were passing the wrong bit of information in. @@ -1423,7 +1731,6 @@ meaning within the community, even though these commands do slightly more than t - [e55fa3f0](https://github.com/changesets/changesets/commit/e55fa3f0) [#92](https://github.com/changesets/changesets/pull/92) Thanks [@highvoltag3](https://github.com/highvoltag3)! - Catch Promise rejection on SIGINT and exit gracefully - [94267ff3](https://github.com/changesets/changesets/commit/94267ff3) [#106](https://github.com/changesets/changesets/pull/106) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Improve 2FA support for publishing: - - Prompt for an OTP code when required - Add `--otp` option to release command @@ -1466,7 +1773,6 @@ meaning within the community, even though these commands do slightly more than t - [7fa42641](https://github.com/changesets/changesets/commit/7fa42641) [#61](https://github.com/changesets/changesets/pull/61) Thanks [@Noviny](https://github.com/Noviny)! - When bumping, run prettier over the changelog file. If you want this option turned off, add `disabledLanguage: ["markdown"] to your prettier config. - - [6dc510f4](https://github.com/changesets/changesets/commit/6dc510f4) [#62](https://github.com/changesets/changesets/pull/62) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Add butterfly emoji prefix to CLI output ## 1.1.4 @@ -1536,14 +1842,12 @@ meaning within the community, even though these commands do slightly more than t the binary has been changed to `changeset`. The new names are: - - `build-releases initialize` => `changeset init` (for ecosystem consistency) - `build-releases changeset` => `changeset` (default command). You can also run `changeset add` - `build-releases version` => `changeset bump` - `build-releases publish` => `changeset release` The function of these commands remains unchanged. - - 51c8b0d6: Change format of changelog entries Previously changelog entries were in the form of: @@ -1604,7 +1908,6 @@ meaning within the community, even though these commands do slightly more than t - 51c8b0d6: Support non-bolt repositories Changesets have been expanded to support: - - bolt repositories - yarn workspaces-based repositories - single package repositories. @@ -1620,7 +1923,6 @@ meaning within the community, even though these commands do slightly more than t If plans to modularize bolt proceed, we may go back to relying on its functions. This should have no impact on use. - - 51c8b0d6: Add 'select all' and 'select all changed' options, to make mass-bumping easier. - eeb4d5c6: Add new command: `status` - see Readme for more information @@ -1629,19 +1931,16 @@ meaning within the community, even though these commands do slightly more than t ## 3.0.3 - [patch][c87337f](https://bitbucket.org/changesets/atlaskit-mk-2/commits/c87337f): - - The version command now removes empty folders before it starts. This should prevent a race condition in CI ## 3.0.2 - [patch][f7b030a](https://bitbucket.org/changesets/atlaskit-mk-2/commits/f7b030a): - - Fixes potential infinite loop in parseChangesetCommit ## 3.0.1 - [patch][494c1fe](https://bitbucket.org/changesets/atlaskit-mk-2/commits/494c1fe): - - Update git commit message to match previous tooling. ## 3.0.0 @@ -1650,7 +1949,6 @@ meaning within the community, even though these commands do slightly more than t d): Changesets now use local file system - this has several effects: - 1. Changesets will no longer automatically create a commit. You will need to add and commit the files yourself. 2. Changesets are easier to modify. You should ONLY modify the changes.md file (_Not changes.json_). 3. There will be a new directory which is `.changeset`, which will hold all the changesets. diff --git a/packages/cli/README.md b/packages/cli/README.md index fa78b5bee..56c801452 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,15 +1,11 @@ ## @changesets/cli 🦋 -[![npm package](https://img.shields.io/npm/v/@changesets/cli)](https://npmjs.com/package/@changesets/cli) -[![View changelog](https://img.shields.io/badge/Explore%20Changelog-brightgreen)](./CHANGELOG.md) +[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/@changesets/cli?name=true)](https://npmx.dev/package/@changesets/cli) +[![View changelog](https://npmx.dev/api/registry/badge/version/@changesets/cli?color=229fe4&value=View+changelog&label=+)](./CHANGELOG.md) -The primary implementation of [changesets](https://github.com/changesets/changesets). Helps you manage the versioning -and changelog entries for your packages, with a focus on versioning within a mono-repository (though we support -single-package repositories too). +A tool to manage versioning and changelogs with a focus on monorepos. -This package is intended as a successor to `@atlaskit/build-releases` with a more general focus. It works in -[bolt](https://www.npmjs.com/package/bolt) multi-package repositories, [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) multi-package repositories, and -in single-package repositories. +[Read the docs to learn more](https://changesets.dev). ## Getting Started @@ -100,13 +96,13 @@ To publish public packages to NPM, you'll need to edit `.changeset/config.json` ### add ```shell -changeset [--empty] [--open] +changeset [--empty] [--open] [--message ] ``` or ```shell -changeset add [--empty] [--open] +changeset add [--empty] [--open] [--message ] ``` This command will ask you a series of questions, first about what packages you want to release, then what semver bump type for each package, then it will ask for a summary of the entire changeset. At the final step it will show the changeset it will generate, and confirm that you want to add it. @@ -137,6 +133,7 @@ A changeset created with the `empty` flag would look like this: If you set the `commit` option in the config, the command will add the updated changeset files and then commit them. - `--open` - opens the created changeset in an external editor +- `--message` (or `-m`) - provides the changeset summary from the command line instead of prompting for it. ### version diff --git a/packages/cli/bin.js b/packages/cli/bin.js index d970c52c4..ab36753fa 100755 --- a/packages/cli/bin.js +++ b/packages/cli/bin.js @@ -1,4 +1,2 @@ #!/usr/bin/env node -"use strict"; - -require("./"); +import "@changesets/cli"; diff --git a/packages/cli/changelog/package.json b/packages/cli/changelog/package.json deleted file mode 100644 index d737e202c..000000000 --- a/packages/cli/changelog/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "dist/changesets-cli-changelog.cjs.js", - "module": "dist/changesets-cli-changelog.esm.js" -} diff --git a/packages/cli/commit/package.json b/packages/cli/commit/package.json deleted file mode 100644 index 8746bf853..000000000 --- a/packages/cli/commit/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "dist/changesets-cli-commit.cjs.js", - "module": "dist/changesets-cli-commit.esm.js" -} diff --git a/packages/cli/default-files/README.md b/packages/cli/default-files/README.md index e5b6d8d6a..654c6d475 100644 --- a/packages/cli/default-files/README.md +++ b/packages/cli/default-files/README.md @@ -2,7 +2,7 @@ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) +find the full documentation for it [in our repository](https://github.com/changesets/changesets). We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/packages/cli/package.json b/packages/cli/package.json index c8e3f4970..0319c4677 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,11 +1,23 @@ { "name": "@changesets/cli", - "version": "2.29.6", - "description": "Organise your package versioning and publishing to make both contributors and maintainers happy", + "version": "3.0.0-next.6", + "description": "A tool to manage versioning and changelogs with a focus on monorepos", + "homepage": "https://changesets.dev", + "license": "MIT", + "author": "Changesets Contributors", + "contributors": [ + "Ben Conolly", + "Mitchell Hamilton", + "Mateusz Burzyński (https://github.com/Andarist)" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/changesets/changesets.git", + "directory": "packages/cli" + }, "bin": { "changeset": "bin.js" }, - "repository": "https://github.com/changesets/changesets/tree/main/packages/cli", "files": [ "default-files", "dist", @@ -13,94 +25,48 @@ "changelog", "commit" ], - "main": "dist/changesets-cli.cjs.js", - "module": "dist/changesets-cli.esm.js", + "type": "module", "exports": { - ".": { - "types": { - "import": "./dist/changesets-cli.cjs.mjs", - "default": "./dist/changesets-cli.cjs.js" - }, - "module": "./dist/changesets-cli.esm.js", - "import": "./dist/changesets-cli.cjs.mjs", - "default": "./dist/changesets-cli.cjs.js" - }, - "./changelog": { - "types": { - "import": "./changelog/dist/changesets-cli-changelog.cjs.mjs", - "default": "./changelog/dist/changesets-cli-changelog.cjs.js" - }, - "module": "./changelog/dist/changesets-cli-changelog.esm.js", - "import": "./changelog/dist/changesets-cli-changelog.cjs.mjs", - "default": "./changelog/dist/changesets-cli-changelog.cjs.js" - }, - "./commit": { - "types": { - "import": "./commit/dist/changesets-cli-commit.cjs.mjs", - "default": "./commit/dist/changesets-cli-commit.cjs.js" - }, - "module": "./commit/dist/changesets-cli-commit.esm.js", - "import": "./commit/dist/changesets-cli-commit.cjs.mjs", - "default": "./commit/dist/changesets-cli-commit.cjs.js" - }, + ".": "./dist/index.mjs", + "./changelog": "./dist/changelog.mjs", + "./commit": "./dist/commit/index.mjs", "./package.json": "./package.json", "./bin.js": "./bin.js" }, - "author": "Changesets Contributors", - "contributors": [ - "Ben Conolly", - "Mitchell Hamilton", - "Mateusz Burzyński (https://github.com/Andarist)" - ], - "preconstruct": { - "entrypoints": [ - "./index.ts", - "./changelog.ts", - "./commit/index.ts" - ], - "exports": { - "extra": { - "./bin.js": "./bin.js" - } - } - }, - "license": "MIT", "dependencies": { - "@changesets/apply-release-plan": "^7.0.12", - "@changesets/assemble-release-plan": "^6.0.9", - "@changesets/changelog-git": "^0.2.1", - "@changesets/config": "^3.1.1", - "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", - "@changesets/get-release-plan": "^4.0.13", - "@changesets/git": "^3.0.4", - "@changesets/logger": "^0.1.1", - "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.5", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@changesets/write": "^0.4.0", - "@inquirer/external-editor": "^1.0.0", - "@manypkg/get-packages": "^1.1.3", - "ansi-colors": "^4.1.3", - "ci-info": "^3.7.0", - "enquirer": "^2.4.1", - "fs-extra": "^7.0.1", - "mri": "^1.2.0", - "p-limit": "^2.2.0", - "package-manager-detector": "^0.2.0", - "picocolors": "^1.1.0", - "resolve-from": "^5.0.0", - "semver": "^7.5.3", - "spawndamnit": "^3.0.1", - "term-size": "^2.1.0" + "@changesets/apply-release-plan": "workspace:^", + "@changesets/assemble-release-plan": "workspace:^", + "@changesets/changelog-git": "workspace:^", + "@changesets/config": "workspace:^", + "@changesets/errors": "workspace:^", + "@changesets/get-dependents-graph": "workspace:^", + "@changesets/git": "workspace:^", + "@changesets/pre": "workspace:^", + "@changesets/read": "workspace:^", + "@changesets/should-skip-package": "workspace:^", + "@changesets/types": "workspace:^", + "@changesets/write": "workspace:^", + "@clack/prompts": "^1.5.1", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.0", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.2.4" }, "devDependencies": { - "@changesets/parse": "*", - "@changesets/test-utils": "*", - "@types/semver": "^7.5.0", - "human-id": "^4.1.1", - "outdent": "^0.5.0", - "strip-ansi": "^5.2.0" + "@changesets/color": "workspace:^", + "@changesets/test-utils": "workspace:*", + "@types/semver": "^7.7.1", + "human-id": "^4.2.0", + "outdent": "^0.8.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" } } diff --git a/packages/cli/src/changelog.ts b/packages/cli/src/changelog.ts index 0a80c80ed..bd20ac76e 100644 --- a/packages/cli/src/changelog.ts +++ b/packages/cli/src/changelog.ts @@ -1 +1,2 @@ +// eslint-disable-next-line import-lite/no-default-export export { default } from "@changesets/changelog-git"; diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts new file mode 100644 index 000000000..a7ac65ce7 --- /dev/null +++ b/packages/cli/src/cli.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, test, vi } from "vitest"; +import { cli } from "./cli.ts"; +import { add } from "./commands/add/index.ts"; +import { init } from "./commands/init/index.ts"; +import { pack } from "./commands/pack/index.ts"; +import { pre } from "./commands/pre/index.ts"; +import { publishPlan } from "./commands/publish-plan/index.ts"; +import { publish } from "./commands/publish/index.ts"; +import { status } from "./commands/status/index.ts"; +import { tag } from "./commands/tag/index.ts"; +import { version } from "./commands/version/index.ts"; + +vi.mock("./commands/init/index.ts"); +vi.mock("./commands/add/index.ts"); +vi.mock("./commands/version/index.ts"); +vi.mock("./commands/publish/index.ts"); +vi.mock("./commands/publish-plan/index.ts"); +vi.mock("./commands/pack/index.ts"); +vi.mock("./commands/status/index.ts"); +vi.mock("./commands/tag/index.ts"); +vi.mock("./commands/pre/index.ts"); + +interface CommandTest { + command: string; + fn: (...args: any[]) => unknown; + cases: CommandCase[]; +} + +interface CommandCase { + args: string[]; + options: Record; +} + +const tests: CommandTest[] = [ + { + command: "init", + fn: init, + cases: [ + { + args: [], + options: {}, + }, + ], + }, + { + command: "add", + fn: add, + cases: [ + { + args: [], + options: {}, + }, + { + args: ["--empty", "--open", "--since", "main", "-m", "hello"], + options: { + empty: true, + open: true, + since: "main", + message: "hello", + }, + }, + { + args: ["--since", "main", "--since", "next"], + options: { + since: "next", + }, + }, + ], + }, + { + command: "version", + fn: version, + cases: [ + { + args: [], + options: {}, + }, + { + args: ["--ignore", "pkg-a", "--ignore", "pkg-b"], + options: { + ignore: ["pkg-a", "pkg-b"], + }, + }, + { + args: ["--snapshot"], + options: { + snapshot: true, + }, + }, + { + args: [ + "--snapshot", + "pr-123", + "--snapshot-prerelease-template", + "{tag}-{commit}", + ], + options: { + snapshot: "pr-123", + snapshotPrereleaseTemplate: "{tag}-{commit}", + }, + }, + ], + }, + { + command: "publish", + fn: publish, + cases: [ + { + args: [], + options: { gitTag: true }, + }, + { + args: ["--no-git-tag"], + options: { gitTag: false }, + }, + { + args: ["--otp", "123456", "--tag", "beta", "--git-tag"], + options: { + otp: "123456", + tag: "beta", + gitTag: true, + }, + }, + { + args: ["--from-pack-dir", ".packed"], + options: { + fromPackDir: ".packed", + gitTag: true, + }, + }, + ], + }, + { + command: "publish-plan", + fn: publishPlan, + cases: [ + { + args: [], + options: {}, + }, + { + args: ["--output", "publish-plan.json"], + options: { + output: "publish-plan.json", + }, + }, + ], + }, + { + command: "pack", + fn: pack, + cases: [ + { + args: ["--out-dir", ".packed"], + options: { + outDir: ".packed", + }, + }, + { + args: [ + "--from-publish-plan", + "publish-plan.json", + "--out-dir", + ".packed", + ], + options: { + fromPublishPlan: "publish-plan.json", + outDir: ".packed", + }, + }, + ], + }, + { + command: "status", + fn: status, + cases: [ + { + args: [], + options: {}, + }, + { + args: ["--since", "main", "--verbose", "--output", "status.json"], + options: { + since: "main", + verbose: true, + output: "status.json", + }, + }, + { + args: ["-v", "-o", "status.json"], + options: { + verbose: true, + output: "status.json", + }, + }, + ], + }, + { + command: "tag", + fn: tag, + cases: [ + { + args: [], + options: {}, + }, + ], + }, + { + command: "pre", + fn: pre, + cases: [ + { + args: ["enter", "beta"], + options: { + command: "enter", + tag: "beta", + }, + }, + ], + }, + { + command: "pre", + fn: pre, + cases: [ + { + args: ["exit"], + options: { + command: "exit", + }, + }, + ], + }, +]; + +for (const { command, fn, cases } of tests) { + describe(`changeset ${command}`, () => { + for (const { args, options } of cases) { + test(`${args.join(" ") || ""}`, async () => { + vi.clearAllMocks(); + cli.parse(["node", "changeset", command, ...args], { run: false }); + await cli.runMatchedCommand(); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith(options); + }); + } + }); +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 000000000..57f20ef87 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,179 @@ +// this json import requires that the package is built _after_ bumping versions before publishing +import manifest from "@changesets/cli/package.json" with { type: "json" }; +import c from "@changesets/color"; +import { log } from "@clack/prompts"; +import { cac } from "cac"; +import { ExitError } from "../../errors/src/index.ts"; + +export const cli = cac("changeset"); + +cli.version(manifest.version); +cli.help((sections) => { + // Show nicer help message title + sections[0] = { body: `🦋 changeset v${manifest.version}` }; +}); + +// Simplify the version output compared to the default +cli.globalCommand.outputVersion = () => console.info(manifest.version); + +function normalizeOptions( + options: Record, + { array }: { array?: string[] } = {}, +) { + // Do not allow positional arguments in options + delete options["--"]; + + for (const key in options) { + // Remove aliases + if (options[key] == null || key.length === 1) { + delete options[key]; + continue; + } + + // If the flag is expected to be an array, ensure it's an array. + if (array?.includes(key)) { + const v = options[key]; + options[key] = Array.isArray(v) ? v.map(String) : [String(v)]; + } + // If a flag is passed multiple times (becoming an array), only take the last value. + else if (Array.isArray(options[key])) { + options[key] = options[key].at(-1); + } + + // Do not be smart and auto cast for number-only strings, keep them as strings. + // If we need some flags as numbers in the future, add a new `number` option like `array`. + if (typeof options[key] === "number") { + options[key] = String(options[key]); + } + } +} + +cli + .command("init", "Initialize a new changesets setup") + .action(async (options) => { + normalizeOptions(options); + const { init } = await import("./commands/init/index.ts"); + await init(options); + }); + +cli + .command("add", "Add a new changeset (default)") + .usage("[command] [options]") + .example(" $ changeset -m 'Description'") + .example(" $ changeset --open --since main") + .alias("!") // special alias for default command + .option("--empty", "Add an empty changeset") + .option("--open", "Open the changeset in the editor after creating it") + .option( + "--since ", + "Detect changed packages since the provided git ref", + ) + .option("-m, --message ", "Directly provide a message to the changeset") + .action(async (options) => { + normalizeOptions(options); + const { add } = await import("./commands/add/index.ts"); + await add(options); + }); + +cli + .command("version", "Version packages and create changelogs") + .example(" $ changeset version") + .example(" $ changeset version --snapshot 'pr#123'") + .option("--ignore ", "Packages to ignore") + .option("--snapshot [name]", "Create a snapshot prerelease") + .option( + "--snapshot-prerelease-template