├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc ├── CHANGES.md ├── LICENSE ├── README.md ├── bin └── cmd.js ├── lib ├── changelog.js ├── changes.js ├── changes.test.js ├── footer.js ├── footer.test.js ├── github.js ├── github.test.js ├── init.js └── init.test.js ├── package-lock.json ├── package.json └── test └── hooks.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: ['16.x', '18.x', '20.x'] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Cache modules 26 | uses: actions/cache@v1 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.OS }}-node-${{ hashFiles('package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.OS }}-node- 32 | ${{ runner.OS }}- 33 | - name: Install 34 | run: npm ci 35 | - name: Lint 36 | run: npm run lint 37 | - name: Prettier 38 | run: npm run prettier:check 39 | - name: Test 40 | run: npm test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | TZ=UTC . "$(dirname "$0")/_/husky.sh" 3 | 4 | node_modules/.bin/lint-staged --relative 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const withRelatedTests = require('@studio/related-tests'); 4 | 5 | module.exports = { 6 | '*.js': ['eslint --fix', withRelatedTests('mocha')], 7 | '*.{js,json,md}': 'prettier --write' 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | /CHANGES.md 3 | /.* 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 3.0.0 4 | 5 | - 💥 [`c496adf`](https://github.com/javascript-studio/studio-changes/commit/c496adf4ea0fae76a852209e2931d2e7b444fae9) 6 | Drop node 12 and 14, support node 18 and 20 7 | - 🍏 [`1a13f3c`](https://github.com/javascript-studio/studio-changes/commit/1a13f3c063cb7f3faa12ace933c357b5190e4b18) 8 | Add support for `--workspace` flag 9 | - 🍏 [`5fdce3b`](https://github.com/javascript-studio/studio-changes/commit/5fdce3b4a73dc13362f497b46767558ea43542a6) 10 | Add support for `--dir` to filter git logs 11 | - ✨ [`777616b`](https://github.com/javascript-studio/studio-changes/commit/777616be2b889812e74853c86a0217b6c2c57c9d) 12 | Setup related tests for lint-staged 13 | - ✨ [`98e308a`](https://github.com/javascript-studio/studio-changes/commit/98e308aa5d4cb560e5f5cfeafd9509f07e7a6a3c) 14 | Move tests next to implementation 15 | - ✨ [`71130a9`](https://github.com/javascript-studio/studio-changes/commit/71130a948a7f6d4733fae99495b92897d4363dbc) 16 | Upgrade eslint-config and eslint 17 | - ✨ [`aa66b78`](https://github.com/javascript-studio/studio-changes/commit/aa66b7820fd4787b687b9a9670b18f6f0a9b2d35) 18 | Update Studio JSON Request 19 | - ✨ [`133fdd3`](https://github.com/javascript-studio/studio-changes/commit/133fdd31a9b29a544024cb69df933e299014a913) 20 | Upgrade hosted-git-info 21 | - ✨ [`0b70b37`](https://github.com/javascript-studio/studio-changes/commit/0b70b37b6c4a3c4a443000112e201aaeb864352d) 22 | Update minimist 23 | - ✨ [`2e1b252`](https://github.com/javascript-studio/studio-changes/commit/2e1b25293c8998517d5a9f752c7b614db7ae58a5) 24 | Update detect-indent 25 | - ✨ [`8c43dba`](https://github.com/javascript-studio/studio-changes/commit/8c43dba59f8d5204dd6e02b113fa7ee19d5ba6ef) 26 | Upgrade husky and lint-staged 27 | - ✨ [`96061cd`](https://github.com/javascript-studio/studio-changes/commit/96061cd384f87fe42cec5eb037179097b4fe368e) 28 | Upgrade prettier to v3 and add scripts 29 | - ✨ [`05d92b7`](https://github.com/javascript-studio/studio-changes/commit/05d92b70417120d834c64b97ebc0ed3ac5ac519e) 30 | Upgrade mocha 31 | - ✨ [`2bcfcf6`](https://github.com/javascript-studio/studio-changes/commit/2bcfcf653393c7ae5af34fb0f0b79a78e513af41) 32 | Upgrade referee-sinon 33 | - 🛡️ [`45a62ad`](https://github.com/javascript-studio/studio-changes/commit/45a62ad28f2249a2424f54310d7a1827177e44a7) 34 | Bump semver-regex from 3.1.3 to 3.1.4 (dependabot[bot]) 35 | - 🛡️ [`d0e261e`](https://github.com/javascript-studio/studio-changes/commit/d0e261e3e2fd2acd51da0b67a88b1ff7b1a34654) 36 | Bump semver-regex from 3.1.2 to 3.1.3 (dependabot[bot]) 37 | - 🛡️ [`5770dc9`](https://github.com/javascript-studio/studio-changes/commit/5770dc9e7c4d00bc720ac11de7f5e2705685cfbb) 38 | Bump path-parse from 1.0.6 to 1.0.7 (dependabot[bot]) 39 | - 🛡️ [`2ddd579`](https://github.com/javascript-studio/studio-changes/commit/2ddd579242a87a04fa345271e05f5e0b82fb109d) 40 | Bump ansi-regex from 3.0.0 to 3.0.1 (dependabot[bot]) 41 | - 🛡️ [`d1f8a3a`](https://github.com/javascript-studio/studio-changes/commit/d1f8a3a98b0bd5389351151e704a29cd1de8eb59) 42 | Bump minimist from 1.2.5 to 1.2.6 (dependabot[bot]) 43 | - 📚 [`47f21dd`](https://github.com/javascript-studio/studio-changes/commit/47f21dd8831c05b32f110a9f888160a2a9ec286c) 44 | Fix build badge 45 | 46 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2023-12-22._ 47 | 48 | ## 2.2.0 49 | 50 | - 🍏 [`bb47350`](https://github.com/javascript-studio/studio-changes/commit/bb4735035879f2d3084684e9efef20bce43af9e9) 51 | Publish to npm with public access when package is scoped and public (Frederik Ring) 52 | - ✨ [`4483f04`](https://github.com/javascript-studio/studio-changes/commit/4483f045088f7e4479eade163505c65d233ca62b) 53 | Dedupe and update dependencies 54 | - ✨ [`d629420`](https://github.com/javascript-studio/studio-changes/commit/d6294208c07d67fe31595f31d344a609781e3410) 55 | Use npm 7 56 | - ✨ [`0e7d6b2`](https://github.com/javascript-studio/studio-changes/commit/0e7d6b2c61c1aa195161197ff0c7c942aa2cdd11) 57 | Update dev tools 58 | 59 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2021-04-22._ 60 | 61 | ## 2.1.0 62 | 63 | - 🍏 [`0c1ef18`](https://github.com/javascript-studio/studio-changes/commit/0c1ef1832f139fd8655468811c36bb53245463db) 64 | Read user name and email from git config 65 | - 🐛 [`3c73ea7`](https://github.com/javascript-studio/studio-changes/commit/3c73ea7c24deb5aef3e904f40e538b3518efb69f) 66 | Fix reading git config 67 | - 🛡 [`c95c939`](https://github.com/javascript-studio/studio-changes/commit/c95c939350902a0388b3049622f63fd25b872b57) 68 | Bump lodash from 4.17.15 to 4.17.19 (dependabot[bot]) 69 | - 🛡 [`1c3987e`](https://github.com/javascript-studio/studio-changes/commit/1c3987eaa603765bf46edd21282989cdf5a6ea70) 70 | Bump minimist from 1.2.2 to 1.2.3 (dependabot[bot]) 71 | - 📚 [`46d2338`](https://github.com/javascript-studio/studio-changes/commit/46d2338fef9e971932a904347e6defa9749268bd) 72 | Remove dependabot details from changelog 73 | - 📚 [`908b88f`](https://github.com/javascript-studio/studio-changes/commit/908b88fd5b32c16524911f372d896cb09bb5b204) 74 | Improve README 75 | - 📚 [`e1e7800`](https://github.com/javascript-studio/studio-changes/commit/e1e78001888ef682aa931f0fec417c98857e376d) 76 | Switch build status badge to GitHub action 77 | - ✨ [`3d20343`](https://github.com/javascript-studio/studio-changes/commit/3d20343fa4fb4583113e40b022ab8daa6711a970) 78 | Move footer generation to own file 79 | - ✨ [`0eed36c`](https://github.com/javascript-studio/studio-changes/commit/0eed36c7d2bd414bce3c827188afa198ca90c0f8) 80 | Use async / await 81 | - ✨ [`b37478d`](https://github.com/javascript-studio/studio-changes/commit/b37478d728ec9376c515f5584d021a56f5517b5c) 82 | Use husky 4 83 | - ✨ [`8af53c9`](https://github.com/javascript-studio/studio-changes/commit/8af53c96376fd840f57a772f2f8a463f1fdc159a) 84 | Run prettier check in lint script 85 | - ✨ [`0aa741f`](https://github.com/javascript-studio/studio-changes/commit/0aa741f6d3e62a2ae5d9de37d9076df94b0e2ba2) 86 | Replace travis with github actions 87 | - ✨ [`ebcd050`](https://github.com/javascript-studio/studio-changes/commit/ebcd050e56bf5d21f497c29dc8509be605721b2d) 88 | Setup husky and lint-staged 89 | - ✨ [`623dfa0`](https://github.com/javascript-studio/studio-changes/commit/623dfa0575ea751fd5367f3783e3b4b99bdc87f2) 90 | Use prettier 91 | - ✨ [`93b16c0`](https://github.com/javascript-studio/studio-changes/commit/93b16c02bbbe4eb947406637a43253c5fe2d87e4) 92 | Upgrade eslint-config to latest 93 | - ✨ [`ec6a0f2`](https://github.com/javascript-studio/studio-changes/commit/ec6a0f2da9af4cde4b85b36c63fba6e2b5e2af11) 94 | Upgrades (#36) 95 | 96 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2021-02-14._ 97 | 98 | ## 2.0.1 99 | 100 | - 🛡 [`ce7b0a7`](https://github.com/javascript-studio/studio-changes/commit/ce7b0a7296544b48e1929e5fb24bd7cd29322612) 101 | Bump acorn from 6.2.1 to 6.4.1 (dependabot[bot]) 102 | - 🛡 [`b9efd0b`](https://github.com/javascript-studio/studio-changes/commit/b9efd0bc2113686e154d79bba9b99422ef565f5d) 103 | Bump minimist from 1.2.0 to 1.2.2 (dependabot[bot]) 104 | 105 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2020-03-21._ 106 | 107 | ## 2.0.0 108 | 109 | - 💥 [`001a065`](https://github.com/javascript-studio/studio-changes/commit/001a0656b9f1a01f1f838a59e204caeebfdf3745) 110 | __BREAKING:__ Drop node 6 support, add node 12 111 | - 🍏 [`2faefe0`](https://github.com/javascript-studio/studio-changes/commit/2faefe0532f78a2896485272dbaccafaeeece8ee) 112 | Resolve "repository" field from `package.json` 113 | - 🐛 [`52d3ec8`](https://github.com/javascript-studio/studio-changes/commit/52d3ec85ebeb9a6229c9410bb565a0c584519739) 114 | `npm audit` 115 | - 🐛 [`c9a35d5`](https://github.com/javascript-studio/studio-changes/commit/c9a35d57c1877d51e5fb43517e9c4c7335cf1431) 116 | Chain all replace calls 117 | - 🐛 [`dd70300`](https://github.com/javascript-studio/studio-changes/commit/dd70300efd857db3f409be9f4b35a07a9acadb9f) 118 | npm audit 119 | - 🐛 [`4f52628`](https://github.com/javascript-studio/studio-changes/commit/4f526287518c5ba0321362b9548a61549045010f) 120 | Fix typo (Morgan Roderick) 121 | - 🐛 [`f8efb51`](https://github.com/javascript-studio/studio-changes/commit/f8efb51c93a5b3a44ec1da568ce7769d5c69fa26) 122 | Bump eslint-utils from 1.3.1 to 1.4.2 (dependabot[bot]) 123 | - 🐛 [`9d16e02`](https://github.com/javascript-studio/studio-changes/commit/9d16e0211028ef872cd64be3f998f8f5c0bd9758) 124 | Bump lodash from 4.17.10 to 4.17.15 (dependabot[bot]) 125 | - 🐛 [`c414ecc`](https://github.com/javascript-studio/studio-changes/commit/c414eccdb0851aa215cf1e5aec2085a0deb4e2c2) 126 | Bump js-yaml from 3.12.0 to 3.13.1 (dependabot[bot]) 127 | - ✨ [`6489a83`](https://github.com/javascript-studio/studio-changes/commit/6489a83f7736e77dc9f9ac706b815e3a2b78d7d2) 128 | Upgrade `@sinonjs/referee-sinon` to v5 129 | - ✨ [`14e421e`](https://github.com/javascript-studio/studio-changes/commit/14e421ed98e7f90cd0da50d000da59e2892a7a9e) 130 | Upgrade `mocha` to v6 131 | - ✨ [`e9a2d31`](https://github.com/javascript-studio/studio-changes/commit/e9a2d319ba9bf9aa7b1b31418f93a18b4d0f381f) 132 | Upgrade `eslint` to v6 133 | - 📚 [`35f5528`](https://github.com/javascript-studio/studio-changes/commit/35f5528554c6a1be55cd1fb0cc54a0b94963bf08) 134 | Change license badge 135 | 136 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2019-12-19._ 137 | 138 | ## 1.7.0 139 | 140 | A new `--footer` option can be used to generate a footer like the one below. 141 | 142 | - 🍏 [`25d84e3`](https://github.com/javascript-studio/studio-changes/commit/25d84e3473e7f79c7e8c1d20d8310cf8c3b42e9d) 143 | Generate a footer with `--footer` 144 | - ✨ [`2905ca8`](https://github.com/javascript-studio/studio-changes/commit/2905ca844df389974c5aa633587525260b81045e) 145 | Dogfood `--footer` 146 | - ✨ [`6a3cecf`](https://github.com/javascript-studio/studio-changes/commit/6a3cecf281fe009132c415501a8817f11a8c52ce) 147 | Refactor `changelog` out of `changes` 148 | - ✨ [`34c7ffb`](https://github.com/javascript-studio/studio-changes/commit/34c7ffbd0e0e81464932025cab5513aac6b1f94e) 149 | Refactor `changes.write` to accept a callback 150 | - 📚 [`1a2c5e1`](https://github.com/javascript-studio/studio-changes/commit/1a2c5e18cd734eacf7015338910dca16f9b27c3c) 151 | New readme header 152 | - 📚 [`fea44b8`](https://github.com/javascript-studio/studio-changes/commit/fea44b8bc3c8047a940e31ada751e5383aaf784d) 153 | Add travis config and readme badges 154 | 155 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2018-08-25._ 156 | 157 | ## 1.6.2 158 | 159 | - 🐛 [`5076735`](https://github.com/javascript-studio/studio-changes/commit/5076735e5a1f81172371438f5af57923bf0ac688) 160 | Fix author string parsing 161 | 162 | ## 1.6.1 163 | 164 | - 🐛 [`f9587e4`](https://github.com/javascript-studio/studio-changes/commit/f9587e402345b7a4beb57d262d1af10eacede5ec) 165 | Fix markdown rendering of lists with blockquotes 166 | > 167 | > Commit body messages used to be framed with blank lines without the 168 | > leading `>` which was only used before lines with text. With this 169 | > change, these empty lines also receive a leading `>`. Without this, the 170 | > rendered HTML would contain a `

` tag in each `

  • ` regardless of 171 | > whether it contained a blockquote or not. 172 | > 173 | 174 | ## 1.6.0 175 | 176 | - 🍏 [`5a7b81a`](https://github.com/javascript-studio/studio-changes/commit/5a7b81a40b87adc9566bbd9b869833eec352acfc) 177 | Make `--init` homepage aware 178 | > 179 | > When a "homepage" property is present in the `package.json`, the 180 | > `--commits` flag is added to the `changes` command. If `--commits URL` 181 | > is explicitly specified, that URL is used, regardless of whether a 182 | > homepage is configured. 183 | > 184 | - ✨ [`b63f97f`](https://github.com/javascript-studio/studio-changes/commit/b63f97ff143bf7b4238e88c29944ce7609b273b1) 185 | Use `assert.calledOnceWith` 186 | - ✨ [`26d18c2`](https://github.com/javascript-studio/studio-changes/commit/26d18c27384409d996954df34bbe60521d60dc63) 187 | Update `devDependencies` 188 | 189 | ## 1.5.2 190 | 191 | - 🐛 [`f7e5f73`](https://github.com/javascript-studio/studio-changes/commit/f7e5f73021c37e0eca02b8f02f334c0d181538d4) 192 | Fail `--commits` if homepage is missing 193 | - 🐛 [`37170e2`](https://github.com/javascript-studio/studio-changes/commit/37170e257df2da65e83c2bcaa3ea1e72358fe5a1) 194 | Fix indentation for multi-line lists in body 195 | - ✨ [`478f1ef`](https://github.com/javascript-studio/studio-changes/commit/478f1efc80ca80c12608f87199af1ead16b7fd81) 196 | Upgrade Referee + Sinon and Eslint 197 | 198 | ## 1.5.1 199 | 200 | - 🐛 [`eff93ab`](https://github.com/javascript-studio/studio-changes/commit/eff93ab669f64a4ea73b4e2a48cac218fc94e616) 201 | Wrap commit link text in back ticks 202 | 203 | ## 1.5.0 204 | 205 | - 🍏 [a8da440](https://github.com/javascript-studio/studio-changes/commit/a8da4404ca9ee546f9b27c3f65df25c683b1c21d) 206 | Add commit links with --commits 207 | - 🍏 [9c7d655](https://github.com/javascript-studio/studio-changes/commit/9c7d65560b33087ee7cd2adc88dee3a1a054901c) 208 | Use new `-c` option 209 | - 🐛 [a5e2d02](https://github.com/javascript-studio/studio-changes/commit/a5e2d029a82b56385746402a8f5b1485d3eede55) 210 | Keep body with multiple paragraphs together 211 | 212 | ## 1.4.2 213 | 214 | - 🐛 Replace editor module with Studio Editor to fix [#14] 215 | 216 | [#14]: https://github.com/javascript-studio/studio-changes/issues/14 217 | 218 | ## 1.4.1 219 | 220 | - 🐛 Support "author" object in `package.json` 221 | 222 | ## 1.4.0 223 | 224 | Pat Cavit made `changes` [lerna][] compatible by allowing to specify a custom 225 | tag name format. Use it like this: 226 | 227 | ``` 228 | $ changes --tag '${name}@${version}' 229 | ``` 230 | 231 | - 🍏 Custom git tag format support (Pat Cavit) 232 | > 233 | > Supports the parsed version from `CHANGES.md` as `${version}` along w/ 234 | > any other string-compatible key in `package.json`. 235 | > 236 | - 📚 Add `npx` invocation examples 237 | 238 | [lerna]: https://github.com/lerna/lerna 239 | 240 | ## 1.3.0 241 | 242 | Now you can generate the npm version lifecycle scripts with `changes` itself: 243 | 244 | ```bash 245 | $ node_modules/.bin/changes --init 246 | ``` 247 | 248 | Indentations are preserved, existing scripts will not be touched, and if a 249 | `version` script already exists, no changes are made. 250 | 251 | - 🍏 Add `--init` to generate lifecycle scripts 252 | - 🍏 Allow to combine `--init` and `--file` 253 | - ✨ Add `package-lock.json` 254 | 255 | ## 1.2.0 256 | 257 | - 🍏 Quote commit body (#10) 258 | > 259 | > Render commit bodies as markdown quotes to better group long commit 260 | > messages. 261 | > 262 | 263 | ## 1.1.0 264 | 265 | 🍏 Blade Barringer [added two command line options][pr6]: 266 | 267 | - `--file` or `-f` allows to configure the changelog file name. It defaults to 268 | `CHANGES.md` as before. 269 | - `--help` or `-h` displays a help message. 270 | 271 | 🍏 If the current version number is found in the changelog, the changes command 272 | exits with code 1. In addition, it will now also print any outstanding commits. 273 | With this you can preview the changes for the next release: 274 | 275 | ```bash 276 | $ node_modules/.bin/changes 277 | ``` 278 | 279 | 🐛 The message body is now indented with four spaces instead of two to make the 280 | paragraph part of the list item. The body is now also separated from the next 281 | list item by a blank line. 282 | 283 | [pr6]: https://github.com/javascript-studio/studio-changes/pull/6 284 | 285 | ## 1.0.5 286 | 287 | 🐛 When git is configured to convert `LF` to `CRLF` on Windows, the header 288 | detection didn't work. [This patch fixes the header detection][pr2] and uses 289 | the line terminator found in the header when generating newlines. 290 | 291 | [pr2]: https://github.com/javascript-studio/studio-changes/pull/2 292 | 293 | ## 1.0.4 294 | 295 | Improve project description and usage notes. 296 | 297 | ## 1.0.3 298 | 299 | 📣 This release open sources `@studio/changes`. It adds the MIT license and 300 | some meta data to the package. The documentation was enriched with an animated 301 | GIF, but no functional changes have been made. Happy releasing! 302 | 303 | ## 1.0.2 304 | 305 | Adding unit tests revealed several bugs, like actually reading the 306 | `package.json` file in the current directory instead of the one from this 307 | project. 308 | 309 | If a commit message has a non-empty body, it is now shown below the subject. 310 | 311 | ## 1.0.1 312 | 313 | With this patch, the previous version is taken from the `CHANGES.md` instead of 314 | using the `package.json` version. This makes the git log range work in case 315 | there has been a release before. Also improved the npm scripts documentation 316 | slightly. 317 | 318 | ## 1.0.0 319 | 320 | - Add pre- and postversion scripts 321 | - Inception 322 | 323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Maximilian Antoni 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | Studio Changes 3 |

    4 |

    5 | 📦 Generate a changelog as part of the npm version command 6 |

    7 |

    8 | 9 | npm Version 10 | 11 | 12 | SemVer 13 | 14 | 15 | Build Status 16 | 17 | 18 | License 19 | 20 |

    21 | 22 | ## Usage 23 | 24 | - Use `npm version [patch|minor|major]` to create a release 25 | - Your editor will open with a generated `CHANGES.md` file 26 | - When you're done writing the release notes, save and close the editor to 27 | continue 28 | - To abort the release, remove the heading with the new version number 29 | 30 | ## Install 31 | 32 | ```bash 33 | ❯ npm install @studio/changes --save-dev 34 | ``` 35 | 36 | ## Configure 37 | 38 | ```bash 39 | ❯ npx changes --init 40 | ``` 41 | 42 | This will add the following to your `package.json`: 43 | 44 | ```json 45 | { 46 | "scripts": { 47 | "preversion": "npm test", 48 | "version": "changes", 49 | "postversion": "git push --follow-tags && npm publish" 50 | } 51 | } 52 | ``` 53 | 54 | ## Options 55 | 56 | - `--help`, `-h`: Display a help message. 57 | - `--commits`, `-c`: Generate links to commits using the given URL as base. If 58 | no URL is given it defaults to `${homepage}/commit` using the homepage 59 | configured in the `package.json`. 60 | - `--footer`: Generate a footer with the git author and release date. The 61 | author name is taken from `$GIT_AUTHOR_NAME` and `$GIT_AUTHOR_EMAIL` is used 62 | to find the authors GitHub profile page. 63 | - `--file`, `-f`: Specify the name of the changelog file. Defaults to 64 | `CHANGES.md`. 65 | - `--init`: Add version lifecycle scripts to `package.json`. Can be combined 66 | with `--file` and `--commits` to configure the `changes` invocation. 67 | - `--tag`: Use a custom git tag, supports simple replacement of `package.json` 68 | fields. Defaults to `v${version}`. 69 | - `--dir`: Passes the given dir to `git log -- ` to filter the commit 70 | history by this directory. 71 | - `--workspace`: Is a convenience flag which sets `--dir` to the current 72 | directory name and `--tag` to `${dir}/v${version}`. 73 | 74 | Configure your preferred editor with the `$EDITOR` environment variable. 75 | 76 | ## Preview next release 77 | 78 | Preview the release notes for the next release by running: 79 | 80 | ```bash 81 | ❯ npx changes 82 | ``` 83 | 84 | ![](https://javascript.studio/assets/changes-1.0.gif) 85 | 86 | ## License 87 | 88 | MIT 89 | 90 |

    Made with ❤️ on 🌍

    91 | 92 | [1]: https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b 93 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) Maximilian Antoni 4 | * 5 | * @license MIT 6 | */ 7 | 'use strict'; 8 | 9 | const editor = require('@studio/editor'); 10 | const changes = require('..'); 11 | 12 | const argv = require('minimist')(process.argv.slice(2), { 13 | alias: { 14 | commits: 'c', 15 | file: 'f', 16 | tag: 't', 17 | dir: 'd', 18 | workspace: 'w', 19 | help: 'h' 20 | } 21 | }); 22 | 23 | if (argv.help) { 24 | console.log(`Usage: changes [options] 25 | 26 | Options: 27 | --init Add version lifecycle scripts to package.json. 28 | -c, --commits [URL] Generate links to commits using the given URL as base. 29 | If no URL is given it defaults to "\${homepage}/commit". 30 | --footer Generate a footer with the git author and release date. 31 | -f, --file [FILENAME] Specify the name of the changelog file. Defaults to CHANGES.md. 32 | -t, --tag [FORMAT] Specify a custom git tag format to use. Defaults to "v\${version}". 33 | -d, --dir [PATH] Specify a directory to filter git log entries. 34 | -w, --workspace Convenience flag to set --dir to the current directory name and --tag to "\${dir} v\${version}" 35 | -h, --help Display this help message. 36 | `); 37 | process.exit(); 38 | } 39 | 40 | if (argv.init) { 41 | // eslint-disable-next-line n/global-require 42 | if (require('../lib/init')()) { 43 | process.exit(); 44 | } 45 | console.error('"version" script already exists'); 46 | process.exit(1); 47 | } 48 | 49 | const options = {}; 50 | if (argv.file) { 51 | options.changes_file = argv.file; 52 | } 53 | if (argv.tag) { 54 | options.tag_format = argv.tag; 55 | } 56 | if (argv.dir) { 57 | options.dir = argv.dir; 58 | } 59 | if (argv.commits) { 60 | options.commits = argv.commits; 61 | } 62 | if (argv.footer) { 63 | options.footer = argv.footer; 64 | } 65 | if (argv.workspace) { 66 | options.workspace = true; 67 | } 68 | 69 | // Write the commit history to the changes file 70 | changes 71 | .write(options) 72 | .then((state) => { 73 | // Let the user edit the changes 74 | editor(state.changes_file, (code) => { 75 | if (code === 0) { 76 | // Add the changes file to git 77 | changes.add(state); 78 | } else { 79 | // Roll back 80 | changes.abort(state); 81 | } 82 | }); 83 | }) 84 | .catch((err) => { 85 | console.error(err); 86 | process.exit(1); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/changelog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const $ = require('child_process'); 9 | 10 | const VARIABLE_RE = /\$\{([^}]+)\}/g; 11 | 12 | function parseAuthor(author) { 13 | const m = author.match(/[<(]/); 14 | return m ? author.substring(0, m.index).trim() : author; 15 | } 16 | 17 | module.exports = function ({ log_range, dir, commits, newline, pkg }) { 18 | let flags = '--format="» '; 19 | if (commits) { 20 | commits = commits.replace(VARIABLE_RE, (match, key) => pkg[key]); 21 | flags += `[\\\`%h\\\`](${commits}/%H)« `; 22 | } 23 | flags += '%s (%an)%n%n%b" --no-merges'; 24 | if (dir) { 25 | flags += ` -- ${dir}`; 26 | } 27 | let changes; 28 | try { 29 | changes = $.execSync(`git log ${log_range} ${flags}`, { 30 | encoding: 'utf8' 31 | }); 32 | } catch (e) { 33 | process.exit(1); 34 | return null; 35 | } 36 | // Remove blanks (if no body) and indent body 37 | changes = changes 38 | .replace(/\n{3,}/g, '\n') 39 | // Indent body with quotes: 40 | .replace(/^([^»])/gm, ' > $1') 41 | // Remove trainling whitespace on blank quote lines 42 | .replace(/^ {4}> \n/gm, ' >\n') 43 | // Replace commit markers with dashes: 44 | .replace(/^»/gm, '-') 45 | // Replace newline markers with newlines: 46 | .replace(/«/gm, '\n') 47 | // Restore original newlines: 48 | .replace(/\n/gm, newline); 49 | 50 | // Only mention contributors 51 | const { author } = pkg; 52 | if (author) { 53 | const author_name = 54 | typeof author === 'object' ? author.name : parseAuthor(author); 55 | changes = changes.replace(new RegExp(` \\(${author_name}\\)$`, 'gm'), ''); 56 | } 57 | 58 | return changes; 59 | }; 60 | -------------------------------------------------------------------------------- /lib/changes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const $ = require('child_process'); 11 | const hostedGitInfo = require('hosted-git-info'); 12 | const changelog = require('./changelog'); 13 | const footer = require('./footer'); 14 | 15 | const CHANGES_HEADING = '# Changes'; 16 | const DEFAULT_CHANGES_FILE = 'CHANGES.md'; 17 | const DEFAULT_TAG_FORMAT = 'v${version}'; 18 | const DEFAULT_COMMIT_URL_FORMAT = '${homepage}/commit'; 19 | const VARIABLE_RE = /\$\{([^}]+)\}/g; 20 | 21 | function exists(changes, version) { 22 | const escaped_version = version.replace(/([.-])/g, '\\$1'); 23 | const regexp = new RegExp(`\r?\n## ${escaped_version}\r?\n`); 24 | return regexp.test(changes); 25 | } 26 | 27 | function buildTag(tag_format, version, pkg) { 28 | return tag_format.replace(VARIABLE_RE, (match, key) => 29 | key === 'version' ? version : pkg[key] 30 | ); 31 | } 32 | 33 | // Write the commit history to the changes file 34 | exports.write = async function (options = {}) { 35 | let tag_format = options.tag_format || DEFAULT_TAG_FORMAT; 36 | let dir = options.dir; 37 | if (options.workspace) { 38 | dir = path.basename(process.cwd()); 39 | tag_format = `${dir}/v\${version}`; 40 | } 41 | 42 | const changes_file = options.changes_file || DEFAULT_CHANGES_FILE; 43 | const package_json = fs.readFileSync('package.json', 'utf8'); 44 | const pkg = JSON.parse(package_json); 45 | const { version } = pkg; 46 | 47 | // Get previous file content 48 | let previous; 49 | let heading; 50 | let newline; 51 | try { 52 | previous = fs.readFileSync(changes_file, 'utf8'); 53 | const match = previous.match(new RegExp(`^${CHANGES_HEADING}(\r?\n){2}`)); 54 | if (!match) { 55 | console.error(`Unexpected ${changes_file} file header`); 56 | process.exit(1); 57 | return null; 58 | } 59 | heading = match[0]; 60 | newline = match[1]; 61 | } catch (e) { 62 | previous = heading = `${CHANGES_HEADING}\n\n`; 63 | newline = '\n'; 64 | } 65 | 66 | // Generate changes for this release 67 | const version_match = previous.match(/^## ([0-9a-z.-]+)$/m); 68 | let log_range = ''; 69 | if (version_match) { 70 | log_range = `${buildTag(tag_format, version_match[1], pkg)}..HEAD`; 71 | } 72 | 73 | let commits = options.commits; 74 | if (commits) { 75 | if (commits === true) { 76 | commits = DEFAULT_COMMIT_URL_FORMAT; 77 | if (pkg.repository && pkg.repository.type === 'git') { 78 | const info = hostedGitInfo.fromUrl(pkg.repository.url); 79 | if (!info) { 80 | console.error('Failed to parse "repository" from package.json\n'); 81 | process.exit(1); 82 | return null; 83 | } 84 | pkg.homepage = info.browse(); 85 | } 86 | if (!pkg.homepage) { 87 | console.error( 88 | '--commits option requires base URL, "repository" or ' + 89 | '"homepage" in package.json\n' 90 | ); 91 | process.exit(1); 92 | return null; 93 | } 94 | } 95 | } 96 | 97 | let changes = changelog({ 98 | log_range, 99 | dir, 100 | commits, 101 | newline, 102 | pkg 103 | }); 104 | 105 | if (options.footer) { 106 | const foot = await footer.generate(); 107 | changes += `${newline}${foot}${newline}`; 108 | } 109 | 110 | // Do not allow version to be added twice 111 | if (exists(previous, version)) { 112 | console.error(`Version ${version} is already in ${changes_file}\n`); 113 | if (changes) { 114 | console.error('# Changes for next release:\n'); 115 | console.error(changes); 116 | } 117 | process.exit(1); 118 | return null; 119 | } 120 | 121 | // Generate new changes 122 | let next = `${heading}## ${version}${newline}${newline}${changes}`; 123 | const remain = previous.substring(heading.length); 124 | if (remain) { 125 | next += `${newline}${remain}`; 126 | } 127 | fs.writeFileSync(changes_file, next); 128 | 129 | return { previous, changes_file }; 130 | }; 131 | 132 | // Roll back changes 133 | exports.abort = function (state) { 134 | fs.writeFileSync(state.changes_file, state.previous); 135 | process.exitCode = 1; 136 | }; 137 | 138 | // Add changes to git, unless the user removed the current version to abort 139 | exports.add = function (state) { 140 | const package_json = fs.readFileSync('package.json', 'utf8'); 141 | const { version } = JSON.parse(package_json); 142 | if (exists(fs.readFileSync(state.changes_file, 'utf8'), version)) { 143 | // Add changes file to git so that npm includes it in the release commit 144 | $.execSync(`git add ${state.changes_file}`); 145 | } else { 146 | exports.abort(state); 147 | } 148 | }; 149 | -------------------------------------------------------------------------------- /lib/changes.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const $ = require('child_process'); 6 | const { assert, refute, match, sinon } = require('@sinonjs/referee-sinon'); 7 | const footer = require('./footer'); 8 | const changes = require('./changes'); 9 | 10 | describe('lib/changes', () => { 11 | beforeEach(() => { 12 | sinon.stub(fs, 'readFileSync'); 13 | sinon.stub(fs, 'writeFileSync'); 14 | sinon.stub($, 'execSync'); 15 | sinon.stub(process, 'exit'); 16 | sinon.stub(console, 'error'); 17 | }); 18 | 19 | afterEach(() => { 20 | sinon.restore(); 21 | }); 22 | 23 | function packageJson(json) { 24 | fs.readFileSync.withArgs('package.json').returns( 25 | JSON.stringify( 26 | json || { 27 | name: '@studio/changes', 28 | version: '1.0.0', 29 | author: 'Studio ', 30 | homepage: 'https://github.com/javascript-studio/studio-changes' 31 | } 32 | ) 33 | ); 34 | } 35 | 36 | function missingChanges() { 37 | fs.readFileSync.withArgs('CHANGES.md').throws(new Error()); 38 | } 39 | 40 | function setChanges(str) { 41 | fs.readFileSync.withArgs('CHANGES.md').returns(str); 42 | } 43 | 44 | function setLog(log) { 45 | $.execSync.returns(log); 46 | } 47 | 48 | it('generates new changes file to default location', async () => { 49 | packageJson(); 50 | missingChanges(); 51 | setLog('» Inception (That Dude)\n\n\n'); 52 | 53 | const state = await changes.write(); 54 | 55 | assert.calledOnceWith( 56 | fs.writeFileSync, 57 | 'CHANGES.md', 58 | '# Changes\n\n## 1.0.0\n\n- Inception (That Dude)\n' 59 | ); 60 | assert.calledOnce($.execSync); 61 | assert.calledWithMatch($.execSync, 'git log --format='); 62 | assert.equals(state.changes_file, 'CHANGES.md'); 63 | }); 64 | 65 | it('generates new changes file to custom location', async () => { 66 | packageJson(); 67 | missingChanges(); 68 | setLog('» Inception (That Dude)\n\n\n'); 69 | 70 | const state = await changes.write({ changes_file: 'foo.txt' }); 71 | 72 | assert.calledOnceWith( 73 | fs.writeFileSync, 74 | 'foo.txt', 75 | '# Changes\n\n## 1.0.0\n\n- Inception (That Dude)\n' 76 | ); 77 | assert.equals(state.changes_file, 'foo.txt'); 78 | }); 79 | 80 | it('removes package author', async () => { 81 | packageJson(); 82 | missingChanges(); 83 | setLog('» Inception (Studio)\n\n\n'); 84 | 85 | await changes.write(); 86 | 87 | assert.calledOnceWith( 88 | fs.writeFileSync, 89 | 'CHANGES.md', 90 | '# Changes\n\n## 1.0.0\n\n- Inception\n' 91 | ); 92 | }); 93 | 94 | async function verifyAuthorRemoval(author) { 95 | packageJson({ name: '@studio/changes', version: '1.0.0', author }); 96 | missingChanges(); 97 | setLog('» Inception (Studio)\n\n\n'); 98 | 99 | await changes.write(); 100 | 101 | assert.calledOnceWith( 102 | fs.writeFileSync, 103 | 'CHANGES.md', 104 | '# Changes\n\n## 1.0.0\n\n- Inception\n' 105 | ); 106 | } 107 | 108 | it('removes package author (with homepage)', async () => { 109 | await verifyAuthorRemoval('Studio (https://javascript.studio)'); 110 | }); 111 | 112 | it('removes package author (without email or homepage)', async () => { 113 | await verifyAuthorRemoval('Studio'); 114 | }); 115 | 116 | it('removes package author (with email and homepage)', async () => { 117 | await verifyAuthorRemoval( 118 | 'Studio (https://javascript.studio)' 119 | ); 120 | }); 121 | 122 | it('removes package author (with homepage and email)', async () => { 123 | await verifyAuthorRemoval( 124 | 'Studio (https://javascript.studio) ' 125 | ); 126 | }); 127 | 128 | it('removes package author (with object)', async () => { 129 | await verifyAuthorRemoval({ 130 | name: 'Studio', 131 | email: 'support@javascript.studio' 132 | }); 133 | }); 134 | 135 | it('add commit log to existing changes file', async () => { 136 | packageJson(); 137 | const initial = '# Changes\n\n## 0.1.0\n\nSome foo.\n'; 138 | setChanges(initial); 139 | setLog('» Inception (Studio)\n\n\n'); 140 | 141 | const state = await changes.write(); 142 | 143 | assert.calledOnceWith( 144 | fs.writeFileSync, 145 | 'CHANGES.md', 146 | '# Changes\n\n## 1.0.0\n\n- Inception\n\n## 0.1.0\n\nSome foo.\n' 147 | ); 148 | assert.calledOnce($.execSync); 149 | assert.calledWithMatch($.execSync, 'git log v0.1.0..HEAD'); 150 | assert.equals(state.previous, initial); 151 | }); 152 | 153 | it('identifies previous commit with -beta suffix', async () => { 154 | packageJson(); 155 | setChanges('# Changes\n\n## 0.1.0-beta\n\nSome foo.\n'); 156 | setLog('» Inception (Studio)\n\n\n'); 157 | 158 | await changes.write(); 159 | 160 | assert.calledWithMatch($.execSync, 'git log v0.1.0-beta..HEAD'); 161 | }); 162 | 163 | it('adds `-- my-module` to git log command', async () => { 164 | packageJson(); 165 | missingChanges(); 166 | setLog('» Inception\n\n\n'); 167 | 168 | await changes.write({ dir: 'my-module' }); 169 | 170 | assert.calledWith($.execSync, match(/ -- my-module$/)); 171 | }); 172 | 173 | it('adds body indented on new line', async () => { 174 | packageJson(); 175 | missingChanges(); 176 | setLog( 177 | '» Inception (Studio)\n\nFoo Bar Doo\n\n» Other (Dude)\n\n\n' + 178 | '» Third (Person)\n\nDoes\nstuff\n\n' 179 | ); 180 | 181 | await changes.write(); 182 | 183 | assert.calledOnceWith( 184 | fs.writeFileSync, 185 | 'CHANGES.md', 186 | '# Changes\n\n## 1.0.0\n\n' + 187 | '- Inception\n >\n > Foo Bar Doo\n >\n' + 188 | '- Other (Dude)\n' + 189 | '- Third (Person)\n >\n > Does\n > stuff\n >\n' 190 | ); 191 | }); 192 | 193 | it('keeps body with two paragraphs together', async () => { 194 | packageJson(); 195 | missingChanges(); 196 | setLog('» Inception (Studio)\n\nFoo\n\nBar\n\n'); 197 | 198 | await changes.write(); 199 | 200 | assert.calledOnceWith( 201 | fs.writeFileSync, 202 | 'CHANGES.md', 203 | '# Changes\n\n## 1.0.0\n\n- Inception\n' + 204 | ' >\n > Foo\n >\n > Bar\n >\n' 205 | ); 206 | }); 207 | 208 | it('keeps body with three paragraphs together', async () => { 209 | packageJson(); 210 | missingChanges(); 211 | setLog('» Inception (Studio)\n\nFoo\n\nBar\n\nDoo\n\n'); 212 | 213 | await changes.write(); 214 | 215 | assert.calledOnceWith( 216 | fs.writeFileSync, 217 | 'CHANGES.md', 218 | '# Changes\n\n## 1.0.0\n\n- Inception\n' + 219 | ' >\n > Foo\n >\n > Bar\n >\n > Doo\n >\n' 220 | ); 221 | }); 222 | 223 | it('properly indents lists', async () => { 224 | packageJson(); 225 | missingChanges(); 226 | setLog('» Inception (Studio)\n\n- Foo\n- Bar\n- Doo\n\n'); 227 | 228 | await changes.write(); 229 | 230 | assert.calledOnceWith( 231 | fs.writeFileSync, 232 | 'CHANGES.md', 233 | '# Changes\n\n## 1.0.0\n\n- Inception\n' + 234 | ' >\n > - Foo\n > - Bar\n > - Doo\n >\n' 235 | ); 236 | }); 237 | 238 | it('properly indents list with multiline entry', async () => { 239 | packageJson(); 240 | missingChanges(); 241 | setLog('» Inception (Studio)\n\n- Foo\n next line\n- Bar\n\n'); 242 | 243 | await changes.write(); 244 | 245 | assert.calledOnceWith( 246 | fs.writeFileSync, 247 | 'CHANGES.md', 248 | '# Changes\n\n## 1.0.0\n\n- Inception\n' + 249 | ' >\n > - Foo\n > next line\n > - Bar\n >\n' 250 | ); 251 | }); 252 | 253 | it('fails if changes file has not the right format', async () => { 254 | packageJson(); 255 | setChanges('# Something else\n\n## 1.0.0\n\nFoo'); 256 | 257 | await changes.write(); 258 | 259 | assert.calledOnceWith(console.error, 'Unexpected CHANGES.md file header'); 260 | assert.calledOnceWith(process.exit, 1); 261 | }); 262 | 263 | it('fails if version is already in changes file', async () => { 264 | packageJson(); 265 | setChanges('# Changes\n\n## 1.0.0\n\nFoo'); 266 | setLog('foo'); 267 | 268 | await changes.write(); 269 | 270 | assert.calledWith( 271 | console.error, 272 | 'Version 1.0.0 is already in CHANGES.md\n' 273 | ); 274 | assert.calledOnceWith(process.exit, 1); 275 | }); 276 | 277 | it('shows outstanding changes if version is already in changes file', async () => { 278 | packageJson(); 279 | setChanges('# Changes\n\n## 1.0.0\n\nFoo'); 280 | setLog('» Up next (Studio)\n\n\n'); 281 | 282 | await changes.write(); 283 | 284 | assert.calledWith(console.error, '# Changes for next release:\n'); 285 | assert.calledWith(console.error, '- Up next\n'); 286 | }); 287 | 288 | it('does not show outstanding changes if no new commits where found', async () => { 289 | packageJson(); 290 | setChanges('# Changes\n\n## 1.0.0\n\nFoo'); 291 | setLog(''); 292 | 293 | await changes.write(); 294 | 295 | assert.calledWith( 296 | console.error, 297 | 'Version 1.0.0 is already in CHANGES.md\n' 298 | ); 299 | refute.calledWith(console.error, '# Changes for next release:\n'); 300 | }); 301 | 302 | it('works if changes file was checked out with CRLF', async () => { 303 | packageJson(); 304 | const initial = '# Changes\r\n\r\n## 0.0.1\r\n\r\n- Inception\r\n'; 305 | setChanges(initial); 306 | setLog('» JavaScript (Studio)\n\nWhat else?\n\n\n'); 307 | 308 | const state = await changes.write(); 309 | 310 | assert.calledOnceWith( 311 | fs.writeFileSync, 312 | 'CHANGES.md', 313 | '# Changes\r\n\r\n' + 314 | '## 1.0.0\r\n\r\n- JavaScript\r\n >\r\n > What else?\r\n\r\n' + 315 | '## 0.0.1\r\n\r\n- Inception\r\n' 316 | ); 317 | assert.calledOnce($.execSync); 318 | assert.calledWithMatch($.execSync, 'git log v0.0.1..HEAD'); 319 | assert.equals(state.previous, initial); 320 | }); 321 | 322 | it('fails if version is already in changes file with CRLF', async () => { 323 | packageJson(); 324 | setChanges('# Changes\r\n\r\n## 1.0.0\r\n\r\nFoo'); 325 | setLog('foo'); 326 | 327 | await changes.write(); 328 | 329 | assert.calledWith( 330 | console.error, 331 | 'Version 1.0.0 is already in CHANGES.md\n' 332 | ); 333 | assert.calledOnceWith(process.exit, 1); 334 | }); 335 | 336 | it('supports custom tag formats when updating a file', async () => { 337 | packageJson(); 338 | const initial = '# Changes\n\n## 0.1.0\n\nSome foo.\n'; 339 | setChanges(initial); 340 | setLog('» Inception (Studio)\n\n\n'); 341 | 342 | const state = await changes.write({ 343 | tag_format: '${name}@${version}' 344 | }); 345 | 346 | assert.calledOnceWith( 347 | fs.writeFileSync, 348 | 'CHANGES.md', 349 | '# Changes\n\n## 1.0.0\n\n- Inception\n\n## 0.1.0\n\nSome foo.\n' 350 | ); 351 | assert.calledOnce($.execSync); 352 | assert.calledWithMatch($.execSync, 'git log @studio/changes@0.1.0..HEAD'); 353 | assert.equals(state.previous, initial); 354 | }); 355 | 356 | it('uses current directory as the workspace convention', async () => { 357 | packageJson(); 358 | const initial = '# Changes\n\n## 0.1.0\n\nSome foo.\n'; 359 | setChanges(initial); 360 | setLog('» Inception (Studio)\n\n\n'); 361 | 362 | const state = await changes.write({ 363 | workspace: true 364 | }); 365 | 366 | assert.calledOnceWith( 367 | fs.writeFileSync, 368 | 'CHANGES.md', 369 | '# Changes\n\n## 1.0.0\n\n- Inception\n\n## 0.1.0\n\nSome foo.\n' 370 | ); 371 | assert.calledOnce($.execSync); 372 | const current_dir = path.basename(process.cwd()); 373 | assert.calledWithMatch($.execSync, `git log ${current_dir}/v0.1.0..HEAD`); 374 | assert.calledWithMatch($.execSync, new RegExp(`-- ${current_dir}$`)); 375 | assert.equals(state.previous, initial); 376 | }); 377 | 378 | it('adds commits with specified base', async () => { 379 | packageJson(); 380 | missingChanges(); 381 | setLog( 382 | '» [`cbac1d0`](https://javascript.studio/commit/' + 383 | 'cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)« Message (Author)\n\n\n' 384 | ); 385 | 386 | await changes.write({ 387 | commits: 'https://javascript.studio/commit' 388 | }); 389 | 390 | assert.calledOnceWith( 391 | fs.writeFileSync, 392 | 'CHANGES.md', 393 | '# Changes\n\n## 1.0.0\n\n' + 394 | '- [`cbac1d0`](https://javascript.studio/commit/' + 395 | 'cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)\n Message (Author)\n' 396 | ); 397 | assert.calledWithMatch( 398 | $.execSync, 399 | 'git log --format="» [\\`%h\\`](https://javascript.studio/commit/%H)' + 400 | '« %s' 401 | ); 402 | }); 403 | 404 | it('adds commits with base from package.json homepage + /commit', async () => { 405 | packageJson(); 406 | missingChanges(); 407 | setLog( 408 | '» [`cbac1d0`](https://github.com/javascript-studio/studio-changes/' + 409 | 'commit/cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)«' + 410 | ' Message (Author)\n\n\n' 411 | ); 412 | 413 | await changes.write({ 414 | commits: true 415 | }); 416 | 417 | assert.calledOnceWith( 418 | fs.writeFileSync, 419 | 'CHANGES.md', 420 | '# Changes\n\n## 1.0.0\n\n' + 421 | '- [`cbac1d0`](https://github.com/javascript-studio/studio-changes/' + 422 | 'commit/cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)\n' + 423 | ' Message (Author)\n' 424 | ); 425 | assert.calledWithMatch( 426 | $.execSync, 427 | 'git log --format="» [\\`%h\\`](https://github.com/javascript-studio/' + 428 | 'studio-changes/commit/%H)« %s' 429 | ); 430 | }); 431 | 432 | it('resolves base from package.json "repository" field', async () => { 433 | packageJson({ 434 | name: '@studio/changes', 435 | version: '1.0.0', 436 | repository: { 437 | type: 'git', 438 | url: 'https://github.com/javascript-studio/studio-changes.git' 439 | } 440 | }); 441 | missingChanges(); 442 | setLog('» Test'); 443 | 444 | await changes.write({ 445 | commits: true 446 | }); 447 | 448 | assert.calledWithMatch( 449 | $.execSync, 450 | 'git log --format="» [\\`%h\\`](https://github.com/javascript-studio/' + 451 | 'studio-changes/commit/%H)« %s' 452 | ); 453 | }); 454 | 455 | it(`ignores package.json "repository" field and uses "homepage" instead if not 456 | type "git"`, async () => { 457 | packageJson({ 458 | name: '@studio/changes', 459 | version: '1.0.0', 460 | repository: { 461 | type: 'foo', 462 | url: 'https://github.com/mantoni/eslint_d.js.git' 463 | }, 464 | homepage: 'https://github.com/javascript-studio/studio-changes' 465 | }); 466 | missingChanges(); 467 | setLog('» Test'); 468 | 469 | await changes.write({ 470 | commits: true 471 | }); 472 | 473 | assert.calledWithMatch( 474 | $.execSync, 475 | 'git log --format="» [\\`%h\\`](https://github.com/javascript-studio/' + 476 | 'studio-changes/commit/%H)« %s' 477 | ); 478 | }); 479 | 480 | it('fails if repository info cannot be parsed', async () => { 481 | packageJson({ 482 | name: '@studio/changes', 483 | version: '1.0.0', 484 | repository: { 485 | type: 'git', 486 | url: 'https://foo.com/mantoni/eslint_d.js.git' 487 | } 488 | }); 489 | missingChanges(); 490 | setLog('» Test'); 491 | 492 | await changes.write({ 493 | commits: true 494 | }); 495 | 496 | assert.calledWith( 497 | console.error, 498 | 'Failed to parse "repository" from package.json\n' 499 | ); 500 | assert.calledOnceWith(process.exit, 1); 501 | }); 502 | 503 | it(`fails if --commits but missing "repository" and "homepage" in 504 | package.json`, async () => { 505 | packageJson({ 506 | name: '@studio/changes', 507 | version: '1.0.0' 508 | }); 509 | 510 | await changes.write({ 511 | commits: true 512 | }); 513 | 514 | assert.calledWith( 515 | console.error, 516 | '--commits option requires base URL, ' + 517 | '"repository" or "homepage" in package.json\n' 518 | ); 519 | assert.calledOnceWith(process.exit, 1); 520 | }); 521 | 522 | it('adds commits using base URL template', async () => { 523 | packageJson(); 524 | missingChanges(); 525 | setLog( 526 | '» [`cbac1d0`](https://github.com/javascript-studio/studio-changes/' + 527 | 'foo/cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)«' + 528 | ' Message (Author)\n\n\n' 529 | ); 530 | 531 | await changes.write({ 532 | commits: '${homepage}/foo' 533 | }); 534 | 535 | assert.calledOnceWith( 536 | fs.writeFileSync, 537 | 'CHANGES.md', 538 | '# Changes\n\n## 1.0.0\n\n' + 539 | '- [`cbac1d0`](https://github.com/javascript-studio/studio-changes/' + 540 | 'foo/cbac1d01d3e7c5d9ab1cf7cd9efee4cfc2988a85)\n' + 541 | ' Message (Author)\n' 542 | ); 543 | assert.calledWithMatch( 544 | $.execSync, 545 | 'git log --format="» [\\`%h\\`](https://github.com/javascript-studio/' + 546 | 'studio-changes/foo/%H)« %s' 547 | ); 548 | }); 549 | 550 | it('generates footer', async () => { 551 | sinon.replace(footer, 'generate', sinon.fake.resolves('**The footer**')); 552 | packageJson(); 553 | missingChanges(); 554 | setLog('» Inception (Studio)\n\n\n'); 555 | 556 | await changes.write({ footer: true }); 557 | 558 | assert.calledOnceWith( 559 | fs.writeFileSync, 560 | 'CHANGES.md', 561 | `# Changes\n\n## 1.0.0\n\n- Inception\n\n**The footer**\n` 562 | ); 563 | }); 564 | }); 565 | -------------------------------------------------------------------------------- /lib/footer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const $ = require('child_process'); 4 | const github = require('./github'); 5 | 6 | function buildFooter(author, homepage) { 7 | let footer = '_Released'; 8 | if (author) { 9 | footer += homepage ? ` by [${author}](${homepage})` : ` by ${author}`; 10 | } 11 | const today = new Date().toISOString().split('T')[0]; 12 | return `${footer} on ${today}._`; 13 | } 14 | 15 | function readGitConfig(name) { 16 | try { 17 | return $.execSync(`git config --get ${name}`).toString().trim(); 18 | } catch (e) { 19 | return ''; 20 | } 21 | } 22 | 23 | async function generateFooter() { 24 | const author = readGitConfig('user.name') || process.env.GIT_AUTHOR_NAME; 25 | if (author) { 26 | const email = readGitConfig('user.email') || process.env.GIT_AUTHOR_EMAIL; 27 | if (email) { 28 | const homepage = await github.fetchUserHomepage(email); 29 | return buildFooter(author, homepage); 30 | } 31 | return buildFooter(author); 32 | } 33 | return buildFooter(); 34 | } 35 | 36 | exports.generate = generateFooter; 37 | -------------------------------------------------------------------------------- /lib/footer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const $ = require('child_process'); 4 | const { assert, sinon } = require('@sinonjs/referee-sinon'); 5 | const github = require('../lib/github'); 6 | const footer = require('../lib/footer'); 7 | 8 | function today() { 9 | return new Date().toISOString().split('T')[0]; 10 | } 11 | 12 | describe('footer', () => { 13 | beforeEach(() => { 14 | delete process.env.GIT_AUTHOR_NAME; 15 | delete process.env.GIT_AUTHOR_EMAIL; 16 | }); 17 | 18 | afterEach(() => { 19 | sinon.restore(); 20 | delete process.env.GIT_AUTHOR_NAME; 21 | delete process.env.GIT_AUTHOR_EMAIL; 22 | }); 23 | 24 | it('generates footer without author', async () => { 25 | sinon.replace($, 'execSync', sinon.fake.throws(new Error())); 26 | 27 | const foot = await footer.generate(); 28 | 29 | assert.equals(foot, `_Released on ${today()}._`); 30 | }); 31 | 32 | it('generates footer with author from environment variable and without link', async () => { 33 | sinon.replace($, 'execSync', sinon.fake.throws(new Error())); 34 | process.env.GIT_AUTHOR_NAME = 'Maximilian Antoni'; 35 | 36 | const foot = await footer.generate(); 37 | 38 | assert.equals(foot, `_Released by Maximilian Antoni on ${today()}._`); 39 | }); 40 | 41 | it('generates footer with author from git config and without link', async () => { 42 | sinon.replace( 43 | $, 44 | 'execSync', 45 | sinon.fake((cmd) => { 46 | if (cmd === 'git config --get user.name') { 47 | return Buffer.from('Maximilian Antoni\n'); 48 | } 49 | throw new Error(); 50 | }) 51 | ); 52 | 53 | const foot = await footer.generate(); 54 | 55 | assert.calledTwice($.execSync); 56 | assert.calledWith($.execSync, 'git config --get user.name'); 57 | assert.calledWith($.execSync, 'git config --get user.email'); 58 | assert.equals(foot, `_Released by Maximilian Antoni on ${today()}._`); 59 | }); 60 | 61 | it('generates footer with author from environment variable and github homepage link', async () => { 62 | sinon.replace( 63 | github, 64 | 'fetchUserHomepage', 65 | sinon.fake.resolves('https://github.com/mantoni') 66 | ); 67 | sinon.replace( 68 | $, 69 | 'execSync', 70 | sinon.fake((cmd) => { 71 | if (cmd === 'git config --get user.name') { 72 | return Buffer.from('Maximilian Antoni\n'); 73 | } 74 | return Buffer.from('mail@maxantoni.de\n'); 75 | }) 76 | ); 77 | 78 | const foot = await footer.generate(); 79 | 80 | assert.calledOnceWith(github.fetchUserHomepage, 'mail@maxantoni.de'); 81 | assert.equals( 82 | foot, 83 | '_Released by [Maximilian Antoni](https://github.com/mantoni) on ' + 84 | `${today()}._` 85 | ); 86 | }); 87 | 88 | it('generates footer with author from git config and github homepage link', async () => { 89 | sinon.replace( 90 | github, 91 | 'fetchUserHomepage', 92 | sinon.fake.resolves('https://github.com/mantoni') 93 | ); 94 | process.env.GIT_AUTHOR_NAME = 'Maximilian Antoni'; 95 | process.env.GIT_AUTHOR_EMAIL = 'mail@maxantoni.de'; 96 | 97 | const foot = await footer.generate(); 98 | 99 | assert.calledOnceWith(github.fetchUserHomepage, 'mail@maxantoni.de'); 100 | assert.equals( 101 | foot, 102 | '_Released by [Maximilian Antoni](https://github.com/mantoni) on ' + 103 | `${today()}._` 104 | ); 105 | }); 106 | 107 | it('fails if github homepage link can not be retrieved', async () => { 108 | const error = new Error('Oh noes!'); 109 | sinon.replace(github, 'fetchUserHomepage', sinon.fake.rejects(error)); 110 | process.env.GIT_AUTHOR_NAME = 'Maximilian Antoni'; 111 | process.env.GIT_AUTHOR_EMAIL = 'mail@maxantoni.de'; 112 | 113 | const promise = footer.generate(); 114 | 115 | await assert.rejects(promise, error); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /lib/github.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const { promisify } = require('util'); 9 | const request = promisify(require('@studio/json-request')); 10 | 11 | exports.fetchUserHomepage = async function (email) { 12 | const json = await request({ 13 | hostname: 'api.github.com', 14 | path: `/search/users?q=${encodeURIComponent(email)}&in=email`, 15 | headers: { 16 | 'User-Agent': '@studio/changes' 17 | }, 18 | timeout: 5000 19 | }); 20 | 21 | if (json.items && json.items.length === 1) { 22 | return json.items[0].html_url; 23 | } 24 | return null; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/github.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const https = require('https'); 4 | const EventEmitter = require('events'); 5 | const { assert, sinon, match } = require('@sinonjs/referee-sinon'); 6 | const github = require('../lib/github'); 7 | 8 | describe('github', () => { 9 | let clock; 10 | let request; 11 | 12 | beforeEach(() => { 13 | clock = sinon.useFakeTimers(); 14 | request = new EventEmitter(); 15 | request.end = sinon.fake(); 16 | request.abort = sinon.fake(); 17 | sinon.replace(https, 'request', sinon.fake.returns(request)); 18 | }); 19 | 20 | afterEach(() => { 21 | sinon.restore(); 22 | }); 23 | 24 | it('searches user for given email', () => { 25 | github.fetchUserHomepage('mail@maxantoni.de'); 26 | 27 | assert.calledOnceWith(https.request, { 28 | hostname: 'api.github.com', 29 | path: '/search/users?q=mail%40maxantoni.de&in=email', 30 | headers: { 'User-Agent': '@studio/changes' } 31 | }); 32 | }); 33 | 34 | it('yields error on timeout', async () => { 35 | const promise = github.fetchUserHomepage('mail@maxantoni.de'); 36 | clock.tick(5000); 37 | 38 | assert.calledOnce(request.abort); 39 | await assert.rejects( 40 | promise, 41 | match({ 42 | code: 'E_TIMEOUT' 43 | }) 44 | ); 45 | }); 46 | 47 | function respond(json) { 48 | const response = new EventEmitter(); 49 | response.setEncoding = () => {}; 50 | response.headers = { 'content-type': 'application/json' }; 51 | response.statusCode = 200; 52 | https.request.callback(response); 53 | response.emit('data', JSON.stringify(json)); 54 | response.emit('end'); 55 | } 56 | 57 | it('resolves to null if no results', async () => { 58 | const promise = github.fetchUserHomepage('mail@maxantoni.de'); 59 | respond({ items: [] }); 60 | 61 | await assert.resolves(promise, null); 62 | }); 63 | 64 | it('resolves to null if more than one result', async () => { 65 | const promise = github.fetchUserHomepage('mail@maxantoni.de'); 66 | respond({ items: [{}, {}] }); 67 | 68 | await assert.resolves(promise, null); 69 | }); 70 | 71 | it('resolves to homepage if exactly one result', async () => { 72 | const html_url = 'https://github.com/mantoni'; 73 | 74 | const promise = github.fetchUserHomepage('mail@maxantoni.de'); 75 | respond({ items: [{ html_url }] }); 76 | 77 | await assert.resolves(promise, html_url); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const fs = require('fs'); 9 | const detectIndent = require('detect-indent'); 10 | 11 | function addScript(scripts, name, source) { 12 | if (!scripts[name]) { 13 | scripts[name] = source; 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | function isScopedPublicPackage(pkg) { 20 | return ( 21 | pkg.private === false && 22 | typeof pkg.name === 'string' && 23 | pkg.name.indexOf('@') === 0 24 | ); 25 | } 26 | 27 | module.exports = function (argv) { 28 | const json = fs.readFileSync('package.json', 'utf8'); 29 | const pkg = JSON.parse(json); 30 | 31 | let scripts = pkg.scripts; 32 | if (!scripts) { 33 | scripts = pkg.scripts = {}; 34 | } 35 | if (scripts.version) { 36 | return false; 37 | } 38 | 39 | let version_script = 'changes'; 40 | let has_commits = false; 41 | if (argv) { 42 | if (argv.file) { 43 | version_script += ` --file ${argv.file}`; 44 | } 45 | if (argv.commits) { 46 | if (typeof argv.commits === 'boolean') { 47 | if (!pkg.homepage) { 48 | console.error( 49 | '--commits option requires base URL or "homepage" in ' + 50 | 'package.json\n' 51 | ); 52 | return false; 53 | } 54 | } else { 55 | version_script += ` --commits ${argv.commits}`; 56 | has_commits = true; 57 | } 58 | } 59 | } 60 | 61 | let postversion_script = 'git push --follow-tags && npm publish'; 62 | if (isScopedPublicPackage(pkg)) { 63 | postversion_script += ' --access public'; 64 | } 65 | 66 | if (!has_commits && pkg.homepage) { 67 | version_script += ' --commits'; 68 | } 69 | 70 | addScript(scripts, 'preversion', 'npm test'); 71 | addScript(scripts, 'version', version_script); 72 | addScript(scripts, 'postversion', postversion_script); 73 | 74 | const indent = detectIndent(json).indent || ' '; 75 | const out = JSON.stringify(pkg, null, indent); 76 | fs.writeFileSync('package.json', `${out}\n`, 'utf8'); 77 | return true; 78 | }; 79 | -------------------------------------------------------------------------------- /lib/init.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const { assert, refute, sinon } = require('@sinonjs/referee-sinon'); 5 | const init = require('../lib/init'); 6 | 7 | const SCRIPT_PREVERSION = 'npm test'; 8 | const SCRIPT_VERSION = 'changes'; 9 | const SCRIPT_POSTVERSION = 'git push --follow-tags && npm publish'; 10 | 11 | describe('init', () => { 12 | beforeEach(() => { 13 | sinon.stub(fs, 'readFileSync'); 14 | sinon.stub(fs, 'writeFileSync'); 15 | fs.readFileSync.withArgs('package.json').returns( 16 | JSON.stringify({ 17 | version: '1.0.0', 18 | author: 'Studio ' 19 | }) 20 | ); 21 | sinon.stub(console, 'error'); 22 | }); 23 | 24 | afterEach(() => { 25 | sinon.restore(); 26 | }); 27 | 28 | it('adds entire scripts section with default indent', () => { 29 | fs.readFileSync.withArgs('package.json').returns('{}'); 30 | 31 | const result = init(); 32 | 33 | assert.isTrue(result); 34 | assert.calledOnceWith( 35 | fs.writeFileSync, 36 | 'package.json', 37 | `{ 38 | "scripts": { 39 | "preversion": "${SCRIPT_PREVERSION}", 40 | "version": "${SCRIPT_VERSION}", 41 | "postversion": "${SCRIPT_POSTVERSION}" 42 | } 43 | } 44 | `, 45 | 'utf8' 46 | ); 47 | }); 48 | 49 | it('adds scripts to existing scripts with 4 space indent', () => { 50 | fs.readFileSync.withArgs('package.json').returns(`{ 51 | "scripts": { 52 | "test": "echo 'no tests'" 53 | } 54 | }`); 55 | 56 | const result = init(); 57 | 58 | assert.isTrue(result); 59 | assert.calledOnceWith( 60 | fs.writeFileSync, 61 | 'package.json', 62 | `{ 63 | "scripts": { 64 | "test": "echo 'no tests'", 65 | "preversion": "${SCRIPT_PREVERSION}", 66 | "version": "${SCRIPT_VERSION}", 67 | "postversion": "${SCRIPT_POSTVERSION}" 68 | } 69 | } 70 | ` 71 | ); 72 | }); 73 | 74 | it('does not replace existing "preversion" script', () => { 75 | fs.readFileSync.withArgs('package.json').returns(`{ 76 | "scripts": { 77 | "preversion": "echo 'Already defined'" 78 | } 79 | }`); 80 | 81 | const result = init(); 82 | 83 | assert.isTrue(result); 84 | assert.calledOnceWith( 85 | fs.writeFileSync, 86 | 'package.json', 87 | `{ 88 | "scripts": { 89 | "preversion": "echo 'Already defined'", 90 | "version": "${SCRIPT_VERSION}", 91 | "postversion": "${SCRIPT_POSTVERSION}" 92 | } 93 | } 94 | ` 95 | ); 96 | }); 97 | 98 | it('does not replace existing "postversion" script', () => { 99 | fs.readFileSync.withArgs('package.json').returns(`{ 100 | "scripts": { 101 | "postversion": "echo 'Already defined'" 102 | } 103 | }`); 104 | 105 | const result = init(); 106 | 107 | assert.isTrue(result); 108 | assert.calledOnceWith( 109 | fs.writeFileSync, 110 | 'package.json', 111 | `{ 112 | "scripts": { 113 | "postversion": "echo 'Already defined'", 114 | "preversion": "${SCRIPT_PREVERSION}", 115 | "version": "${SCRIPT_VERSION}" 116 | } 117 | } 118 | ` 119 | ); 120 | }); 121 | 122 | it('does nothing if "version" script is already defined', () => { 123 | fs.readFileSync.withArgs('package.json').returns(`{ 124 | "scripts": { 125 | "version": "echo 'Already defined'" 126 | } 127 | }`); 128 | 129 | const result = init(); 130 | 131 | assert.isFalse(result); 132 | refute.called(fs.writeFileSync); 133 | }); 134 | 135 | it('adds --file options if passed', () => { 136 | fs.readFileSync.withArgs('package.json').returns('{}'); 137 | 138 | const result = init({ file: 'changelog.md' }); 139 | 140 | assert.isTrue(result); 141 | assert.calledOnceWith( 142 | fs.writeFileSync, 143 | 'package.json', 144 | `{ 145 | "scripts": { 146 | "preversion": "${SCRIPT_PREVERSION}", 147 | "version": "${SCRIPT_VERSION} --file changelog.md", 148 | "postversion": "${SCRIPT_POSTVERSION}" 149 | } 150 | } 151 | ` 152 | ); 153 | }); 154 | 155 | it('adds --commits if homepage is configured', () => { 156 | fs.readFileSync.withArgs('package.json').returns(`{ 157 | "homepage": "https://github.com/javascript-studio/studio-changes" 158 | }`); 159 | 160 | const result = init(); 161 | 162 | assert.isTrue(result); 163 | assert.calledOnceWith( 164 | fs.writeFileSync, 165 | 'package.json', 166 | `{ 167 | "homepage": "https://github.com/javascript-studio/studio-changes", 168 | "scripts": { 169 | "preversion": "${SCRIPT_PREVERSION}", 170 | "version": "${SCRIPT_VERSION} --commits", 171 | "postversion": "${SCRIPT_POSTVERSION}" 172 | } 173 | } 174 | ` 175 | ); 176 | }); 177 | 178 | it('adds --commits option if passed', () => { 179 | fs.readFileSync.withArgs('package.json').returns('{}'); 180 | 181 | const result = init({ commits: 'https://javascript.studio' }); 182 | 183 | assert.isTrue(result); 184 | assert.calledOnceWith( 185 | fs.writeFileSync, 186 | 'package.json', 187 | `{ 188 | "scripts": { 189 | "preversion": "${SCRIPT_PREVERSION}", 190 | "version": "${SCRIPT_VERSION} --commits https://javascript.studio", 191 | "postversion": "${SCRIPT_POSTVERSION}" 192 | } 193 | } 194 | ` 195 | ); 196 | }); 197 | 198 | it('adds --commits if homepage is configured and --commits is given', () => { 199 | fs.readFileSync.withArgs('package.json').returns(`{ 200 | "homepage": "https://github.com/javascript-studio/studio-changes" 201 | }`); 202 | 203 | const result = init({ commits: true }); // no argument provided, but present 204 | 205 | assert.isTrue(result); 206 | assert.calledOnceWith( 207 | fs.writeFileSync, 208 | 'package.json', 209 | `{ 210 | "homepage": "https://github.com/javascript-studio/studio-changes", 211 | "scripts": { 212 | "preversion": "${SCRIPT_PREVERSION}", 213 | "version": "${SCRIPT_VERSION} --commits", 214 | "postversion": "${SCRIPT_POSTVERSION}" 215 | } 216 | } 217 | ` 218 | ); 219 | }); 220 | 221 | it('fails if --commits is given but homepage is missing', () => { 222 | fs.readFileSync.withArgs('package.json').returns('{}'); 223 | 224 | const result = init({ commits: true }); // no argument provided, but present 225 | 226 | assert.isFalse(result); 227 | refute.called(fs.writeFileSync); 228 | assert.calledOnceWith( 229 | console.error, 230 | '--commits option requires base URL or "homepage" in package.json\n' 231 | ); 232 | }); 233 | 234 | it('adds --file and --commits options if passed', () => { 235 | fs.readFileSync.withArgs('package.json').returns('{}'); 236 | 237 | const result = init({ 238 | file: 'changelog.md', 239 | commits: 'https://studio' 240 | }); 241 | 242 | assert.isTrue(result); 243 | assert.calledOnceWith( 244 | fs.writeFileSync, 245 | 'package.json', 246 | `{ 247 | "scripts": { 248 | "preversion": "${SCRIPT_PREVERSION}", 249 | "version": "${SCRIPT_VERSION} --file changelog.md --commits https://studio", 250 | "postversion": "${SCRIPT_POSTVERSION}" 251 | } 252 | } 253 | ` 254 | ); 255 | }); 256 | 257 | it('prefers explicitly specified --commits config over homepage', () => { 258 | fs.readFileSync.withArgs('package.json').returns(`{ 259 | "homepage": "https://github.com/javascript-studio/studio-changes" 260 | }`); 261 | 262 | const result = init({ commits: 'https://javascript.studio' }); 263 | 264 | assert.isTrue(result); 265 | assert.calledOnceWith( 266 | fs.writeFileSync, 267 | 'package.json', 268 | `{ 269 | "homepage": "https://github.com/javascript-studio/studio-changes", 270 | "scripts": { 271 | "preversion": "${SCRIPT_PREVERSION}", 272 | "version": "${SCRIPT_VERSION} --commits https://javascript.studio", 273 | "postversion": "${SCRIPT_POSTVERSION}" 274 | } 275 | } 276 | ` 277 | ); 278 | }); 279 | 280 | it('combines --file with package.json homepage', () => { 281 | fs.readFileSync.withArgs('package.json').returns(`{ 282 | "homepage": "https://github.com/javascript-studio/studio-changes" 283 | }`); 284 | 285 | const result = init({ file: 'changelog.md' }); 286 | 287 | assert.isTrue(result); 288 | assert.calledOnceWith( 289 | fs.writeFileSync, 290 | 'package.json', 291 | `{ 292 | "homepage": "https://github.com/javascript-studio/studio-changes", 293 | "scripts": { 294 | "preversion": "${SCRIPT_PREVERSION}", 295 | "version": "${SCRIPT_VERSION} --file changelog.md --commits", 296 | "postversion": "${SCRIPT_POSTVERSION}" 297 | } 298 | } 299 | ` 300 | ); 301 | }); 302 | 303 | it('automatically passes `--access public` to scoped public packages', () => { 304 | fs.readFileSync 305 | .withArgs('package.json') 306 | .returns('{"name":"@studio/changes","private":false}'); 307 | 308 | const result = init(); 309 | 310 | assert.isTrue(result); 311 | assert.calledOnceWith( 312 | fs.writeFileSync, 313 | 'package.json', 314 | `{ 315 | "name": "@studio/changes", 316 | "private": false, 317 | "scripts": { 318 | "preversion": "${SCRIPT_PREVERSION}", 319 | "version": "${SCRIPT_VERSION}", 320 | "postversion": "${SCRIPT_POSTVERSION} --access public" 321 | } 322 | } 323 | `, 324 | 'utf8' 325 | ); 326 | }); 327 | 328 | it('does not pass `--access` to packages implicitly restricted by a scoped name', () => { 329 | fs.readFileSync 330 | .withArgs('package.json') 331 | .returns('{"name":"@acme-corp/ledger-tool"}'); 332 | 333 | const result = init(); 334 | 335 | assert.isTrue(result); 336 | assert.calledOnceWith( 337 | fs.writeFileSync, 338 | 'package.json', 339 | `{ 340 | "name": "@acme-corp/ledger-tool", 341 | "scripts": { 342 | "preversion": "${SCRIPT_PREVERSION}", 343 | "version": "${SCRIPT_VERSION}", 344 | "postversion": "${SCRIPT_POSTVERSION}" 345 | } 346 | } 347 | `, 348 | 'utf8' 349 | ); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio/changes", 3 | "version": "3.0.0", 4 | "description": "Generate a changelog as part of the npm version command", 5 | "bin": { 6 | "changes": "bin/cmd.js" 7 | }, 8 | "main": "lib/changes.js", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "test": "mocha '**/*.test.js'", 12 | "watch": "npm test -- --watch", 13 | "preversion": "npm run lint && npm run prettier:check && npm test", 14 | "version": "bin/cmd.js -c --footer", 15 | "postversion": "git push --follow-tags && npm publish", 16 | "prettier:check": "prettier --check '**/*.{js,json,md}'", 17 | "prettier:write": "prettier --write '**/*.{js,json,md}'", 18 | "prepare": "husky install" 19 | }, 20 | "keywords": [ 21 | "changelog", 22 | "version", 23 | "release", 24 | "productivity" 25 | ], 26 | "author": "Maximilian Antoni ", 27 | "contributors": [ 28 | "Blade Barringer ", 29 | "Pat Cavit " 30 | ], 31 | "homepage": "https://github.com/javascript-studio/studio-changes", 32 | "eslintConfig": { 33 | "extends": "@studio", 34 | "rules": { 35 | "n/no-sync": 0, 36 | "n/no-process-exit": 0, 37 | "no-template-curly-in-string": 0 38 | } 39 | }, 40 | "mocha": { 41 | "require": "test/hooks.js", 42 | "ignore": "node_modules/**", 43 | "reporter": "dot" 44 | }, 45 | "dependencies": { 46 | "@studio/editor": "^1.1.1", 47 | "@studio/json-request": "^3.0.1", 48 | "detect-indent": "^6.1.0", 49 | "hosted-git-info": "^7.0.1", 50 | "minimist": "^1.2.8" 51 | }, 52 | "devDependencies": { 53 | "@sinonjs/referee-sinon": "^11.0.0", 54 | "@studio/eslint-config": "^6.0.0", 55 | "@studio/related-tests": "^0.2.0", 56 | "eslint": "^8.56.0", 57 | "husky": "^8.0.3", 58 | "lint-staged": "^15.2.0", 59 | "mocha": "^10.2.0", 60 | "prettier": "^3.1.1" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/javascript-studio/studio-changes.git" 65 | }, 66 | "files": [ 67 | "CHANGES.md", 68 | "**/*.js", 69 | "!**/*.test.js", 70 | "!test/**", 71 | "!.*" 72 | ], 73 | "license": "MIT" 74 | } 75 | -------------------------------------------------------------------------------- /test/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { sinon } = require('@sinonjs/referee-sinon'); 4 | 5 | exports.mochaHooks = { 6 | afterEach() { 7 | sinon.restore(); 8 | } 9 | }; 10 | --------------------------------------------------------------------------------