├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .huskyrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.js ├── config.js ├── eslint.js ├── husky.js ├── jest.config.js ├── jest.js ├── other ├── CODE_OF_CONDUCT.md ├── MAINTAINING.md ├── USERS.md └── manual-releases.md ├── package.json ├── prettier.js ├── shared-tsconfig.json └── src ├── __mocks__ └── cross-spawn.js ├── __tests__ ├── __snapshots__ │ ├── index.js.snap │ └── utils.js.snap ├── index.js └── utils.js ├── config ├── __tests__ │ └── umbrella.js ├── babel-transform.js ├── babelrc.js ├── eslintignore ├── eslintrc.js ├── huskyrc.js ├── index.js ├── jest.config.js ├── lintstagedrc.js ├── prettierignore ├── prettierrc.js └── rollup.config.js ├── index.js ├── run-script.js ├── scripts ├── __tests__ │ ├── __snapshots__ │ │ ├── format.js.snap │ │ ├── lint.js.snap │ │ ├── precommit.js.snap │ │ ├── test.js.snap │ │ └── validate.js.snap │ ├── format.js │ ├── helpers │ │ └── serializers.js │ ├── lint.js │ ├── precommit.js │ ├── test.js │ └── validate.js ├── build │ ├── babel.js │ ├── index.js │ └── rollup.js ├── format.js ├── lint.js ├── pre-commit.js ├── test.js ├── typecheck.js └── validate.js └── utils.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "kcd-scripts", 3 | "projectOwner": "kentcdodds", 4 | "imageSize": 100, 5 | "commit": false, 6 | "contributorsPerLine": 7, 7 | "repoHost": "https://github.com", 8 | "repoType": "github", 9 | "skipCi": false, 10 | "files": [ 11 | "README.md" 12 | ], 13 | "contributors": [ 14 | { 15 | "login": "kentcdodds", 16 | "name": "Kent C. Dodds", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 18 | "profile": "https://kentcdodds.com", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "infra", 23 | "test" 24 | ] 25 | }, 26 | { 27 | "login": "sudo-suhas", 28 | "name": "Suhas Karanth", 29 | "avatar_url": "https://avatars2.githubusercontent.com/u/22251956?v=4", 30 | "profile": "https://github.com/sudo-suhas", 31 | "contributions": [ 32 | "code", 33 | "bug", 34 | "test" 35 | ] 36 | }, 37 | { 38 | "login": "pbomb", 39 | "name": "Matt Parrish", 40 | "avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4", 41 | "profile": "https://github.com/pbomb", 42 | "contributions": [ 43 | "code", 44 | "test" 45 | ] 46 | }, 47 | { 48 | "login": "mateuscb", 49 | "name": "Mateus", 50 | "avatar_url": "https://avatars3.githubusercontent.com/u/1319157?v=4", 51 | "profile": "https://github.com/mateuscb", 52 | "contributions": [ 53 | "code", 54 | "test" 55 | ] 56 | }, 57 | { 58 | "login": "macklinu", 59 | "name": "Macklin Underdown", 60 | "avatar_url": "https://avatars1.githubusercontent.com/u/2344137?v=4", 61 | "profile": "http://macklin.underdown.me", 62 | "contributions": [ 63 | "code", 64 | "test" 65 | ] 66 | }, 67 | { 68 | "login": "stereobooster", 69 | "name": "stereobooster", 70 | "avatar_url": "https://avatars2.githubusercontent.com/u/179534?v=4", 71 | "profile": "https://github.com/stereobooster", 72 | "contributions": [ 73 | "code", 74 | "test" 75 | ] 76 | }, 77 | { 78 | "login": "donysukardi", 79 | "name": "Dony Sukardi", 80 | "avatar_url": "https://avatars0.githubusercontent.com/u/410792?v=4", 81 | "profile": "http://dsds.io", 82 | "contributions": [ 83 | "bug", 84 | "code" 85 | ] 86 | }, 87 | { 88 | "login": "alexandernanberg", 89 | "name": "Alexander Nanberg", 90 | "avatar_url": "https://avatars3.githubusercontent.com/u/8997319?v=4", 91 | "profile": "https://alexandernanberg.com", 92 | "contributions": [ 93 | "code" 94 | ] 95 | }, 96 | { 97 | "login": "fobbyal", 98 | "name": "Alex Liang", 99 | "avatar_url": "https://avatars2.githubusercontent.com/u/7818365?v=4", 100 | "profile": "https://github.com/fobbyal", 101 | "contributions": [ 102 | "code" 103 | ] 104 | }, 105 | { 106 | "login": "shellthor", 107 | "name": "Jeff Detmer", 108 | "avatar_url": "https://avatars1.githubusercontent.com/u/649578?v=4", 109 | "profile": "http://www.jeffdetmer.com", 110 | "contributions": [ 111 | "code" 112 | ] 113 | }, 114 | { 115 | "login": "alexzherdev", 116 | "name": "Alex Zherdev", 117 | "avatar_url": "https://avatars3.githubusercontent.com/u/93752?v=4", 118 | "profile": "https://twitter.com/endymion_r", 119 | "contributions": [ 120 | "code" 121 | ] 122 | }, 123 | { 124 | "login": "adamdharrington", 125 | "name": "Adam Harrington", 126 | "avatar_url": "https://avatars0.githubusercontent.com/u/5477801?v=4", 127 | "profile": "https://github.com/adamdharrington", 128 | "contributions": [ 129 | "code", 130 | "test" 131 | ] 132 | }, 133 | { 134 | "login": "afontcu", 135 | "name": "Adrià Fontcuberta", 136 | "avatar_url": "https://avatars0.githubusercontent.com/u/9197791?v=4", 137 | "profile": "https://afontcu.dev", 138 | "contributions": [ 139 | "code" 140 | ] 141 | }, 142 | { 143 | "login": "coderberry", 144 | "name": "Eric Berry", 145 | "avatar_url": "https://avatars2.githubusercontent.com/u/12481?v=4", 146 | "profile": "https://codefund.io", 147 | "contributions": [ 148 | "fundingFinding" 149 | ] 150 | }, 151 | { 152 | "login": "schaab", 153 | "name": "Jared Schaab", 154 | "avatar_url": "https://avatars0.githubusercontent.com/u/1103255?v=4", 155 | "profile": "https://github.com/schaab", 156 | "contributions": [ 157 | "code", 158 | "test" 159 | ] 160 | }, 161 | { 162 | "login": "SerkanSipahi", 163 | "name": "Bitcollage", 164 | "avatar_url": "https://avatars2.githubusercontent.com/u/1880749?v=4", 165 | "profile": "https://www.linkedin.com/in/serkan-sipahi-59b20081/", 166 | "contributions": [ 167 | "code" 168 | ] 169 | }, 170 | { 171 | "login": "MichaelDeBoey", 172 | "name": "Michaël De Boey", 173 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 174 | "profile": "https://michaeldeboey.be", 175 | "contributions": [ 176 | "code", 177 | "review" 178 | ] 179 | }, 180 | { 181 | "login": "weyert", 182 | "name": "Weyert de Boer", 183 | "avatar_url": "https://avatars3.githubusercontent.com/u/7049?v=4", 184 | "profile": "https://github.com/weyert", 185 | "contributions": [ 186 | "code" 187 | ] 188 | }, 189 | { 190 | "login": "KubaJastrz", 191 | "name": "Jakub Jastrzębski", 192 | "avatar_url": "https://avatars0.githubusercontent.com/u/6443113?v=4", 193 | "profile": "https://kubajastrz.com", 194 | "contributions": [ 195 | "code" 196 | ] 197 | }, 198 | { 199 | "login": "Lukas-Kullmann", 200 | "name": "Lukas", 201 | "avatar_url": "https://avatars0.githubusercontent.com/u/387547?v=4", 202 | "profile": "https://github.com/Lukas-Kullmann", 203 | "contributions": [ 204 | "code", 205 | "doc" 206 | ] 207 | }, 208 | { 209 | "login": "mihar-22", 210 | "name": "Rahim Alwer", 211 | "avatar_url": "https://avatars2.githubusercontent.com/u/14304599?v=4", 212 | "profile": "https://github.com/mihar-22", 213 | "contributions": [ 214 | "code" 215 | ] 216 | }, 217 | { 218 | "login": "ghostd", 219 | "name": "Vincent Ricard", 220 | "avatar_url": "https://avatars1.githubusercontent.com/u/1098399?v=4", 221 | "profile": "https://github.com/ghostd", 222 | "contributions": [ 223 | "code" 224 | ] 225 | }, 226 | { 227 | "login": "timdeschryver", 228 | "name": "Tim Deschryver", 229 | "avatar_url": "https://avatars1.githubusercontent.com/u/28659384?v=4", 230 | "profile": "http://timdeschryver.dev", 231 | "contributions": [ 232 | "code" 233 | ] 234 | }, 235 | { 236 | "login": "eddyw", 237 | "name": "Eddy Wilson", 238 | "avatar_url": "https://avatars0.githubusercontent.com/u/1407526?v=4", 239 | "profile": "https://github.com/eddyw", 240 | "contributions": [ 241 | "review" 242 | ] 243 | }, 244 | { 245 | "login": "rbusquet", 246 | "name": "Ricardo Busquet", 247 | "avatar_url": "https://avatars1.githubusercontent.com/u/7198302?v=4", 248 | "profile": "https://ricardobusquet.com", 249 | "contributions": [ 250 | "review" 251 | ] 252 | }, 253 | { 254 | "login": "aprillion", 255 | "name": "Peter Hozák", 256 | "avatar_url": "https://avatars0.githubusercontent.com/u/1087670?v=4", 257 | "profile": "http://peter.hozak.info/", 258 | "contributions": [ 259 | "review" 260 | ] 261 | }, 262 | { 263 | "login": "marcosvega91", 264 | "name": "Marco Moretti", 265 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", 266 | "profile": "https://github.com/marcosvega91", 267 | "contributions": [ 268 | "code" 269 | ] 270 | }, 271 | { 272 | "login": "rafgraph", 273 | "name": "Rafael Pedicini", 274 | "avatar_url": "https://avatars0.githubusercontent.com/u/11911299?v=4", 275 | "profile": "http://rafgraph.dev", 276 | "contributions": [ 277 | "doc" 278 | ] 279 | }, 280 | { 281 | "login": "mpeyper", 282 | "name": "Michael Peyper", 283 | "avatar_url": "https://avatars0.githubusercontent.com/u/23029903?v=4", 284 | "profile": "https://github.com/mpeyper", 285 | "contributions": [ 286 | "code" 287 | ] 288 | }, 289 | { 290 | "login": "HOUCe", 291 | "name": "HOU Ce", 292 | "avatar_url": "https://avatars.githubusercontent.com/u/19988985?v=4", 293 | "profile": "https://www.zhihu.com/people/lucas-hc/activities", 294 | "contributions": [ 295 | "code" 296 | ] 297 | }, 298 | { 299 | "login": "AriPerkkio", 300 | "name": "Ari Perkkiö", 301 | "avatar_url": "https://avatars.githubusercontent.com/u/14806298?v=4", 302 | "profile": "https://codepen.io/ariperkkio/", 303 | "contributions": [ 304 | "code", 305 | "test" 306 | ] 307 | }, 308 | { 309 | "login": "eps1lon", 310 | "name": "Sebastian Silbermann", 311 | "avatar_url": "https://avatars.githubusercontent.com/u/12292047?v=4", 312 | "profile": "https://solverfox.dev", 313 | "contributions": [ 314 | "code" 315 | ] 316 | }, 317 | { 318 | "login": "nstepien", 319 | "name": "Nicolas Stepien", 320 | "avatar_url": "https://avatars.githubusercontent.com/u/567105?v=4", 321 | "profile": "https://github.com/nstepien", 322 | "contributions": [ 323 | "code" 324 | ] 325 | }, 326 | { 327 | "login": "KSVarun", 328 | "name": "Varun", 329 | "avatar_url": "https://avatars.githubusercontent.com/u/15784650?v=4", 330 | "profile": "https://github.com/KSVarun", 331 | "contributions": [ 332 | "doc" 333 | ] 334 | }, 335 | { 336 | "login": "nickmccurdy", 337 | "name": "Nick McCurdy", 338 | "avatar_url": "https://avatars.githubusercontent.com/u/927220?v=4", 339 | "profile": "https://nickmccurdy.com/", 340 | "contributions": [ 341 | "code" 342 | ] 343 | }, 344 | { 345 | "login": "SaiMaheshwarReddy", 346 | "name": "Sai Maheshwar", 347 | "avatar_url": "https://avatars.githubusercontent.com/u/61627080?v=4", 348 | "profile": "https://github.com/SaiMaheshwarReddy", 349 | "contributions": [ 350 | "code" 351 | ] 352 | } 353 | ], 354 | "commitType": "docs", 355 | "commitConvention": "angular" 356 | } 357 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `kcd-scripts` version: 15 | - `node` version: 16 | - `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | 22 | ``` 23 | 24 | What you did: 25 | 26 | What happened: 27 | 28 | 29 | 30 | Reproduction repository: 31 | 32 | 36 | 37 | Problem description: 38 | 39 | Suggested solution: 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | 37 | - [ ] Documentation 38 | - [ ] Tests 39 | - [ ] Ready to be merged 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'next' 8 | - 'next-major' 9 | - 'beta' 10 | - 'alpha' 11 | - '!all-contributors/**' 12 | pull_request: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | main: 20 | # ignore all-contributors PRs 21 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest, windows-latest] 25 | node: [lts/-1, lts/*, latest] 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: ⬇️ Checkout repo 29 | uses: actions/checkout@v4 30 | 31 | - name: ⎔ Setup node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node }} 35 | 36 | - name: 📥 Download deps 37 | uses: bahmutov/npm-install@v1 38 | with: 39 | useLockFile: false 40 | 41 | - name: ▶️ Run validate script 42 | run: npm run validate 43 | 44 | - name: ⬆️ Upload coverage report 45 | uses: codecov/codecov-action@v3 46 | 47 | release: 48 | needs: main 49 | runs-on: ubuntu-latest 50 | if: 51 | ${{ github.repository == 'kentcdodds/kcd-scripts' && 52 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 53 | github.ref) && github.event_name == 'push' }} 54 | steps: 55 | - name: ⬇️ Checkout repo 56 | uses: actions/checkout@v4 57 | 58 | - name: ⎔ Setup node 59 | uses: actions/setup-node@v4 60 | with: 61 | node-version: lts/* 62 | 63 | - name: 📥 Download deps 64 | uses: bahmutov/npm-install@v1 65 | with: 66 | useLockFile: false 67 | 68 | - name: 🏗 Run build script 69 | run: npm run build 70 | 71 | - name: 🚀 Release 72 | uses: cycjimmy/semantic-release-action@v4 73 | with: 74 | branches: | 75 | [ 76 | '+([0-9])?(.{+([0-9]),x}).x', 77 | 'main', 78 | 'next', 79 | 'next-major', 80 | {name: 'beta', prerelease: true}, 81 | {name: 'alpha', prerelease: true} 82 | ] 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | 6 | # these cause more harm than good 7 | # when working with contributors 8 | package-lock.json 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/config/huskyrc') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/config/prettierrc') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. `$ npm install` to install dependencies 12 | 3. `$ npm run validate` to validate you've got it working 13 | 4. Create a branch for your PR 14 | 15 | > Tip: Keep your `master` branch pointing at the original repository and make 16 | > pull requests from branches on your fork. To do this, run: 17 | > 18 | > ``` 19 | > git remote add upstream https://github.com/kentcdodds/kcd-scripts 20 | > git fetch upstream 21 | > git branch --set-upstream-to=upstream/master master 22 | > ``` 23 | > 24 | > This will add the original repository as a "remote" called "upstream," Then 25 | > fetch the git information from that remote, then set your local `master` 26 | > branch to use the upstream master branch whenever you run `git pull`. Then you 27 | > can make all of your pull request branches based on this `master` branch. 28 | > Whenever you want to update your version of `master`, do a regular `git pull`. 29 | 30 | ## Committing and Pushing changes 31 | 32 | Please make sure to run the tests before you commit your changes. You can run 33 | `npm run test:update` which will update any snapshots that need updating. Make 34 | sure to include those changes (if they exist) in your commit. 35 | 36 | ## Help needed 37 | 38 | Please checkout the [the open issues][issues] 39 | 40 | Also, please watch the repo and respond to questions/bug reports/feature 41 | requests! Thanks! 42 | 43 | 44 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 45 | [issues]: https://github.com/kentcdodds/kcd-scripts/issues 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

kcd-scripts 🛠📦

3 | 4 |

CLI toolbox for common scripts for my projects

5 |
6 | 7 | --- 8 | 9 | 10 | [![Build Status][build-badge]][build] 11 | [![Code Coverage][coverage-badge]][coverage] 12 | [![version][version-badge]][package] 13 | [![downloads][downloads-badge]][npmtrends] 14 | [![MIT License][license-badge]][license] 15 | [![All Contributors][all-contributors-badge]](#contributors-) 16 | [![PRs Welcome][prs-badge]][prs] 17 | [![Code of Conduct][coc-badge]][coc] 18 | 19 | 20 | ## The problem 21 | 22 | I do a bunch of open source and want to make it easier to maintain so many 23 | projects. 24 | 25 | ## This solution 26 | 27 | This is a CLI that abstracts away all configuration for my open source projects 28 | for linting, testing, building, and more. 29 | 30 | ## Table of Contents 31 | 32 | 33 | 34 | 35 | - [Installation](#installation) 36 | - [Usage](#usage) 37 | - [Overriding Config](#overriding-config) 38 | - [TypeScript Support](#typescript-support) 39 | - [Inspiration](#inspiration) 40 | - [Other Solutions](#other-solutions) 41 | - [Issues](#issues) 42 | - [🐛 Bugs](#-bugs) 43 | - [💡 Feature Requests](#-feature-requests) 44 | - [Contributors ✨](#contributors-) 45 | - [LICENSE](#license) 46 | 47 | 48 | 49 | ## Installation 50 | 51 | This module is distributed via [npm][npm] which is bundled with [node][node] and 52 | should be installed as one of your project's `devDependencies`: 53 | 54 | ``` 55 | npm install --save-dev kcd-scripts 56 | ``` 57 | 58 | ## Usage 59 | 60 | This is a CLI and exposes a bin called `kcd-scripts`. I don't really plan on 61 | documenting or testing it super duper well because it's really specific to my 62 | needs. You'll find all available scripts in `src/scripts`. 63 | 64 | This project actually dogfoods itself. If you look in the `package.json`, you'll 65 | find scripts with `node src {scriptName}`. This serves as an example of some of 66 | the things you can do with `kcd-scripts`. 67 | 68 | ### Overriding Config 69 | 70 | Unlike `react-scripts`, `kcd-scripts` allows you to specify your own 71 | configuration for things and have that plug directly into the way things work 72 | with `kcd-scripts`. There are various ways that it works, but basically if you 73 | want to have your own config for something, just add the configuration and 74 | `kcd-scripts` will use that instead of it's own internal config. In addition, 75 | `kcd-scripts` exposes its configuration so you can use it and override only the 76 | parts of the config you need to. 77 | 78 | This can be a very helpful way to make editor integration work for tools like 79 | ESLint which require project-based ESLint configuration to be present to work. 80 | 81 | So, if we were to do this for ESLint, you could create an `.eslintrc` with the 82 | contents of: 83 | 84 | ``` 85 | {"extends": "./node_modules/kcd-scripts/eslint.js"} 86 | ``` 87 | 88 | > Note: for now, you'll have to include an `.eslintignore` in your project until 89 | > [this eslint issue is resolved](https://github.com/eslint/eslint/issues/9227). 90 | 91 | Or, for `babel`, a `.babelrc` with: 92 | 93 | ``` 94 | {"presets": ["kcd-scripts/babel"]} 95 | ``` 96 | 97 | Or, for `jest`: 98 | 99 | ```javascript 100 | const {jest: jestConfig} = require('kcd-scripts/config') 101 | module.exports = Object.assign(jestConfig, { 102 | // your overrides here 103 | 104 | // for test written in Typescript, add: 105 | transform: { 106 | '\\.(ts|tsx)$': '/node_modules/ts-jest/preprocessor.js', 107 | }, 108 | }) 109 | ``` 110 | 111 | > Note: `kcd-scripts` intentionally does not merge things for you when you start 112 | > configuring things to make it less magical and more straightforward. Extending 113 | > can take place on your terms. I think this is actually a great way to do this. 114 | 115 | ### TypeScript Support 116 | 117 | If the `tsconfig.json`-file is present in the project root directory and 118 | `typescript` is a dependency the `@babel/preset-typescript` will automatically 119 | get loaded when you use the default babel config that comes with `kcd-scripts`. 120 | If you customized your `.babelrc`-file you might need to manually add 121 | `@babel/preset-typescript` to the `presets`-section. 122 | 123 | `kcd-scripts` will automatically load any `.ts` and `.tsx` files, including the 124 | default entry point, so you don't have to worry about any rollup configuration. 125 | 126 | If you have a `typecheck` script (normally set to `kcd-scripts typecheck`) that 127 | will be run as part of the `validate` script (which is run as part of the 128 | `pre-commit` script as well). 129 | 130 | TypeScript definition files will also automatically be generated during the 131 | `build` script. 132 | 133 | ## Inspiration 134 | 135 | This is inspired by `react-scripts`. 136 | 137 | ## Other Solutions 138 | 139 | If you are aware of any please [make a pull request][prs] and add it here! 140 | Again, this is a very specific-to-me solution. 141 | 142 | - [Rollpkg](https://github.com/rafgraph/rollpkg) - convention over config build 143 | tool to create packages with TypeScript and Rollup. 144 | - [bebbi-scripts](https://github.com/bebbi/bebbi-scripts) - like kcd-scripts but 145 | ✅ tsc, ✅ `esm`/`cjs`/`types`, ✅ in TS, ✅ yarn 3, ✅ init package.json, 146 | ✅ yarn workspace, ✅ extensible (babel, storybook, ..), 🚫 yarn pnp, 🚫 npm 147 | 148 | ## Issues 149 | 150 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 151 | label._ 152 | 153 | ### 🐛 Bugs 154 | 155 | Please file an issue for bugs, missing documentation, or unexpected behavior. 156 | 157 | [**See Bugs**][bugs] 158 | 159 | ### 💡 Feature Requests 160 | 161 | Please file an issue to suggest new features. Vote on feature requests by adding 162 | a 👍. This helps maintainers prioritize what to work on. 163 | 164 | [**See Feature Requests**][requests] 165 | 166 | ## Contributors ✨ 167 | 168 | Thanks goes to these people ([emoji key][emojis]): 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 |
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
Suhas Karanth
Suhas Karanth

💻 🐛 ⚠️
Matt Parrish
Matt Parrish

💻 ⚠️
Mateus
Mateus

💻 ⚠️
Macklin Underdown
Macklin Underdown

💻 ⚠️
stereobooster
stereobooster

💻 ⚠️
Dony Sukardi
Dony Sukardi

🐛 💻
Alexander Nanberg
Alexander Nanberg

💻
Alex Liang
Alex Liang

💻
Jeff Detmer
Jeff Detmer

💻
Alex Zherdev
Alex Zherdev

💻
Adam Harrington
Adam Harrington

💻 ⚠️
Adrià Fontcuberta
Adrià Fontcuberta

💻
Eric Berry
Eric Berry

🔍
Jared Schaab
Jared Schaab

💻 ⚠️
Bitcollage
Bitcollage

💻
Michaël De Boey
Michaël De Boey

💻 👀
Weyert de Boer
Weyert de Boer

💻
Jakub Jastrzębski
Jakub Jastrzębski

💻
Lukas
Lukas

💻 📖
Rahim Alwer
Rahim Alwer

💻
Vincent Ricard
Vincent Ricard

💻
Tim Deschryver
Tim Deschryver

💻
Eddy Wilson
Eddy Wilson

👀
Ricardo Busquet
Ricardo Busquet

👀
Peter Hozák
Peter Hozák

👀
Marco Moretti
Marco Moretti

💻
Rafael Pedicini
Rafael Pedicini

📖
Michael Peyper
Michael Peyper

💻
HOU Ce
HOU Ce

💻
Ari Perkkiö
Ari Perkkiö

💻 ⚠️
Sebastian Silbermann
Sebastian Silbermann

💻
Nicolas Stepien
Nicolas Stepien

💻
Varun
Varun

📖
Nick McCurdy
Nick McCurdy

💻
Sai Maheshwar
Sai Maheshwar

💻
225 | 226 | 227 | 228 | 229 | 230 | 231 | This project follows the [all-contributors][all-contributors] specification. 232 | Contributions of any kind welcome! 233 | 234 | ## LICENSE 235 | 236 | MIT 237 | 238 | 239 | [npm]: https://www.npmjs.com 240 | [node]: https://nodejs.org 241 | [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/kcd-scripts/validate?logo=github&style=flat-square 242 | [build]: https://github.com/kentcdodds/kcd-scripts/actions?query=workflow%3Avalidate 243 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/kcd-scripts.svg?style=flat-square 244 | [coverage]: https://codecov.io/github/kentcdodds/kcd-scripts 245 | [version-badge]: https://img.shields.io/npm/v/kcd-scripts.svg?style=flat-square 246 | [package]: https://www.npmjs.com/package/kcd-scripts 247 | [downloads-badge]: https://img.shields.io/npm/dm/kcd-scripts.svg?style=flat-square 248 | [npmtrends]: http://www.npmtrends.com/kcd-scripts 249 | [license-badge]: https://img.shields.io/npm/l/kcd-scripts.svg?style=flat-square 250 | [license]: https://github.com/kentcdodds/kcd-scripts/blob/master/LICENSE 251 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 252 | [prs]: http://makeapullrequest.com 253 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 254 | [coc]: https://github.com/kentcdodds/kcd-scripts/blob/master/other/CODE_OF_CONDUCT.md 255 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 256 | [all-contributors]: https://github.com/all-contributors/all-contributors 257 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/kcd-scripts?color=orange&style=flat-square 258 | [bugs]: https://github.com/kentcdodds/kcd-scripts/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 259 | [requests]: https://github.com/kentcdodds/kcd-scripts/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 260 | [good-first-issue]: https://github.com/kentcdodds/kcd-scripts/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 261 | 262 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/config/babelrc') 2 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/config') 2 | -------------------------------------------------------------------------------- /eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/config/eslintrc') 2 | -------------------------------------------------------------------------------- /husky.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/config/huskyrc') 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const {jest} = require('./src/config') 2 | 3 | module.exports = { 4 | ...jest, 5 | coverageThreshold: null, 6 | } 7 | -------------------------------------------------------------------------------- /jest.js: -------------------------------------------------------------------------------- 1 | // eslint-ignore-next-line import/extensions 2 | module.exports = require('./dist/config/jest.config') 3 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | 4 | 5 | 6 | 7 | **Table of Contents** 8 | 9 | - [Our Pledge](#our-pledge) 10 | - [Our Standards](#our-standards) 11 | - [Our Responsibilities](#our-responsibilities) 12 | - [Scope](#scope) 13 | - [Enforcement](#enforcement) 14 | - [Attribution](#attribution) 15 | 16 | 17 | 18 | ## Our Pledge 19 | 20 | In the interest of fostering an open and welcoming environment, we as 21 | contributors and maintainers pledge to making participation in our project and 22 | our community a harassment-free experience for everyone, regardless of age, body 23 | size, disability, ethnicity, gender identity and expression, level of 24 | experience, nationality, personal appearance, race, religion, or sexual identity 25 | and orientation. 26 | 27 | ## Our Standards 28 | 29 | Examples of behavior that contributes to creating a positive environment 30 | include: 31 | 32 | - Using welcoming and inclusive language 33 | - Being respectful of differing viewpoints and experiences 34 | - Gracefully accepting constructive criticism 35 | - Focusing on what is best for the community 36 | - Showing empathy towards other community members 37 | 38 | Examples of unacceptable behavior by participants include: 39 | 40 | - The use of sexualized language or imagery and unwelcome sexual attention or 41 | advances 42 | - Trolling, insulting/derogatory comments, and personal or political attacks 43 | - Public or private harassment 44 | - Publishing others' private information, such as a physical or electronic 45 | address, without explicit permission 46 | - Other conduct which could reasonably be considered inappropriate in a 47 | professional setting 48 | 49 | ## Our Responsibilities 50 | 51 | Project maintainers are responsible for clarifying the standards of acceptable 52 | behavior and are expected to take appropriate and fair corrective action in 53 | response to any instances of unacceptable behavior. 54 | 55 | Project maintainers have the right and responsibility to remove, edit, or reject 56 | comments, commits, code, wiki edits, issues, and other contributions that are 57 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 58 | contributor for other behaviors that they deem inappropriate, threatening, 59 | offensive, or harmful. 60 | 61 | ## Scope 62 | 63 | This Code of Conduct applies both within project spaces and in public spaces 64 | when an individual is representing the project or its community. Examples of 65 | representing a project or community include using an official project e-mail 66 | address, posting via an official social media account, or acting as an appointed 67 | representative at an online or offline event. Representation of a project may be 68 | further defined and clarified by project maintainers. 69 | 70 | ## Enforcement 71 | 72 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 73 | reported by contacting the project team at kent+coc@doddsfamily.us. All 74 | complaints will be reviewed and investigated and will result in a response that 75 | is deemed necessary and appropriate to the circumstances. The project team is 76 | obligated to maintain confidentiality with regard to the reporter of an 77 | incident. Further details of specific enforcement policies may be posted 78 | separately. 79 | 80 | Project maintainers who do not follow or enforce the Code of Conduct in good 81 | faith may face temporary or permanent repercussions as determined by other 82 | members of the project's leadership. 83 | 84 | ## Attribution 85 | 86 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 87 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 88 | 89 | [homepage]: http://contributor-covenant.org 90 | [version]: http://contributor-covenant.org/version/1/4/ 91 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | 4 | 5 | 6 | **Table of Contents** 7 | 8 | - [Code of Conduct](#code-of-conduct) 9 | - [Issues](#issues) 10 | - [Pull Requests](#pull-requests) 11 | - [Release](#release) 12 | - [Thanks!](#thanks) 13 | 14 | 15 | 16 | This is documentation for maintainers of this project. 17 | 18 | ## Code of Conduct 19 | 20 | Please review, understand, and be an example of it. Violations of the code of 21 | conduct are taken seriously, even (especially) for maintainers. 22 | 23 | ## Issues 24 | 25 | We want to support and build the community. We do that best by helping people 26 | learn to solve their own problems. We have an issue template and hopefully most 27 | folks follow it. If it's not clear what the issue is, invite them to create a 28 | minimal reproduction of what they're trying to accomplish or the bug they think 29 | they've found. 30 | 31 | Once it's determined that a code change is necessary, point people to 32 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a 33 | pull request. If they're the one who needs the feature, they're the one who can 34 | build it. If they need some hand holding and you have time to lend a hand, 35 | please do so. It's an investment into another human being, and an investment 36 | into a potential maintainer. 37 | 38 | Remember that this is open source, so the code is not yours, it's ours. If 39 | someone needs a change in the codebase, you don't have to make it happen 40 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 41 | any more of you than that. 42 | 43 | ## Pull Requests 44 | 45 | As a maintainer, you're fine to make your branches on the main repo or on your 46 | own fork. Either way is fine. 47 | 48 | When we receive a pull request, a GitHub Action is kicked off automatically (see 49 | the `.github/workflows/validate.yml` for what runs in the Action). We avoid 50 | merging anything that breaks the GitHub Action. 51 | 52 | Please review PRs and focus on the code rather than the individual. You never 53 | know when this is someone's first ever PR and we want their experience to be as 54 | positive as possible, so be uplifting and constructive. 55 | 56 | When you merge the pull request, 99% of the time you should use the 57 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 58 | feature. This keeps our git history clean, but more importantly, this allows us 59 | to make any necessary changes to the commit message so we release what we want 60 | to release. See the next section on Releases for more about that. 61 | 62 | ## Release 63 | 64 | Our releases are automatic. They happen whenever code lands into `master`. A 65 | GitHub Action gets kicked off and if it's successful, a tool called 66 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 67 | used to automatically publish a new release to npm as well as a changelog to 68 | GitHub. It is only able to determine the version and whether a release is 69 | necessary by the git commit messages. With this in mind, **please brush up on 70 | [the commit message convention][commit] which drives our releases.** 71 | 72 | > One important note about this: Please make sure that commit messages do NOT 73 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 74 | > version. I've been burned by this more than once where someone will include 75 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 76 | > a huge deal honestly, but kind of annoying... 77 | 78 | ## Thanks! 79 | 80 | Thank you so much for helping to maintain this project! 81 | 82 | 83 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 84 | 85 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | If you or your company uses this project, add your name to this list! Eventually 10 | we may have a website to showcase these (wanna build it!?) 11 | 12 | > No users have been added yet! 13 | 14 | 19 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | This project has an automated release set up. So things are only released when 10 | there are useful changes in the code that justify a release. But sometimes 11 | things get messed up one way or another and we need to trigger the release 12 | ourselves. When this happens, simply bump the number below and commit that with 13 | the following commit message based on your needs: 14 | 15 | **Major** 16 | 17 | ``` 18 | fix(release): manually release a major version 19 | 20 | There was an issue with a major release, so this manual-releases.md 21 | change is to release a new major version. 22 | 23 | Reference: # 24 | 25 | BREAKING CHANGE: 26 | ``` 27 | 28 | **Minor** 29 | 30 | ``` 31 | feat(release): manually release a minor version 32 | 33 | There was an issue with a minor release, so this manual-releases.md 34 | change is to release a new minor version. 35 | 36 | Reference: # 37 | ``` 38 | 39 | **Patch** 40 | 41 | ``` 42 | fix(release): manually release a patch version 43 | 44 | There was an issue with a patch release, so this manual-releases.md 45 | change is to release a new patch version. 46 | 47 | Reference: # 48 | ``` 49 | 50 | The number of times we've had to do a manual release is: 2 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kcd-scripts", 3 | "version": "0.0.0-semantically-released", 4 | "description": "CLI for common scripts for my projects", 5 | "keywords": [], 6 | "homepage": "https://github.com/kentcdodds/kcd-scripts#readme", 7 | "bugs": { 8 | "url": "https://github.com/kentcdodds/kcd-scripts/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/kentcdodds/kcd-scripts" 13 | }, 14 | "license": "MIT", 15 | "author": "Kent C. Dodds (https://kentcdodds.com)", 16 | "bin": { 17 | "kcd-scripts": "dist/index.js" 18 | }, 19 | "files": [ 20 | "dist", 21 | "babel.js", 22 | "config.js", 23 | "eslint.js", 24 | "husky.js", 25 | "jest.js", 26 | "prettier.js", 27 | "shared-tsconfig.json" 28 | ], 29 | "scripts": { 30 | "build": "node src build", 31 | "format": "node src format", 32 | "lint": "node src lint", 33 | "test": "node src test", 34 | "test:update": "node src test --updateSnapshot", 35 | "validate": "node src validate" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "kentcdodds", 40 | "kentcdodds/jest" 41 | ], 42 | "rules": { 43 | "no-console": "off", 44 | "no-nested-ternary": "off", 45 | "no-process-exit": "off", 46 | "no-useless-catch": "off", 47 | "import/extensions": "off", 48 | "import/no-dynamic-require": "off", 49 | "import/no-import-module-exports": "off", 50 | "import/no-unassigned-import": "off" 51 | } 52 | }, 53 | "eslintIgnore": [ 54 | "node_modules", 55 | "coverage", 56 | "dist" 57 | ], 58 | "dependencies": { 59 | "@babel/cli": "^7.18.10", 60 | "@babel/core": "^7.18.13", 61 | "@babel/plugin-transform-class-properties": "^7.18.6", 62 | "@babel/plugin-transform-modules-commonjs": "^7.18.6", 63 | "@babel/plugin-transform-runtime": "^7.18.10", 64 | "@babel/preset-env": "^7.18.10", 65 | "@babel/preset-react": "^7.18.6", 66 | "@babel/preset-typescript": "^7.18.6", 67 | "@babel/runtime": "^7.18.9", 68 | "@rollup/plugin-babel": "^6.0.4", 69 | "@rollup/plugin-commonjs": "^25.0.7", 70 | "@rollup/plugin-json": "^6.1.0", 71 | "@rollup/plugin-node-resolve": "^15.2.3", 72 | "@rollup/plugin-replace": "^5.0.5", 73 | "@rollup/plugin-terser": "^0.4.4", 74 | "@types/jest": "^29.4.0", 75 | "arrify": "^2.0.1", 76 | "babel-jest": "^29.4.1", 77 | "babel-plugin-macros": "^3.1.0", 78 | "babel-plugin-minify-dead-code-elimination": "^0.5.2", 79 | "babel-plugin-module-resolver": "^5.0.0", 80 | "babel-plugin-transform-inline-environment-variables": "^0.4.4", 81 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 82 | "browserslist": "4.21.3", 83 | "builtin-modules": "^3.3.0", 84 | "chalk": "^4.1.2", 85 | "concurrently": "^7.3.0", 86 | "cosmiconfig": "^7.0.1", 87 | "cpy": "npm:@brickdoc/cpy@8.1.2-patch.1", 88 | "cross-env": "^7.0.3", 89 | "cross-spawn": "^7.0.3", 90 | "doctoc": "^2.2.0", 91 | "eslint": "^8.23.0", 92 | "eslint-config-kentcdodds": "^20.4.0", 93 | "glob": "^8.0.3", 94 | "husky": "^4.3.8", 95 | "is-ci": "^3.0.1", 96 | "jest": "^29.4.1", 97 | "jest-environment-jsdom": "^29.4.1", 98 | "jest-serializer-path": "^0.1.15", 99 | "jest-snapshot-serializer-raw": "^1.2.0", 100 | "jest-watch-typeahead": "^2.2.2", 101 | "lint-staged": "^12.5.0", 102 | "lodash.camelcase": "^4.3.0", 103 | "lodash.has": "^4.5.2", 104 | "lodash.omit": "^4.5.0", 105 | "mkdirp": "^1.0.4", 106 | "prettier": "^3", 107 | "read-pkg-up": "^7.0.1", 108 | "resolve": "^1.22.1", 109 | "rimraf": "^3.0.2", 110 | "rollup": "^4.12.0", 111 | "rollup-plugin-polyfill-node": "^0.13.0", 112 | "semver": "^7.3.7", 113 | "which": "^2.0.2", 114 | "yargs-parser": "^21.1.1" 115 | }, 116 | "devDependencies": { 117 | "jest-in-case": "^1.0.2", 118 | "slash": "^3.0.0" 119 | }, 120 | "overrides": { 121 | "caniuse-lite": "1.0.30001553" 122 | }, 123 | "engines": { 124 | "node": "^16.10.0 || >=17.0.0", 125 | "npm": ">=6" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /prettier.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/config/prettierrc') 2 | -------------------------------------------------------------------------------- /shared-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["../../src/**/*"], 4 | "compilerOptions": { 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "noEmit": true, 9 | "strict": true, 10 | "jsx": "react", 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": "../../src", 14 | "paths": { 15 | "*": ["*", "../tests/*"] 16 | }, 17 | "preserveWatchOutput": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/__mocks__/cross-spawn.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sync: jest.fn(() => ({status: 0})), 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`format calls node with the script path and args including inspect-brk argument: format script 1`] = `node --inspect-brk=3080 /src/scripts/test.js --no-watch`; 4 | 5 | exports[`format calls node with the script path and args: format script 1`] = `node /src/scripts/test.js --no-watch`; 6 | 7 | exports[`format does not log for other signals: format signal 1`] = `[]`; 8 | 9 | exports[`format logs for SIGKILL signal: format signal 1`] = ` 10 | [ 11 | [ 12 | The script "lint" failed because the process exited too early. This probably means the system ran out of memory or someone called \`kill -9\` on the process., 13 | ], 14 | ] 15 | `; 16 | 17 | exports[`format logs for SIGTERM signal: format signal 1`] = ` 18 | [ 19 | [ 20 | The script "build" failed because the process exited too early. Someone might have called \`kill\` or \`killall\`, or the system could be shutting down., 21 | ], 22 | ] 23 | `; 24 | 25 | exports[`format logs help with no args: format snapshotLog 1`] = ` 26 | [ 27 | [ 28 | 29 | Usage: ../ [script] [--flags] 30 | 31 | Available Scripts: 32 | build 33 | format 34 | lint 35 | pre-commit 36 | test 37 | typecheck 38 | validate 39 | 40 | Options: 41 | All options depend on the script. Docs will be improved eventually, but for most scripts you can assume that the args you pass will be forwarded to the respective tool that's being run under the hood. 42 | 43 | May the force be with you. 44 | , 45 | ], 46 | ] 47 | `; 48 | 49 | exports[`format throws unknown script: format error 1`] = `Unknown script "unknown-script".`; 50 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/utils.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getConcurrentlyArgs gives good args to pass to concurrently 1`] = ` 4 | [ 5 | --kill-others-on-fail, 6 | --prefix, 7 | [{name}], 8 | --names, 9 | build,lint,test,validate,a,b,c,d,e,f,g,h,i,j, 10 | --prefix-colors, 11 | bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white,bgCyan.bold.white,bgWhite.bold.white,bgRed.bold.white,bgBlack.bold.white,bgYellow.bold.white,bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white,bgCyan.bold.white,bgWhite.bold.white,bgRed.bold.white, 12 | "echo build", 13 | "echo lint", 14 | "echo test", 15 | "echo validate", 16 | "echo a", 17 | "echo b", 18 | "echo c", 19 | "echo d", 20 | "echo e", 21 | "echo f", 22 | "echo g", 23 | "echo h", 24 | "echo i", 25 | "echo j", 26 | ] 27 | `; 28 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import slash from 'slash' 3 | import cases from 'jest-in-case' 4 | 5 | const projectRoot = path.join(__dirname, '../../') 6 | 7 | expect.addSnapshotSerializer({ 8 | print: val => slash(val.replace(projectRoot, '/')), 9 | test: val => typeof val === 'string' && val.includes(projectRoot), 10 | }) 11 | 12 | cases( 13 | 'format', 14 | ({snapshotLog = false, throws = false, signal = false, args = []}) => { 15 | // beforeEach 16 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 17 | const originalExit = process.exit 18 | const originalArgv = process.argv 19 | const originalLog = console.log 20 | process.exit = jest.fn() 21 | console.log = jest.fn() 22 | try { 23 | // tests 24 | process.argv = ['node', '../', ...args] 25 | crossSpawnSyncMock.mockClear() 26 | if (signal) { 27 | crossSpawnSyncMock.mockReturnValueOnce({result: 1, signal}) 28 | } 29 | require('../') 30 | if (snapshotLog) { 31 | expect(console.log.mock.calls).toMatchSnapshot('format snapshotLog') 32 | } else if (signal) { 33 | expect(process.exit).toHaveBeenCalledTimes(1) 34 | expect(process.exit).toHaveBeenCalledWith(1) 35 | expect(console.log.mock.calls).toMatchSnapshot('format signal') 36 | } else { 37 | expect(crossSpawnSyncMock).toHaveBeenCalledTimes(1) 38 | const [firstCall] = crossSpawnSyncMock.mock.calls 39 | const [script, calledArgs] = firstCall 40 | expect([script, ...calledArgs].join(' ')).toMatchSnapshot( 41 | 'format script', 42 | ) 43 | } 44 | } catch (error) { 45 | if (throws) { 46 | expect(error.message).toMatchSnapshot('format error') 47 | } else { 48 | throw error 49 | } 50 | } finally { 51 | // afterEach 52 | process.exit = originalExit 53 | process.argv = originalArgv 54 | console.log = originalLog 55 | jest.resetModules() 56 | } 57 | }, 58 | { 59 | 'calls node with the script path and args': { 60 | args: ['test', '--no-watch'], 61 | }, 62 | 'calls node with the script path and args including inspect-brk argument': { 63 | args: ['--inspect-brk=3080', 'test', '--no-watch'], 64 | }, 65 | 'throws unknown script': { 66 | args: ['unknown-script'], 67 | throws: true, 68 | }, 69 | 'logs help with no args': { 70 | snapshotLog: true, 71 | }, 72 | 'logs for SIGKILL signal': { 73 | args: ['lint'], 74 | signal: 'SIGKILL', 75 | }, 76 | 'logs for SIGTERM signal': { 77 | args: ['build'], 78 | signal: 'SIGTERM', 79 | }, 80 | 'does not log for other signals': { 81 | args: ['test'], 82 | signal: 'SIGBREAK', 83 | }, 84 | }, 85 | ) 86 | 87 | /* eslint complexity:0 */ 88 | -------------------------------------------------------------------------------- /src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | jest.mock('read-pkg-up', () => ({ 2 | sync: jest.fn(() => ({packageJson: {}, path: '/blah/package.json'})), 3 | })) 4 | jest.mock('which', () => ({sync: jest.fn(() => {})})) 5 | 6 | jest.mock('cosmiconfig', () => { 7 | const cosmiconfigExports = jest.requireActual('cosmiconfig') 8 | return {...cosmiconfigExports, cosmiconfigSync: jest.fn()} 9 | }) 10 | 11 | jest.mock('cpy') 12 | 13 | let whichSyncMock, readPkgUpSyncMock, cpy, nodePath 14 | 15 | beforeEach(() => { 16 | jest.resetModules() 17 | nodePath = require('path') 18 | whichSyncMock = require('which').sync 19 | readPkgUpSyncMock = require('read-pkg-up').sync 20 | cpy = require('cpy') 21 | }) 22 | 23 | test('package is the package.json', () => { 24 | const myPkg = {name: 'blah'} 25 | mockPkg({package: myPkg}) 26 | expect(require('../utils').pkg).toBe(myPkg) 27 | }) 28 | 29 | test('appDirectory is the dirname to the package.json', () => { 30 | const pkgPath = '/some/path/to' 31 | mockPkg({path: `${pkgPath}/package.json`}) 32 | expect(require('../utils').appDirectory).toBe(pkgPath) 33 | }) 34 | 35 | test('resolveKcdScripts resolves to src/index.js when in the kcd-scripts package', () => { 36 | mockPkg({package: {name: 'kcd-scripts'}}) 37 | expect(require('../utils').resolveKcdScripts()).toBe( 38 | require.resolve('../').replace(process.cwd(), '.'), 39 | ) 40 | }) 41 | 42 | test('resolveKcdScripts resolves to kcd-scripts if not in the kcd-scripts package', () => { 43 | mockPkg({package: {name: 'not-kcd-scripts'}}) 44 | whichSyncMock.mockImplementationOnce(() => require.resolve('../')) 45 | expect(require('../utils').resolveKcdScripts()).toBe('kcd-scripts') 46 | }) 47 | 48 | test(`resolveBin resolves to the full path when it's not in $PATH`, () => { 49 | expect(require('../utils').resolveBin('cross-env')).toBe( 50 | require.resolve('cross-env/src/bin/cross-env').replace(process.cwd(), '.'), 51 | ) 52 | }) 53 | 54 | test(`resolveBin resolves to the binary if it's in $PATH`, () => { 55 | whichSyncMock.mockImplementationOnce(() => 56 | require.resolve('cross-env/src/bin/cross-env').replace(process.cwd(), '.'), 57 | ) 58 | expect(require('../utils').resolveBin('cross-env')).toBe('cross-env') 59 | expect(whichSyncMock).toHaveBeenCalledTimes(1) 60 | expect(whichSyncMock).toHaveBeenCalledWith('cross-env') 61 | }) 62 | 63 | describe('for windows', () => { 64 | let realpathSync 65 | 66 | beforeEach(() => { 67 | jest.doMock('fs', () => ({realpathSync: jest.fn()})) 68 | realpathSync = require('fs').realpathSync 69 | }) 70 | afterEach(() => { 71 | jest.unmock('fs') 72 | }) 73 | 74 | test('resolveBin resolves to .bin path when which returns a windows-style cmd', () => { 75 | const fullBinPath = '\\project\\node_modules\\.bin\\concurrently.CMD' 76 | realpathSync.mockImplementation(() => fullBinPath) 77 | expect(require('../utils').resolveBin('concurrently')).toBe(fullBinPath) 78 | expect(realpathSync).toHaveBeenCalledTimes(2) 79 | }) 80 | }) 81 | 82 | test('getConcurrentlyArgs gives good args to pass to concurrently', () => { 83 | expect( 84 | require('../utils').getConcurrentlyArgs({ 85 | build: 'echo build', 86 | lint: 'echo lint', 87 | test: 'echo test', 88 | validate: 'echo validate', 89 | a: 'echo a', 90 | b: 'echo b', 91 | c: 'echo c', 92 | d: 'echo d', 93 | e: 'echo e', 94 | f: 'echo f', 95 | g: 'echo g', 96 | h: 'echo h', 97 | i: 'echo i', 98 | j: 'echo j', 99 | }), 100 | ).toMatchSnapshot() 101 | }) 102 | 103 | test('parseEnv parses the existing environment variable', () => { 104 | const globals = {react: 'React', 'prop-types': 'PropTypes'} 105 | process.env.BUILD_GLOBALS = JSON.stringify(globals) 106 | expect(require('../utils').parseEnv('BUILD_GLOBALS')).toEqual(globals) 107 | delete process.env.BUILD_GLOBALS 108 | }) 109 | 110 | test(`parseEnv returns the default if the environment variable doesn't exist`, () => { 111 | const defaultVal = {hello: 'world'} 112 | expect(require('../utils').parseEnv('DOES_NOT_EXIST', defaultVal)).toBe( 113 | defaultVal, 114 | ) 115 | }) 116 | 117 | test('ifAnyDep returns the true argument if true and false argument if false', () => { 118 | mockPkg({package: {peerDependencies: {react: '*'}}}) 119 | const t = {a: 'b'} 120 | const f = {c: 'd'} 121 | expect(require('../utils').ifAnyDep('react', t, f)).toBe(t) 122 | expect(require('../utils').ifAnyDep('preact', t, f)).toBe(f) 123 | }) 124 | 125 | test('ifAnyDep works with arrays of dependencies', () => { 126 | mockPkg({package: {peerDependencies: {react: '*'}}}) 127 | const t = {a: 'b'} 128 | const f = {c: 'd'} 129 | expect(require('../utils').ifAnyDep(['preact', 'react'], t, f)).toBe(t) 130 | expect(require('../utils').ifAnyDep(['preact', 'webpack'], t, f)).toBe(f) 131 | }) 132 | 133 | test('ifScript returns the true argument if true and the false argument if false', () => { 134 | mockPkg({package: {scripts: {build: 'echo build'}}}) 135 | const t = {e: 'f'} 136 | const f = {g: 'h'} 137 | expect(require('../utils').ifScript('build', t, f)).toBe(t) 138 | expect(require('../utils').ifScript('lint', t, f)).toBe(f) 139 | }) 140 | 141 | test('ifFile returns the true argument if true and the false argument if false', () => { 142 | mockPkg({path: require.resolve('../../package.json')}) 143 | const t = {e: 'f'} 144 | const f = {g: 'h'} 145 | expect(require('../utils').ifFile('package.json', t, f)).toBe(t) 146 | expect(require('../utils').ifFile('does-not-exist.blah', t, f)).toBe(f) 147 | }) 148 | 149 | test('hasLocalConfiguration returns false if no local configuration found', () => { 150 | mockCosmiconfig() 151 | 152 | expect(require('../utils').hasLocalConfig('module')).toBe(false) 153 | }) 154 | 155 | test('hasLocalConfig returns true if a local configuration found', () => { 156 | mockCosmiconfig({config: {}, filepath: 'path/to/config'}) 157 | 158 | expect(require('../utils').hasLocalConfig('module')).toBe(true) 159 | }) 160 | 161 | test('hasLocalConfiguration returns true if a local config found and it is empty', () => { 162 | mockCosmiconfig({isEmpty: true}) 163 | 164 | expect(require('../utils').hasLocalConfig('module')).toBe(true) 165 | }) 166 | 167 | test('should generate typescript definitions into provided folder', async () => { 168 | whichSyncMock.mockImplementationOnce(() => require.resolve('../')) 169 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 170 | await require('../utils').generateTypeDefs('destination folder') 171 | expect(crossSpawnSyncMock).toHaveBeenCalledTimes(1) 172 | const args = crossSpawnSyncMock.mock.calls[0][1] 173 | const outDirIndex = args.findIndex(arg => arg === '--outDir') + 1 174 | 175 | expect(args[outDirIndex]).toBe('destination folder') 176 | 177 | expect(cpy).toHaveBeenCalledTimes(1) 178 | expect(cpy).toHaveBeenCalledWith('**/*.d.ts', '../dist', { 179 | cwd: `${nodePath.sep}blah${nodePath.sep}src`, 180 | parents: true, 181 | }) 182 | }) 183 | 184 | function mockPkg({package: pkg = {}, path = '/blah/package.json'}) { 185 | readPkgUpSyncMock.mockImplementationOnce(() => ({packageJson: pkg, path})) 186 | } 187 | 188 | function mockCosmiconfig(result = null) { 189 | const {cosmiconfigSync} = require('cosmiconfig') 190 | 191 | cosmiconfigSync.mockImplementationOnce(() => ({search: () => result})) 192 | } 193 | 194 | test.each([ 195 | {format: 'cjs', extension: '.cjs'}, 196 | {format: 'esm', extension: '.mjs'}, 197 | {format: 'umd', extension: '.js'}, 198 | {format: 'amd', extension: '.js'}, 199 | ])( 200 | 'file extension in rollupOutput with $format should be $extension', 201 | ({format, extension}) => { 202 | expect( 203 | require('../utils').getRollupOutput(format).filename.endsWith(extension), 204 | ).toBeTruthy() 205 | }, 206 | ) 207 | -------------------------------------------------------------------------------- /src/config/__tests__/umbrella.js: -------------------------------------------------------------------------------- 1 | test('requiring some files does not blow up', () => { 2 | require('../babel-transform') 3 | require('../babelrc') 4 | require('../eslintrc') 5 | require('../jest.config') 6 | require('../lintstagedrc') 7 | require('../prettierrc') 8 | require('../rollup.config') 9 | require('../').getRollupConfig() 10 | }) 11 | -------------------------------------------------------------------------------- /src/config/babel-transform.js: -------------------------------------------------------------------------------- 1 | const babelJest = require('babel-jest').default 2 | 3 | module.exports = babelJest.createTransformer({ 4 | presets: [require.resolve('./babelrc')], 5 | }) 6 | -------------------------------------------------------------------------------- /src/config/babelrc.js: -------------------------------------------------------------------------------- 1 | const browserslist = require('browserslist') 2 | const semver = require('semver') 3 | 4 | const { 5 | ifDep, 6 | ifAnyDep, 7 | ifTypescript, 8 | parseEnv, 9 | appDirectory, 10 | pkg, 11 | } = require('../utils') 12 | 13 | const {BABEL_ENV, NODE_ENV, BUILD_FORMAT} = process.env 14 | const isTest = (BABEL_ENV || NODE_ENV) === 'test' 15 | const isPreact = parseEnv('BUILD_PREACT', false) 16 | const isRollup = parseEnv('BUILD_ROLLUP', false) 17 | const isUMD = BUILD_FORMAT === 'umd' 18 | const isCJS = BUILD_FORMAT === 'cjs' 19 | const isWebpack = parseEnv('BUILD_WEBPACK', false) 20 | const isMinify = parseEnv('BUILD_MINIFY', false) 21 | const treeshake = parseEnv('BUILD_TREESHAKE', isRollup || isWebpack) 22 | const alias = parseEnv( 23 | 'BUILD_ALIAS', 24 | isPreact ? {react: 'preact/compat'} : null, 25 | ) 26 | 27 | const hasBabelRuntimeDep = Boolean( 28 | pkg.dependencies && pkg.dependencies['@babel/runtime'], 29 | ) 30 | const RUNTIME_HELPERS_WARN = 31 | 'You should add @babel/runtime as dependency to your package. It will allow reusing "babel helpers" from node_modules rather than bundling their copies into your files.' 32 | 33 | if (!treeshake && !hasBabelRuntimeDep && !isTest) { 34 | throw new Error(RUNTIME_HELPERS_WARN) 35 | } else if (treeshake && !isUMD && !hasBabelRuntimeDep) { 36 | console.warn(RUNTIME_HELPERS_WARN) 37 | } 38 | 39 | /** 40 | * use the strategy declared by browserslist to load browsers configuration. 41 | * fallback to the default if don't found custom configuration 42 | * @see https://github.com/browserslist/browserslist/blob/master/node.js#L139 43 | */ 44 | const browsersConfig = browserslist.loadConfig({path: appDirectory}) || [ 45 | 'defaults', 46 | ] 47 | 48 | const envTargets = isTest 49 | ? {node: 'current'} 50 | : isWebpack || isRollup 51 | ? {browsers: browsersConfig} 52 | : {node: getNodeVersion(pkg)} 53 | const envOptions = {modules: false, loose: true, targets: envTargets} 54 | 55 | module.exports = () => ({ 56 | presets: [ 57 | [require.resolve('@babel/preset-env'), envOptions], 58 | ifAnyDep( 59 | ['react', 'preact'], 60 | [ 61 | require.resolve('@babel/preset-react'), 62 | {pragma: isPreact ? ifDep('react', 'React.h', 'h') : undefined}, 63 | ], 64 | ), 65 | ifTypescript([require.resolve('@babel/preset-typescript')]), 66 | ].filter(Boolean), 67 | plugins: [ 68 | [ 69 | require.resolve('@babel/plugin-transform-runtime'), 70 | {useESModules: treeshake && !isCJS}, 71 | ], 72 | require.resolve('babel-plugin-macros'), 73 | alias 74 | ? [ 75 | require.resolve('babel-plugin-module-resolver'), 76 | {root: ['./src'], alias}, 77 | ] 78 | : null, 79 | ifAnyDep( 80 | ['react', 'preact'], 81 | [ 82 | require.resolve('babel-plugin-transform-react-remove-prop-types'), 83 | isPreact ? {removeImport: true} : {mode: 'unsafe-wrap'}, 84 | ], 85 | ), 86 | isUMD 87 | ? require.resolve('babel-plugin-transform-inline-environment-variables') 88 | : null, 89 | [ 90 | require.resolve('@babel/plugin-transform-class-properties'), 91 | {loose: true}, 92 | ], 93 | isMinify 94 | ? require.resolve('babel-plugin-minify-dead-code-elimination') 95 | : null, 96 | treeshake 97 | ? null 98 | : require.resolve('@babel/plugin-transform-modules-commonjs'), 99 | ].filter(Boolean), 100 | }) 101 | 102 | function getNodeVersion({engines: {node: nodeVersion = '10.13'} = {}}) { 103 | const oldestVersion = semver 104 | .validRange(nodeVersion) 105 | .replace(/[>=<|]/g, ' ') 106 | .split(' ') 107 | .filter(Boolean) 108 | .sort(semver.compare)[0] 109 | if (!oldestVersion) { 110 | throw new Error( 111 | `Unable to determine the oldest version in the range in your package.json at engines.node: "${nodeVersion}". Please attempt to make it less ambiguous.`, 112 | ) 113 | } 114 | return oldestVersion 115 | } 116 | -------------------------------------------------------------------------------- /src/config/eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | build/ 5 | out/ 6 | .next/ 7 | -------------------------------------------------------------------------------- /src/config/eslintrc.js: -------------------------------------------------------------------------------- 1 | const {ifAnyDep} = require('../utils') 2 | 3 | module.exports = { 4 | extends: [ 5 | require.resolve('eslint-config-kentcdodds'), 6 | require.resolve('eslint-config-kentcdodds/jest'), 7 | ifAnyDep('react', require.resolve('eslint-config-kentcdodds/jsx-a11y')), 8 | ifAnyDep('react', require.resolve('eslint-config-kentcdodds/react')), 9 | ].filter(Boolean), 10 | rules: {}, 11 | } 12 | -------------------------------------------------------------------------------- /src/config/huskyrc.js: -------------------------------------------------------------------------------- 1 | const {resolveKcdScripts} = require('../utils') 2 | 3 | const kcdScripts = resolveKcdScripts() 4 | 5 | module.exports = { 6 | hooks: { 7 | 'pre-commit': `"${kcdScripts}" pre-commit`, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babel: require('./babelrc'), 3 | eslint: require('./eslintrc'), 4 | husky: require('./huskyrc'), 5 | jest: require('./jest.config'), 6 | lintStaged: require('./lintstagedrc'), 7 | prettier: require('./prettierrc'), 8 | getRollupConfig: () => require('./rollup.config'), 9 | } 10 | -------------------------------------------------------------------------------- /src/config/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const {ifAnyDep, hasFile, hasPkgProp} = require('../utils') 3 | 4 | const here = p => path.join(__dirname, p) 5 | 6 | const useBuiltInBabelConfig = !hasFile('.babelrc') && !hasPkgProp('babel') 7 | 8 | const ignores = [ 9 | '/node_modules/', 10 | '/__fixtures__/', 11 | '/fixtures/', 12 | '/__tests__/helpers/', 13 | '/__tests__/utils/', 14 | '__mocks__', 15 | ] 16 | 17 | /** @type {import('@jest/types').Config.InitialOptions} */ 18 | const jestConfig = { 19 | roots: ['/src'], 20 | testEnvironment: ifAnyDep( 21 | ['webpack', 'rollup', 'react', 'preact'], 22 | 'jsdom', 23 | 'node', 24 | ), 25 | testEnvironmentOptions: { 26 | url: 'http://localhost', 27 | }, 28 | moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'], 29 | modulePaths: ['/src', 'shared', '/tests'], 30 | collectCoverageFrom: ['src/**/*.+(js|jsx|ts|tsx)'], 31 | testMatch: ['**/__tests__/**/*.+(js|jsx|ts|tsx)'], 32 | testPathIgnorePatterns: [...ignores], 33 | coveragePathIgnorePatterns: [...ignores, 'src/(umd|cjs|esm)-entry.js$'], 34 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 35 | coverageThreshold: { 36 | global: { 37 | branches: 100, 38 | functions: 100, 39 | lines: 100, 40 | statements: 100, 41 | }, 42 | }, 43 | watchPlugins: [ 44 | require.resolve('jest-watch-typeahead/filename'), 45 | require.resolve('jest-watch-typeahead/testname'), 46 | ], 47 | snapshotSerializers: [ 48 | require.resolve('jest-serializer-path'), 49 | require.resolve('jest-snapshot-serializer-raw/always'), 50 | ], 51 | } 52 | 53 | const setupFiles = [ 54 | 'tests/setup-env.js', 55 | 'tests/setup-env.ts', 56 | 'tests/setup-env.tsx', 57 | ] 58 | for (const setupFile of setupFiles) { 59 | if (hasFile(setupFile)) { 60 | jestConfig.setupFilesAfterEnv = [`/${setupFile}`] 61 | } 62 | } 63 | 64 | if (useBuiltInBabelConfig) { 65 | jestConfig.transform = {'^.+\\.(js|jsx|ts|tsx)$': here('./babel-transform')} 66 | } 67 | 68 | module.exports = jestConfig 69 | -------------------------------------------------------------------------------- /src/config/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const {resolveKcdScripts, resolveBin} = require('../utils') 2 | 3 | const kcdScripts = resolveKcdScripts() 4 | const doctoc = resolveBin('doctoc') 5 | 6 | module.exports = { 7 | 'README.md': [`${doctoc} --maxlevel 3 --notitle`], 8 | '*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue)': [ 9 | `${kcdScripts} format`, 10 | `${kcdScripts} lint`, 11 | `${kcdScripts} test --findRelatedTests`, 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /src/config/prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | build/ 5 | out/ 6 | .next/ 7 | -------------------------------------------------------------------------------- /src/config/prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Options} */ 2 | module.exports = { 3 | arrowParens: 'avoid', 4 | bracketSameLine: false, 5 | bracketSpacing: false, 6 | embeddedLanguageFormatting: 'auto', 7 | endOfLine: 'lf', 8 | htmlWhitespaceSensitivity: 'css', 9 | insertPragma: false, 10 | jsxSingleQuote: false, 11 | printWidth: 80, 12 | proseWrap: 'always', 13 | quoteProps: 'as-needed', 14 | requirePragma: false, 15 | semi: false, 16 | singleAttributePerLine: false, 17 | singleQuote: true, 18 | tabWidth: 2, 19 | trailingComma: 'all', 20 | useTabs: false, 21 | } 22 | -------------------------------------------------------------------------------- /src/config/rollup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const builtInModules = require('builtin-modules') 3 | const {babel: rollupBabel} = require('@rollup/plugin-babel') 4 | const commonjs = require('@rollup/plugin-commonjs') 5 | const json = require('@rollup/plugin-json') 6 | const { 7 | DEFAULTS: nodeResolveDefaults, 8 | nodeResolve, 9 | } = require('@rollup/plugin-node-resolve') 10 | const replace = require('@rollup/plugin-replace') 11 | const camelcase = require('lodash.camelcase') 12 | const omit = require('lodash.omit') 13 | const nodePolyfills = require('rollup-plugin-polyfill-node') 14 | const terser = require('@rollup/plugin-terser') 15 | const { 16 | pkg, 17 | hasFile, 18 | hasPkgProp, 19 | hasDep, 20 | hasTypescript, 21 | parseEnv, 22 | getRollupInputs, 23 | getRollupOutput, 24 | uniq, 25 | writeExtraEntry, 26 | } = require('../utils') 27 | 28 | const here = p => path.join(__dirname, p) 29 | const capitalize = s => s[0].toUpperCase() + s.slice(1) 30 | 31 | const minify = parseEnv('BUILD_MINIFY', false) 32 | const format = process.env.BUILD_FORMAT 33 | const isPreact = parseEnv('BUILD_PREACT', false) 34 | const isNode = parseEnv('BUILD_NODE', false) 35 | const name = process.env.BUILD_NAME || capitalize(camelcase(pkg.name)) 36 | 37 | const esm = format === 'esm' 38 | const umd = format === 'umd' 39 | 40 | const defaultGlobals = Object.keys(pkg.peerDependencies || {}).reduce( 41 | (deps, dep) => { 42 | deps[dep] = capitalize(camelcase(dep)) 43 | return deps 44 | }, 45 | {}, 46 | ) 47 | 48 | const deps = Object.keys(pkg.dependencies || {}) 49 | const peerDeps = Object.keys(pkg.peerDependencies || {}) 50 | const defaultExternal = builtInModules.concat( 51 | umd ? peerDeps : deps.concat(peerDeps), 52 | ) 53 | 54 | const globals = parseEnv( 55 | 'BUILD_GLOBALS', 56 | isPreact ? Object.assign(defaultGlobals, {preact: 'preact'}) : defaultGlobals, 57 | ) 58 | const external = parseEnv( 59 | 'BUILD_EXTERNAL', 60 | isPreact ? defaultExternal.concat(['preact', 'prop-types']) : defaultExternal, 61 | ).filter((e, i, arry) => arry.indexOf(e) === i) 62 | 63 | if (isPreact) { 64 | delete globals.react 65 | delete globals['prop-types'] // TODO: is this necessary? 66 | external.splice(external.indexOf('react'), 1) 67 | } 68 | 69 | const externalPattern = new RegExp(`^(${external.join('|')})($|/)`) 70 | 71 | function externalPredicate(id) { 72 | const isDep = external.length > 0 && externalPattern.test(id) 73 | if (umd) { 74 | // for UMD, we want to bundle all non-peer deps 75 | return isDep 76 | } 77 | // for esm/cjs we want to make all node_modules external 78 | // TODO: support bundledDependencies if someone needs it ever... 79 | const isNodeModule = id.includes('node_modules') 80 | const isRelative = id.startsWith('.') 81 | return isDep || (!isRelative && !path.isAbsolute(id)) || isNodeModule 82 | } 83 | 84 | const useBuiltinConfig = 85 | !hasFile('.babelrc') && 86 | !hasFile('.babelrc.js') && 87 | !hasFile('babel.config.js') && 88 | !hasPkgProp('babel') 89 | const babelPresets = useBuiltinConfig ? [here('../config/babelrc.js')] : [] 90 | 91 | const replacements = Object.entries( 92 | umd ? process.env : omit(process.env, ['NODE_ENV']), 93 | ).reduce((acc, [key, value]) => { 94 | let val 95 | if (value === 'true' || value === 'false' || Number.isInteger(+value)) { 96 | val = value 97 | } else { 98 | val = JSON.stringify(value) 99 | } 100 | acc[`process.env.${key}`] = val 101 | return acc 102 | }, {}) 103 | 104 | const extensions = hasTypescript 105 | ? [...nodeResolveDefaults.extensions, '.ts', '.tsx'] 106 | : nodeResolveDefaults.extensions 107 | 108 | const input = getRollupInputs() 109 | const codeSplitting = input.length > 1 110 | 111 | if ( 112 | codeSplitting && 113 | uniq(input.map(single => path.basename(single))).length !== input.length 114 | ) { 115 | throw new Error( 116 | 'Filenames of code-splitted entries should be unique to get deterministic output filenames.' + 117 | `\nReceived those: ${input}.`, 118 | ) 119 | } 120 | 121 | const {dirpath, filename} = getRollupOutput() 122 | 123 | const output = [ 124 | { 125 | name, 126 | ...(codeSplitting 127 | ? {dir: path.join(dirpath, format)} 128 | : {file: path.join(dirpath, filename)}), 129 | format: esm ? 'es' : format, 130 | exports: esm ? 'named' : 'auto', 131 | globals, 132 | }, 133 | ] 134 | 135 | /** @returns {import('rollup').RollupOptions} */ 136 | module.exports = { 137 | input: codeSplitting ? input : input[0], 138 | output, 139 | external: externalPredicate, 140 | plugins: [ 141 | isNode ? nodePolyfills() : null, 142 | nodeResolve({ 143 | preferBuiltins: isNode, 144 | mainFields: ['module', 'main', 'jsnext', 'browser'], 145 | extensions, 146 | }), 147 | commonjs({include: /node_modules/i}), 148 | json(), 149 | rollupBabel({ 150 | presets: babelPresets, 151 | babelrc: !useBuiltinConfig, 152 | babelHelpers: hasDep('@babel/runtime') ? 'runtime' : 'bundled', 153 | extensions, 154 | }), 155 | replace(replacements), 156 | minify ? terser() : null, 157 | codeSplitting && 158 | ((writes = 0) => ({ 159 | onwrite() { 160 | if (++writes !== input.length) { 161 | return 162 | } 163 | 164 | input 165 | .filter(single => single.indexOf('index.js') === -1) 166 | .forEach(single => { 167 | const chunk = path.basename(single) 168 | 169 | writeExtraEntry(chunk.replace(/\..+$/, ''), { 170 | cjs: `${dirpath}/cjs/${chunk}`, 171 | esm: `${dirpath}/esm/${chunk}`, 172 | }) 173 | }) 174 | }, 175 | }))(), 176 | ].filter(Boolean), 177 | } 178 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | let shouldThrow = false 3 | 4 | try { 5 | const [major, minor] = process.version.slice(1).split('.').map(Number) 6 | shouldThrow = 7 | require(`${process.cwd()}/package.json`).name === 'kcd-scripts' && 8 | (major < 10 || (major === 10 && minor < 18)) 9 | } catch (error) { 10 | // ignore 11 | } 12 | 13 | if (shouldThrow) { 14 | throw new Error( 15 | 'You must use Node version 10.18 or greater to run the scripts within kcd-scripts, because we dogfood the untranspiled version of the scripts.', 16 | ) 17 | } 18 | 19 | require('./run-script') 20 | -------------------------------------------------------------------------------- /src/run-script.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const spawn = require('cross-spawn') 3 | const glob = require('glob') 4 | const {toPOSIX} = require('./utils') 5 | 6 | const [executor, ignoredBin, script] = process.argv 7 | 8 | if (script && script !== '--help' && script !== 'help') { 9 | spawnScript() 10 | } else { 11 | const scriptsPath = path.join(__dirname, 'scripts/') 12 | const scriptsAvailable = glob.sync( 13 | toPOSIX(path.join(__dirname, 'scripts', '*')), 14 | ) 15 | // `glob.sync` returns paths with unix style path separators even on Windows. 16 | // So we normalize it before attempting to strip out the scripts path. 17 | const scriptsAvailableMessage = scriptsAvailable 18 | .map(path.normalize) 19 | .map(s => 20 | s 21 | .replace(scriptsPath, '') 22 | .replace(/__tests__/, '') 23 | .replace(/\.js$/, ''), 24 | ) 25 | .filter(Boolean) 26 | .join('\n ') 27 | .trim() 28 | const fullMessage = ` 29 | Usage: ${ignoredBin} [script] [--flags] 30 | 31 | Available Scripts: 32 | ${scriptsAvailableMessage} 33 | 34 | Options: 35 | All options depend on the script. Docs will be improved eventually, but for most scripts you can assume that the args you pass will be forwarded to the respective tool that's being run under the hood. 36 | 37 | May the force be with you. 38 | `.trim() 39 | console.log(`\n${fullMessage}\n`) 40 | } 41 | 42 | function getEnv() { 43 | // this is required to address an issue in cross-spawn 44 | // https://github.com/kentcdodds/kcd-scripts/issues/4 45 | return Object.keys(process.env) 46 | .filter(key => process.env[key] !== undefined) 47 | .reduce( 48 | (envCopy, key) => { 49 | envCopy[key] = process.env[key] 50 | return envCopy 51 | }, 52 | { 53 | [`SCRIPTS_${script.toUpperCase().replace(/-/g, '_')}`]: true, 54 | }, 55 | ) 56 | } 57 | 58 | function spawnScript() { 59 | // get all the arguments of the script and find the position of our script commands 60 | const args = process.argv.slice(2) 61 | const scriptIndex = args.findIndex(x => 62 | [ 63 | 'build', 64 | 'format', 65 | 'lint', 66 | 'pre-commit', 67 | 'test', 68 | 'validate', 69 | 'typecheck', 70 | ].includes(x), 71 | ) 72 | 73 | // Extract the node arguments so we can pass them to node later on 74 | const buildCommand = scriptIndex === -1 ? args[0] : args[scriptIndex] 75 | const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [] 76 | 77 | if (!buildCommand) { 78 | throw new Error(`Unknown script "${script}".`) 79 | } 80 | 81 | const relativeScriptPath = path.join(__dirname, './scripts', buildCommand) 82 | const scriptPath = attemptResolve(relativeScriptPath) 83 | if (!scriptPath) { 84 | throw new Error(`Unknown script "${script}".`) 85 | } 86 | 87 | // Attempt to strt the script with the passed node arguments 88 | const result = spawn.sync( 89 | executor, 90 | nodeArgs.concat(scriptPath).concat(args.slice(scriptIndex + 1)), 91 | { 92 | stdio: 'inherit', 93 | env: getEnv(), 94 | }, 95 | ) 96 | 97 | if (result.signal) { 98 | handleSignal(result) 99 | } else { 100 | process.exit(result.status) 101 | } 102 | } 103 | 104 | function handleSignal(result) { 105 | if (result.signal === 'SIGKILL') { 106 | console.log( 107 | `The script "${script}" failed because the process exited too early. ` + 108 | 'This probably means the system ran out of memory or someone called ' + 109 | '`kill -9` on the process.', 110 | ) 111 | } else if (result.signal === 'SIGTERM') { 112 | console.log( 113 | `The script "${script}" failed because the process exited too early. ` + 114 | 'Someone might have called `kill` or `killall`, or the system could ' + 115 | 'be shutting down.', 116 | ) 117 | } 118 | process.exit(1) 119 | } 120 | 121 | function attemptResolve(...resolveArgs) { 122 | try { 123 | return require.resolve(...resolveArgs) 124 | } catch (error) { 125 | return null 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/scripts/__tests__/__snapshots__/format.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`format --config arg can be used for a custom config 1`] = `prettier --write **/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue) --config ./my-config.js`; 4 | 5 | exports[`format --ignore-path arg can be used for a custom ignore file 1`] = `prettier --write **/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue) --ignore-path ./.myignore`; 6 | 7 | exports[`format --no-write prevents --write argument from being added 1`] = `prettier **/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue) --no-write`; 8 | 9 | exports[`format calls prettier CLI with args 1`] = `prettier --write my-src/**/*.js`; 10 | -------------------------------------------------------------------------------- /src/scripts/__tests__/__snapshots__/lint.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`lint --no-cache will disable caching 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --ignore-path ./src/config/eslintignore --no-cache .`; 4 | 5 | exports[`lint calls eslint CLI with default args 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache .`; 6 | 7 | exports[`lint does not use built-in config with .eslintrc file 1`] = `eslint --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache .`; 8 | 9 | exports[`lint does not use built-in config with .eslintrc.js file 1`] = `eslint --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache .`; 10 | 11 | exports[`lint does not use built-in config with --config 1`] = `eslint --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache --config ./custom-config.js .`; 12 | 13 | exports[`lint does not use built-in config with eslintConfig pkg prop 1`] = `eslint --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache .`; 14 | 15 | exports[`lint does not use built-in ignore with .eslintignore file 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --cache --cache-location /node_modules/.cache/.eslintcache .`; 16 | 17 | exports[`lint does not use built-in ignore with --ignore-path 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --cache --cache-location /node_modules/.cache/.eslintcache --ignore-path ./my-ignore .`; 18 | 19 | exports[`lint does not use built-in ignore with eslintIgnore pkg prop 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --cache --cache-location /node_modules/.cache/.eslintcache .`; 20 | 21 | exports[`lint runs on given files, but only js files 1`] = `eslint --config ./src/config/eslintrc.js --ext js,ts,tsx --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache ./src/index.js ./src/thing.ts ./src/lib.tsx ./src/component.js`; 22 | 23 | exports[`lint supports custom --ext 1`] = `eslint --config ./src/config/eslintrc.js --ignore-path ./src/config/eslintignore --cache --cache-location /node_modules/.cache/.eslintcache --ext js .`; 24 | -------------------------------------------------------------------------------- /src/scripts/__tests__/__snapshots__/precommit.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`pre-commit calls lint-staged CLI with default args: pre-commit scriptOne 1`] = `lint-staged --config ./src/config/lintstagedrc.js`; 4 | 5 | exports[`pre-commit calls lint-staged CLI with default args: pre-commit scriptTwo 1`] = `npm run validate`; 6 | 7 | exports[`pre-commit does not use built-in config with .lintstagedrc file: pre-commit scriptOne 1`] = `lint-staged`; 8 | 9 | exports[`pre-commit does not use built-in config with .lintstagedrc file: pre-commit scriptTwo 1`] = `npm run validate`; 10 | 11 | exports[`pre-commit does not use built-in config with --config: pre-commit scriptOne 1`] = `lint-staged --config ./custom-config.js`; 12 | 13 | exports[`pre-commit does not use built-in config with --config: pre-commit scriptTwo 1`] = `npm run validate`; 14 | 15 | exports[`pre-commit does not use built-in config with lint-staged pkg prop: pre-commit scriptOne 1`] = `lint-staged`; 16 | 17 | exports[`pre-commit does not use built-in config with lint-staged pkg prop: pre-commit scriptTwo 1`] = `npm run validate`; 18 | 19 | exports[`pre-commit does not use built-in config with lint-staged.config.js file: pre-commit scriptOne 1`] = `lint-staged`; 20 | 21 | exports[`pre-commit does not use built-in config with lint-staged.config.js file: pre-commit scriptTwo 1`] = `npm run validate`; 22 | 23 | exports[`pre-commit forwards args: pre-commit scriptOne 1`] = `lint-staged --config ./src/config/lintstagedrc.js --verbose`; 24 | 25 | exports[`pre-commit forwards args: pre-commit scriptTwo 1`] = `npm run validate`; 26 | -------------------------------------------------------------------------------- /src/scripts/__tests__/__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test calls jest.run with default args 1`] = `--config {"builtInConfig":true} --watch`; 4 | 5 | exports[`test does not watch --updateSnapshot 1`] = `--config {"builtInConfig":true} --updateSnapshot`; 6 | 7 | exports[`test does not watch on CI 1`] = `--config {"builtInConfig":true}`; 8 | 9 | exports[`test does not watch on SCRIPTS_PRE_COMMIT 1`] = `--config {"builtInConfig":true}`; 10 | 11 | exports[`test does not watch with --coverage 1`] = `--config {"builtInConfig":true} --coverage`; 12 | 13 | exports[`test does not watch with --no-watch 1`] = `--config {"builtInConfig":true} --no-watch`; 14 | 15 | exports[`test forwards args 1`] = `--config {"builtInConfig":true} --coverage --watch`; 16 | 17 | exports[`test uses custom config with --config 1`] = `--watch --config ./my-config.js`; 18 | 19 | exports[`test uses custom config with jest prop in pkg 1`] = `--watch`; 20 | 21 | exports[`test uses custom config with jest.config.js file 1`] = `--watch`; 22 | -------------------------------------------------------------------------------- /src/scripts/__tests__/__snapshots__/validate.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate allows you to specify your own npm scripts 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names specialbuild,specialtest,speciallint --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white "npm run specialbuild --silent" "npm run specialtest --silent" "npm run speciallint --silent"`; 4 | 5 | exports[`validate calls concurrently with all scripts 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names build,lint,test,typecheck --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white,bgCyan.bold.white "npm run build --silent" "npm run lint --silent" "npm run test --silent -- --coverage" "npm run typecheck --silent"`; 6 | 7 | exports[`validate does not include "build" if it doesn't have that script 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names lint,test,typecheck --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white "npm run lint --silent" "npm run test --silent -- --coverage" "npm run typecheck --silent"`; 8 | 9 | exports[`validate does not include "lint" if it doesn't have that script 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names build,test,typecheck --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white "npm run build --silent" "npm run test --silent -- --coverage" "npm run typecheck --silent"`; 10 | 11 | exports[`validate does not include "test" if it doesn't have that script 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names build,lint,typecheck --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white "npm run build --silent" "npm run lint --silent" "npm run typecheck --silent"`; 12 | 13 | exports[`validate does not include "typecheck" if it doesn't have that script 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names build,lint,test --prefix-colors bgBlue.bold.white,bgGreen.bold.white,bgMagenta.bold.white "npm run build --silent" "npm run lint --silent" "npm run test --silent -- --coverage"`; 14 | 15 | exports[`validate doesn't use test or lint if it's in pre-commit 1`] = `concurrently --kill-others-on-fail --prefix [{name}] --names build,typecheck --prefix-colors bgBlue.bold.white,bgGreen.bold.white "npm run build --silent" "npm run typecheck --silent"`; 16 | 17 | exports[`validate exits if there are no scripts to be run 1`] = ``; 18 | -------------------------------------------------------------------------------- /src/scripts/__tests__/format.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case' 2 | 3 | import {winPathSerializer} from './helpers/serializers' 4 | 5 | expect.addSnapshotSerializer(winPathSerializer) 6 | 7 | cases( 8 | 'format', 9 | ({args}) => { 10 | // beforeEach 11 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 12 | const originalExit = process.exit 13 | const originalArgv = process.argv 14 | const utils = require('../../utils') 15 | utils.resolveBin = (modName, {executable = modName} = {}) => executable 16 | process.exit = jest.fn() 17 | 18 | // tests 19 | process.argv = ['node', '../format', ...args] 20 | require('../format') 21 | expect(crossSpawnSyncMock).toHaveBeenCalledTimes(1) 22 | const [firstCall] = crossSpawnSyncMock.mock.calls 23 | const [script, calledArgs] = firstCall 24 | expect([script, ...calledArgs].join(' ')).toMatchSnapshot() 25 | 26 | // afterEach 27 | process.exit = originalExit 28 | process.argv = originalArgv 29 | jest.resetModules() 30 | }, 31 | { 32 | 'calls prettier CLI with args': { 33 | args: ['my-src/**/*.js'], 34 | }, 35 | '--no-write prevents --write argument from being added': { 36 | args: ['--no-write'], 37 | }, 38 | '--config arg can be used for a custom config': { 39 | args: ['--config', './my-config.js'], 40 | }, 41 | '--ignore-path arg can be used for a custom ignore file': { 42 | args: ['--ignore-path', './.myignore'], 43 | }, 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /src/scripts/__tests__/helpers/serializers.js: -------------------------------------------------------------------------------- 1 | import slash from 'slash' 2 | 3 | // this converts windows style file paths to unix... 4 | export const winPathSerializer = { 5 | print: val => slash(val), 6 | test: val => typeof val === 'string' && val.includes('\\'), 7 | } 8 | 9 | export const relativePathSerializer = { 10 | print: val => normalizePaths(val), 11 | test: val => normalizePaths(val) !== val, 12 | } 13 | 14 | function normalizePaths(value) { 15 | if (typeof value !== 'string') { 16 | return value 17 | } 18 | return slash(value.split(process.cwd()).join('')) 19 | } 20 | -------------------------------------------------------------------------------- /src/scripts/__tests__/lint.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case' 2 | 3 | import {winPathSerializer, relativePathSerializer} from './helpers/serializers' 4 | 5 | expect.addSnapshotSerializer(winPathSerializer) 6 | expect.addSnapshotSerializer(relativePathSerializer) 7 | 8 | cases( 9 | 'lint', 10 | ({ 11 | args = [], 12 | utils = require('../../utils'), 13 | hasPkgProp = () => false, 14 | hasFile = () => false, 15 | setup = () => () => {}, 16 | }) => { 17 | // beforeEach 18 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 19 | const originalArgv = process.argv 20 | const originalExit = process.exit 21 | Object.assign(utils, { 22 | hasPkgProp, 23 | hasFile, 24 | resolveBin: (modName, {executable = modName} = {}) => executable, 25 | }) 26 | process.exit = jest.fn() 27 | const teardown = setup() 28 | 29 | process.argv = ['node', '../lint', ...args] 30 | 31 | try { 32 | // tests 33 | require('../lint') 34 | expect(crossSpawnSyncMock).toHaveBeenCalledTimes(1) 35 | const [firstCall] = crossSpawnSyncMock.mock.calls 36 | const [script, calledArgs] = firstCall 37 | expect([script, ...calledArgs].join(' ')).toMatchSnapshot() 38 | } catch (error) { 39 | throw error 40 | } finally { 41 | teardown() 42 | // afterEach 43 | process.exit = originalExit 44 | process.argv = originalArgv 45 | jest.resetModules() 46 | } 47 | }, 48 | { 49 | 'calls eslint CLI with default args': {}, 50 | 'does not use built-in config with --config': { 51 | args: ['--config', './custom-config.js'], 52 | }, 53 | 'does not use built-in config with .eslintrc file': { 54 | hasFile: filename => filename === '.eslintrc', 55 | }, 56 | 'does not use built-in config with .eslintrc.js file': { 57 | hasFile: filename => filename === '.eslintrc.js', 58 | }, 59 | 'does not use built-in config with eslintConfig pkg prop': { 60 | hasPkgProp: prop => prop === 'eslintConfig', 61 | }, 62 | 'does not use built-in ignore with --ignore-path': { 63 | args: ['--ignore-path', './my-ignore'], 64 | }, 65 | 'does not use built-in ignore with .eslintignore file': { 66 | hasFile: filename => filename === '.eslintignore', 67 | }, 68 | 'does not use built-in ignore with eslintIgnore pkg prop': { 69 | hasPkgProp: prop => prop === 'eslintIgnore', 70 | }, 71 | '--no-cache will disable caching': { 72 | args: ['--no-cache'], 73 | }, 74 | 'supports custom --ext': { 75 | args: ['--ext', 'js'], 76 | }, 77 | 'runs on given files, but only js files': { 78 | args: [ 79 | './src/index.js', 80 | './package.json', 81 | './src/index.css', 82 | './src/thing.ts', 83 | './src/lib.tsx', 84 | './src/component.js', 85 | ], 86 | }, 87 | }, 88 | ) 89 | -------------------------------------------------------------------------------- /src/scripts/__tests__/precommit.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case' 2 | 3 | import {winPathSerializer} from './helpers/serializers' 4 | 5 | expect.addSnapshotSerializer(winPathSerializer) 6 | 7 | cases( 8 | 'pre-commit', 9 | ({ 10 | args = [], 11 | utils = require('../../utils'), 12 | hasPkgProp = () => false, 13 | hasFile = () => false, 14 | }) => { 15 | // beforeEach 16 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 17 | const originalArgv = process.argv 18 | const originalExit = process.exit 19 | Object.assign(utils, { 20 | hasPkgProp, 21 | hasFile, 22 | resolveBin: (modName, {executable = modName} = {}) => executable, 23 | }) 24 | process.exit = jest.fn() 25 | 26 | process.argv = ['node', '../pre-commit', ...args] 27 | 28 | try { 29 | // tests 30 | require('../pre-commit') 31 | expect(crossSpawnSyncMock).toHaveBeenCalledTimes(2) 32 | const [firstCall, secondCall] = crossSpawnSyncMock.mock.calls 33 | const [scriptOne, calledArgsOne] = firstCall 34 | expect([scriptOne, ...calledArgsOne].join(' ')).toMatchSnapshot( 35 | 'pre-commit scriptOne', 36 | ) 37 | const [scriptTwo, calledArgsTwo] = secondCall 38 | expect([scriptTwo, ...calledArgsTwo].join(' ')).toMatchSnapshot( 39 | 'pre-commit scriptTwo', 40 | ) 41 | } catch (error) { 42 | throw error 43 | } finally { 44 | // afterEach 45 | process.exit = originalExit 46 | process.argv = originalArgv 47 | jest.resetModules() 48 | } 49 | }, 50 | { 51 | 'calls lint-staged CLI with default args': {}, 52 | 'does not use built-in config with --config': { 53 | args: ['--config', './custom-config.js'], 54 | }, 55 | 'does not use built-in config with .lintstagedrc file': { 56 | hasFile: filename => filename === '.lintstagedrc', 57 | }, 58 | 'does not use built-in config with lint-staged.config.js file': { 59 | hasFile: filename => filename === 'lint-staged.config.js', 60 | }, 61 | 'does not use built-in config with lint-staged pkg prop': { 62 | hasPkgProp: prop => prop === 'lint-staged', 63 | }, 64 | 'forwards args': { 65 | args: ['--verbose'], 66 | }, 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /src/scripts/__tests__/test.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case' 2 | 3 | jest.mock('jest', () => ({run: jest.fn()})) 4 | jest.mock('../../config/jest.config', () => ({builtInConfig: true})) 5 | let mockIsCI = false 6 | jest.mock('is-ci', () => mockIsCI) 7 | 8 | cases( 9 | 'test', 10 | ({ 11 | args = [], 12 | utils = require('../../utils'), 13 | pkgHasJestProp = false, 14 | hasJestConfigFile = false, 15 | setup = () => () => {}, 16 | ci = false, 17 | preCommit = 'false', 18 | }) => { 19 | // beforeEach 20 | // eslint-disable-next-line jest/no-jest-import 21 | const {run: jestRunMock} = require('jest') 22 | const originalArgv = process.argv 23 | const prevCI = mockIsCI 24 | const prevPreCommit = process.env.SCRIPTS_PRE_COMMIT 25 | mockIsCI = ci 26 | process.env.SCRIPTS_PRE_COMMIT = preCommit 27 | Object.assign(utils, { 28 | hasPkgProp: () => pkgHasJestProp, 29 | hasFile: () => hasJestConfigFile, 30 | }) 31 | process.exit = jest.fn() 32 | const teardown = setup() 33 | 34 | process.argv = ['node', '../test', ...args] 35 | 36 | try { 37 | // tests 38 | require('../test') 39 | expect(jestRunMock).toHaveBeenCalledTimes(1) 40 | const [firstCall] = jestRunMock.mock.calls 41 | const [jestArgs] = firstCall 42 | expect(jestArgs.join(' ')).toMatchSnapshot() 43 | } catch (error) { 44 | throw error 45 | } finally { 46 | teardown() 47 | // afterEach 48 | process.argv = originalArgv 49 | mockIsCI = prevCI 50 | process.env.SCRIPTS_PRE_COMMIT = prevPreCommit 51 | jest.resetModules() 52 | } 53 | }, 54 | { 55 | 'calls jest.run with default args': {}, 56 | 'does not watch on CI': { 57 | ci: true, 58 | }, 59 | 'does not watch on SCRIPTS_PRE_COMMIT': { 60 | preCommit: 'true', 61 | }, 62 | 'does not watch with --no-watch': { 63 | args: ['--no-watch'], 64 | }, 65 | 'does not watch with --coverage': { 66 | args: ['--coverage'], 67 | }, 68 | 'does not watch --updateSnapshot': { 69 | args: ['--updateSnapshot'], 70 | }, 71 | 'uses custom config with --config': { 72 | args: ['--config', './my-config.js'], 73 | }, 74 | 'uses custom config with jest prop in pkg': { 75 | pkgHasJestProp: true, 76 | }, 77 | 'uses custom config with jest.config.js file': { 78 | hasJestConfigFile: true, 79 | }, 80 | 'forwards args': { 81 | args: ['--coverage', '--watch'], 82 | }, 83 | }, 84 | ) 85 | -------------------------------------------------------------------------------- /src/scripts/__tests__/validate.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case' 2 | 3 | cases( 4 | 'validate', 5 | ({setup = () => () => {}}) => { 6 | // beforeEach 7 | const {sync: crossSpawnSyncMock} = require('cross-spawn') 8 | const originalExit = process.exit 9 | process.exit = jest.fn() 10 | process.env.SCRIPTS_PRE_COMMIT = 'false' 11 | const teardown = setup() 12 | 13 | try { 14 | // tests 15 | require('../validate') 16 | const [firstCall] = crossSpawnSyncMock.mock.calls 17 | const [script, calledArgs] = firstCall || ['', []] 18 | expect([script, ...calledArgs].join(' ')).toMatchSnapshot() 19 | } catch (error) { 20 | throw error 21 | } finally { 22 | teardown() 23 | } 24 | 25 | // afterEach 26 | process.exit = originalExit 27 | jest.resetModules() 28 | }, 29 | { 30 | 'calls concurrently with all scripts': { 31 | setup: withDefaultSetup(setupWithScripts()), 32 | }, 33 | [`does not include "lint" if it doesn't have that script`]: { 34 | setup: withDefaultSetup(setupWithScripts(['test', 'build', 'typecheck'])), 35 | }, 36 | [`does not include "test" if it doesn't have that script`]: { 37 | setup: withDefaultSetup(setupWithScripts(['lint', 'build', 'typecheck'])), 38 | }, 39 | [`does not include "build" if it doesn't have that script`]: { 40 | setup: withDefaultSetup(setupWithScripts(['test', 'lint', 'typecheck'])), 41 | }, 42 | [`does not include "typecheck" if it doesn't have that script`]: { 43 | setup: withDefaultSetup(setupWithScripts(['test', 'build', 'lint'])), 44 | }, 45 | 'allows you to specify your own npm scripts': { 46 | setup: setupWithArgs(['specialbuild,specialtest,speciallint']), 47 | }, 48 | [`doesn't use test or lint if it's in pre-commit`]: { 49 | setup: withDefaultSetup(() => { 50 | const previousVal = process.env.SCRIPTS_PRE_COMMIT 51 | process.env.SCRIPTS_PRE_COMMIT = 'true' 52 | return function teardown() { 53 | process.env.SCRIPTS_PRE_COMMIT = previousVal 54 | } 55 | }), 56 | }, 57 | 'exits if there are no scripts to be run': { 58 | setup: withDefaultSetup(setupWithScripts([])), 59 | }, 60 | }, 61 | ) 62 | 63 | function setupWithScripts(scripts = ['test', 'lint', 'build', 'typecheck']) { 64 | return function setup() { 65 | const utils = require('../../utils') 66 | const originalIfScript = utils.ifScript 67 | utils.ifScript = (script, t, f) => (scripts.includes(script) ? t : f) 68 | return function teardown() { 69 | utils.ifScript = originalIfScript 70 | } 71 | } 72 | } 73 | 74 | function setupWithArgs(args = []) { 75 | return function setup() { 76 | const utils = require('../../utils') 77 | const originalResolveBin = utils.resolveBin 78 | utils.resolveBin = (modName, {executable = modName} = {}) => executable 79 | const originalArgv = process.argv 80 | process.argv = ['node', '../format', ...args] 81 | return function teardown() { 82 | process.argv = originalArgv 83 | utils.resolveBin = originalResolveBin 84 | } 85 | } 86 | } 87 | 88 | function withDefaultSetup(setupFn) { 89 | return function defaultSetup() { 90 | const utils = require('../../utils') 91 | utils.resolveBin = (modName, {executable = modName} = {}) => executable 92 | const argsTeardown = setupWithArgs()() 93 | const teardownScripts = setupWithScripts()() 94 | const teardownFn = setupFn() 95 | return function defaultTeardown() { 96 | argsTeardown() 97 | teardownFn() 98 | teardownScripts() 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/scripts/build/babel.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const {DEFAULT_EXTENSIONS} = require('@babel/core') 3 | const spawn = require('cross-spawn') 4 | const yargsParser = require('yargs-parser') 5 | const rimraf = require('rimraf') 6 | const glob = require('glob') 7 | const { 8 | hasPkgProp, 9 | fromRoot, 10 | resolveBin, 11 | hasFile, 12 | hasTypescript, 13 | generateTypeDefs, 14 | } = require('../../utils') 15 | 16 | let args = process.argv.slice(2) 17 | const here = p => path.join(__dirname, p) 18 | 19 | const parsedArgs = yargsParser(args) 20 | 21 | const useBuiltinConfig = 22 | !args.includes('--presets') && 23 | !hasFile('.babelrc') && 24 | !hasFile('.babelrc.js') && 25 | !hasFile('babel.config.js') && 26 | !hasPkgProp('babel') 27 | const config = useBuiltinConfig 28 | ? ['--presets', here('../../config/babelrc.js')] 29 | : [] 30 | 31 | const extensions = 32 | args.includes('--extensions') || args.includes('--x') 33 | ? [] 34 | : ['--extensions', [...DEFAULT_EXTENSIONS, '.ts', '.tsx']] 35 | 36 | const builtInIgnore = '**/__tests__/**,**/__mocks__/**' 37 | 38 | const ignore = args.includes('--ignore') ? [] : ['--ignore', builtInIgnore] 39 | 40 | const copyFiles = args.includes('--no-copy-files') ? [] : ['--copy-files'] 41 | 42 | const useSpecifiedOutDir = args.includes('--out-dir') 43 | const builtInOutDir = 'dist' 44 | const outDir = useSpecifiedOutDir ? [] : ['--out-dir', builtInOutDir] 45 | const noTypeDefinitions = args.includes('--no-ts-defs') 46 | 47 | if (!useSpecifiedOutDir && !args.includes('--no-clean')) { 48 | rimraf.sync(fromRoot('dist')) 49 | } else { 50 | args = args.filter(a => a !== '--no-clean') 51 | } 52 | 53 | if (noTypeDefinitions) { 54 | args = args.filter(a => a !== '--no-ts-defs') 55 | } 56 | 57 | async function go() { 58 | let result = spawn.sync( 59 | resolveBin('@babel/cli', {executable: 'babel'}), 60 | [ 61 | ...outDir, 62 | ...copyFiles, 63 | ...ignore, 64 | ...extensions, 65 | ...config, 66 | 'src', 67 | ].concat(args), 68 | {stdio: 'inherit'}, 69 | ) 70 | if (result.status !== 0) return result.status 71 | 72 | const pathToOutDir = fromRoot(parsedArgs.outDir || builtInOutDir) 73 | 74 | if (hasTypescript && !noTypeDefinitions) { 75 | console.log('Generating TypeScript definitions') 76 | result = await generateTypeDefs(pathToOutDir) 77 | if (result.status !== 0) return result.status 78 | console.log('TypeScript definitions generated') 79 | } 80 | 81 | // because babel will copy even ignored files, we need to remove the ignored files 82 | const ignoredPatterns = (parsedArgs.ignore || builtInIgnore) 83 | .split(',') 84 | .map(pattern => path.join(pathToOutDir, pattern)) 85 | 86 | // type def files are compiled to an empty file and they're useless 87 | // so we'll get rid of those too. 88 | const typeDefCompiledFiles = path.join(pathToOutDir, '**/*.d.js') 89 | const ignoredFiles = ignoredPatterns.reduce( 90 | (all, pattern) => [...all, ...glob.sync(pattern)], 91 | [typeDefCompiledFiles], 92 | ) 93 | ignoredFiles.forEach(ignoredFile => { 94 | rimraf.sync(ignoredFile) 95 | }) 96 | 97 | return result.status 98 | } 99 | 100 | go().then(process.exit) 101 | -------------------------------------------------------------------------------- /src/scripts/build/index.js: -------------------------------------------------------------------------------- 1 | if (process.argv.includes('--browser')) { 2 | console.error('--browser has been deprecated, use --bundle instead') 3 | } 4 | 5 | if (process.argv.includes('--bundle') || process.argv.includes('--browser')) { 6 | require('./rollup') 7 | } else { 8 | require('./babel') 9 | } 10 | -------------------------------------------------------------------------------- /src/scripts/build/rollup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const spawn = require('cross-spawn') 4 | const glob = require('glob') 5 | const rimraf = require('rimraf') 6 | const yargsParser = require('yargs-parser') 7 | const { 8 | hasFile, 9 | resolveBin, 10 | fromRoot, 11 | toPOSIX, 12 | getConcurrentlyArgs, 13 | writeExtraEntry, 14 | hasTypescript, 15 | generateTypeDefs, 16 | getRollupInputs, 17 | getRollupOutput, 18 | } = require('../../utils') 19 | 20 | const crossEnv = resolveBin('cross-env') 21 | const rollup = resolveBin('rollup') 22 | const args = process.argv.slice(2) 23 | const here = p => path.join(__dirname, p) 24 | const hereRelative = p => here(p).replace(process.cwd(), '.') 25 | const parsedArgs = yargsParser(args) 26 | 27 | const useBuiltinConfig = 28 | !args.includes('--config') && !hasFile('rollup.config.js') 29 | const config = useBuiltinConfig 30 | ? `--config ${hereRelative('../../config/rollup.config.js')}` 31 | : args.includes('--config') 32 | ? '' 33 | : '--config' // --config will pick up the rollup.config.js file 34 | 35 | const environment = parsedArgs.environment 36 | ? `--environment ${parsedArgs.environment}` 37 | : '' 38 | const watch = parsedArgs.watch ? '--watch' : '' 39 | const sizeSnapshot = parsedArgs['size-snapshot'] 40 | 41 | let formats = ['esm', 'cjs', 'umd', 'umd.min'] 42 | 43 | if (typeof parsedArgs.bundle === 'string') { 44 | formats = parsedArgs.bundle.split(',') 45 | } 46 | 47 | const defaultEnv = 'BUILD_ROLLUP=true' 48 | 49 | const getCommand = (env, ...flags) => 50 | [crossEnv, defaultEnv, env, rollup, config, environment, watch, ...flags] 51 | .filter(Boolean) 52 | .join(' ') 53 | 54 | const buildPreact = args.includes('--p-react') 55 | const scripts = getConcurrentlyArgs( 56 | buildPreact ? getPReactCommands() : getCommands(), 57 | ) 58 | 59 | const cleanBuildDirs = !args.includes('--no-clean') 60 | 61 | if (cleanBuildDirs) { 62 | rimraf.sync(fromRoot('dist')) 63 | 64 | if (buildPreact) { 65 | rimraf.sync(fromRoot('preact')) 66 | } 67 | } 68 | 69 | function go() { 70 | let result = spawn.sync(resolveBin('concurrently'), scripts, { 71 | stdio: 'inherit', 72 | }) 73 | 74 | if (result.status !== 0) return result.status 75 | 76 | if (buildPreact && !args.includes('--no-package-json')) { 77 | writeExtraEntry( 78 | 'preact', 79 | { 80 | cjs: glob.sync(toPOSIX(fromRoot('preact/**/*.cjs.js')))[0], 81 | esm: glob.sync(toPOSIX(fromRoot('preact/**/*.esm.js')))[0], 82 | }, 83 | false, 84 | ) 85 | } 86 | 87 | if (hasTypescript && !args.includes('--no-ts-defs')) { 88 | console.log('Generating TypeScript definitions') 89 | result = generateTypeDefs(fromRoot('dist')) 90 | if (result.status !== 0) return result.status 91 | 92 | const rollupInputs = getRollupInputs() 93 | const typeDefFiles = rollupInputs.map(input => { 94 | return input 95 | .replace(path.join(process.cwd(), 'src'), 'dist') 96 | .replace(/\.(t|j)sx?$/, '.d.ts') 97 | }) 98 | 99 | for (const format of formats) { 100 | const {dirpath, filename} = getRollupOutput(format) 101 | 102 | const isCodesplitting = rollupInputs.length > 1 103 | 104 | const outputs = isCodesplitting 105 | ? glob.sync(toPOSIX(fromRoot(path.posix.join(dirpath, format, '*.js')))) 106 | : [fromRoot(path.join(dirpath, filename))] 107 | 108 | for (const output of outputs) { 109 | const {name, dir} = path.parse(output) 110 | const typeDef = isCodesplitting 111 | ? typeDefFiles.find(f => path.basename(f) === `${name}.d.ts`) 112 | : 'dist/index.d.ts' 113 | const relativePath = path 114 | .join(path.relative(dir, process.cwd()), typeDef) 115 | .replace(/\.d\.ts$/, '') 116 | // make a .d.ts file for every generated file that re-exports index.d.ts 117 | fs.writeFileSync( 118 | path.join(dir, `${name}.d.ts`), 119 | `export * from "${relativePath}";\n`, 120 | ) 121 | } 122 | } 123 | 124 | // because typescript generates type defs for ignored files, we need to 125 | // remove the ignored files 126 | const ignoredFiles = [ 127 | ...glob.sync(toPOSIX(fromRoot('dist', '**/__tests__/**'))), 128 | ...glob.sync(toPOSIX(fromRoot('dist', '**/__mocks__/**'))), 129 | ] 130 | ignoredFiles.forEach(ignoredFile => { 131 | rimraf.sync(ignoredFile) 132 | }) 133 | console.log('TypeScript definitions generated') 134 | } 135 | 136 | return result.status 137 | } 138 | 139 | function getPReactCommands() { 140 | return { 141 | ...prefixKeys('react.', getCommands()), 142 | ...prefixKeys('preact.', getCommands({preact: true})), 143 | } 144 | } 145 | 146 | function prefixKeys(prefix, object) { 147 | return Object.entries(object).reduce((cmds, [key, value]) => { 148 | cmds[`${prefix}${key}`] = value 149 | return cmds 150 | }, {}) 151 | } 152 | 153 | function getCommands({preact = false} = {}) { 154 | return formats.reduce((cmds, format) => { 155 | const [formatName, minify = false] = format.split('.') 156 | const nodeEnv = minify ? 'production' : 'development' 157 | const sourceMap = formatName === 'umd' ? '--sourcemap' : '' 158 | const buildMinify = Boolean(minify) 159 | 160 | cmds[format] = getCommand( 161 | [ 162 | `BUILD_FORMAT=${formatName}`, 163 | `BUILD_MINIFY=${buildMinify}`, 164 | `NODE_ENV=${nodeEnv}`, 165 | `BUILD_PREACT=${preact}`, 166 | `BUILD_SIZE_SNAPSHOT=${sizeSnapshot}`, 167 | `BUILD_NODE=${process.env.BUILD_NODE || false}`, 168 | `BUILD_REACT_NATIVE=${process.env.BUILD_REACT_NATIVE || false}`, 169 | ].join(' '), 170 | sourceMap, 171 | ) 172 | return cmds 173 | }, {}) 174 | } 175 | 176 | process.exit(go()) 177 | -------------------------------------------------------------------------------- /src/scripts/format.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const spawn = require('cross-spawn') 3 | const yargsParser = require('yargs-parser') 4 | const {resolveBin, hasFile, hasLocalConfig} = require('../utils') 5 | 6 | const args = process.argv.slice(2) 7 | const parsedArgs = yargsParser(args) 8 | 9 | const here = p => path.join(__dirname, p) 10 | const hereRelative = p => here(p).replace(process.cwd(), '.') 11 | 12 | const useBuiltinConfig = 13 | !args.includes('--config') && !hasLocalConfig('prettier') 14 | const config = useBuiltinConfig 15 | ? ['--config', hereRelative('../config/prettierrc.js')] 16 | : [] 17 | 18 | const useBuiltinIgnore = 19 | !args.includes('--ignore-path') && !hasFile('.prettierignore') 20 | const ignore = useBuiltinIgnore 21 | ? ['--ignore-path', hereRelative('../config/prettierignore')] 22 | : [] 23 | 24 | const write = args.includes('--no-write') ? [] : ['--write'] 25 | 26 | // this ensures that when running format as a pre-commit hook and we get 27 | // the full file path, we make that non-absolute so it is treated as a glob, 28 | // This way the prettierignore will be applied 29 | const relativeArgs = args.map(a => a.replace(`${process.cwd()}/`, '')) 30 | 31 | const filesToApply = parsedArgs._.length 32 | ? [] 33 | : ['**/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue)'] 34 | 35 | const result = spawn.sync( 36 | resolveBin('prettier'), 37 | [...config, ...ignore, ...write, ...filesToApply].concat(relativeArgs), 38 | {stdio: 'inherit'}, 39 | ) 40 | 41 | process.exit(result.status) 42 | -------------------------------------------------------------------------------- /src/scripts/lint.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const spawn = require('cross-spawn') 3 | const yargsParser = require('yargs-parser') 4 | const {hasPkgProp, resolveBin, hasFile, fromRoot} = require('../utils') 5 | 6 | let args = process.argv.slice(2) 7 | const here = p => path.join(__dirname, p) 8 | const hereRelative = p => here(p).replace(process.cwd(), '.') 9 | const parsedArgs = yargsParser(args) 10 | 11 | const useBuiltinConfig = 12 | !args.includes('--config') && 13 | !hasFile('.eslintrc') && 14 | !hasFile('.eslintrc.js') && 15 | !hasPkgProp('eslintConfig') 16 | 17 | const config = useBuiltinConfig 18 | ? ['--config', hereRelative('../config/eslintrc.js')] 19 | : [] 20 | 21 | const defaultExtensions = 'js,ts,tsx' 22 | const ext = args.includes('--ext') ? [] : ['--ext', defaultExtensions] 23 | const extensions = (parsedArgs.ext || defaultExtensions).split(',') 24 | 25 | const useBuiltinIgnore = 26 | !args.includes('--ignore-path') && 27 | !hasFile('.eslintignore') && 28 | !hasPkgProp('eslintIgnore') 29 | 30 | const ignore = useBuiltinIgnore 31 | ? ['--ignore-path', hereRelative('../config/eslintignore')] 32 | : [] 33 | 34 | const cache = args.includes('--no-cache') 35 | ? [] 36 | : [ 37 | '--cache', 38 | '--cache-location', 39 | fromRoot('node_modules/.cache/.eslintcache'), 40 | ] 41 | 42 | const filesGiven = parsedArgs._.length > 0 43 | 44 | const filesToApply = filesGiven ? [] : ['.'] 45 | 46 | if (filesGiven) { 47 | // we need to take all the flag-less arguments (the files that should be linted) 48 | // and filter out the ones that aren't js files. Otherwise json or css files 49 | // may be passed through 50 | args = args.filter( 51 | a => !parsedArgs._.includes(a) || extensions.some(e => a.endsWith(e)), 52 | ) 53 | } 54 | 55 | const result = spawn.sync( 56 | resolveBin('eslint'), 57 | [...config, ...ext, ...ignore, ...cache, ...args, ...filesToApply], 58 | {stdio: 'inherit'}, 59 | ) 60 | 61 | process.exit(result.status) 62 | -------------------------------------------------------------------------------- /src/scripts/pre-commit.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const spawn = require('cross-spawn') 3 | const {hasPkgProp, hasFile, resolveBin} = require('../utils') 4 | 5 | const here = p => path.join(__dirname, p) 6 | const hereRelative = p => here(p).replace(process.cwd(), '.') 7 | 8 | const args = process.argv.slice(2) 9 | 10 | const useBuiltInConfig = 11 | !args.includes('--config') && 12 | !hasFile('.lintstagedrc') && 13 | !hasFile('lint-staged.config.js') && 14 | !hasPkgProp('lint-staged') 15 | 16 | const config = useBuiltInConfig 17 | ? ['--config', hereRelative('../config/lintstagedrc.js')] 18 | : [] 19 | 20 | function go() { 21 | let result 22 | 23 | result = spawn.sync(resolveBin('lint-staged'), [...config, ...args], { 24 | stdio: 'inherit', 25 | }) 26 | 27 | if (result.status !== 0) return result.status 28 | 29 | result = spawn.sync('npm', ['run', 'validate'], { 30 | stdio: 'inherit', 31 | }) 32 | 33 | return result.status 34 | } 35 | 36 | process.exit(go()) 37 | -------------------------------------------------------------------------------- /src/scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.BABEL_ENV = 'test' 2 | process.env.NODE_ENV = 'test' 3 | 4 | const isCI = require('is-ci') 5 | const {hasPkgProp, parseEnv, hasFile} = require('../utils') 6 | 7 | const args = process.argv.slice(2) 8 | 9 | const watch = 10 | !isCI && 11 | !parseEnv('SCRIPTS_PRE_COMMIT', false) && 12 | !args.includes('--no-watch') && 13 | !args.includes('--coverage') && 14 | !args.includes('--updateSnapshot') 15 | ? ['--watch'] 16 | : [] 17 | 18 | const config = 19 | !args.includes('--config') && 20 | !hasFile('jest.config.js') && 21 | !hasPkgProp('jest') 22 | ? ['--config', JSON.stringify(require('../config/jest.config'))] 23 | : [] 24 | 25 | // eslint-disable-next-line jest/no-jest-import 26 | require('jest').run([...config, ...watch, ...args]) 27 | -------------------------------------------------------------------------------- /src/scripts/typecheck.js: -------------------------------------------------------------------------------- 1 | const spawn = require('cross-spawn') 2 | const yargsParser = require('yargs-parser') 3 | const {hasAnyDep, resolveBin, hasFile} = require('../utils') 4 | 5 | let args = process.argv.slice(2) 6 | const parsedArgs = yargsParser(args) 7 | 8 | if (!hasAnyDep('typescript')) { 9 | throw new Error( 10 | 'Cannot use the "typecheck" script in a project that does not have typescript listed as a dependency (or devDependency).', 11 | ) 12 | } 13 | 14 | if (!parsedArgs.project && !parsedArgs.build && !hasFile('tsconfig.json')) { 15 | throw new Error( 16 | 'Cannot use the "typecheck" script without --project or --build in a project that does not have a tsconfig.json file.', 17 | ) 18 | } 19 | 20 | // if --project is provided, we can't pass --build 21 | // if --build is provided, we don't need to add it 22 | // if --no-build is passed, we'll just trust they know what they're doing 23 | if (!parsedArgs.project && !parsedArgs.build && !parsedArgs.noBuild) { 24 | args = ['--build', ...args] 25 | } 26 | 27 | const result = spawn.sync(resolveBin('typescript', {executable: 'tsc'}), args, { 28 | stdio: 'inherit', 29 | }) 30 | 31 | process.exit(result.status) 32 | -------------------------------------------------------------------------------- /src/scripts/validate.js: -------------------------------------------------------------------------------- 1 | const spawn = require('cross-spawn') 2 | const { 3 | parseEnv, 4 | resolveBin, 5 | ifScript, 6 | getConcurrentlyArgs, 7 | } = require('../utils') 8 | 9 | // pre-commit runs linting and tests on the relevant files 10 | // so those scripts don't need to be run if we're running 11 | // this in the context of a pre-commit hook. 12 | const preCommit = parseEnv('SCRIPTS_PRE_COMMIT', false) 13 | 14 | const validateScripts = process.argv[2] 15 | 16 | const useDefaultScripts = typeof validateScripts !== 'string' 17 | 18 | const scripts = useDefaultScripts 19 | ? { 20 | build: ifScript('build', 'npm run build --silent'), 21 | lint: preCommit ? null : ifScript('lint', 'npm run lint --silent'), 22 | test: preCommit 23 | ? null 24 | : ifScript('test', 'npm run test --silent -- --coverage'), 25 | typecheck: ifScript('typecheck', 'npm run typecheck --silent'), 26 | } 27 | : validateScripts.split(',').reduce( 28 | (scriptsToRun, name) => ({ 29 | ...scriptsToRun, 30 | [name]: `npm run ${name} --silent`, 31 | }), 32 | {}, 33 | ) 34 | 35 | const scriptCount = Object.values(scripts).filter(Boolean).length 36 | 37 | if (scriptCount > 0) { 38 | const result = spawn.sync( 39 | resolveBin('concurrently'), 40 | getConcurrentlyArgs(scripts), 41 | {stdio: 'inherit'}, 42 | ) 43 | 44 | process.exit(result.status) 45 | } else { 46 | process.exit(0) 47 | } 48 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const cpy = require('cpy') 4 | const spawn = require('cross-spawn') 5 | const rimraf = require('rimraf') 6 | const mkdirp = require('mkdirp') 7 | const arrify = require('arrify') 8 | const has = require('lodash.has') 9 | const glob = require('glob') 10 | const readPkgUp = require('read-pkg-up') 11 | const which = require('which') 12 | const {cosmiconfigSync} = require('cosmiconfig') 13 | 14 | const {packageJson: pkg, path: pkgPath} = readPkgUp.sync({ 15 | cwd: fs.realpathSync(process.cwd()), 16 | }) 17 | const appDirectory = path.dirname(pkgPath) 18 | 19 | function resolveKcdScripts() { 20 | if ( 21 | pkg.name === 'kcd-scripts' || 22 | // this happens on install of husky within kcd-scripts locally 23 | appDirectory.includes(path.join(__dirname, '..')) 24 | ) { 25 | return require.resolve('./').replace(process.cwd(), '.') 26 | } 27 | return resolveBin('kcd-scripts') 28 | } 29 | 30 | // eslint-disable-next-line complexity 31 | function resolveBin(modName, {executable = modName, cwd = process.cwd()} = {}) { 32 | let pathFromWhich 33 | try { 34 | pathFromWhich = fs.realpathSync(which.sync(executable)) 35 | if (pathFromWhich && pathFromWhich.includes('.CMD')) return pathFromWhich 36 | } catch (_error) { 37 | // ignore _error 38 | } 39 | try { 40 | if (modName === 'rollup') { 41 | // Rollup uses subpath exports without exporting package.json which is problematic 42 | // Convert to absolute path first 43 | const modPkgPathDist = require.resolve('rollup/dist/rollup.js') 44 | const modPkgDirDist = path.dirname(modPkgPathDist) 45 | modName = path.join(modPkgDirDist, '..') 46 | } 47 | const modPkgPath = require.resolve(`${modName}/package.json`) 48 | const modPkgDir = path.dirname(modPkgPath) 49 | const {bin} = require(modPkgPath) 50 | const binPath = typeof bin === 'string' ? bin : bin[executable] 51 | const fullPathToBin = path.join(modPkgDir, binPath) 52 | if (fullPathToBin === pathFromWhich) { 53 | return executable 54 | } 55 | return fullPathToBin.replace(cwd, '.') 56 | } catch (error) { 57 | if (pathFromWhich) { 58 | return executable 59 | } 60 | throw error 61 | } 62 | } 63 | 64 | const fromRoot = (...p) => path.join(appDirectory, ...p) 65 | const toPOSIX = p => p.split(path.sep).join(path.posix.sep) 66 | const hasFile = (...p) => fs.existsSync(fromRoot(...p)) 67 | const ifFile = (files, t, f) => 68 | arrify(files).some(file => hasFile(file)) ? t : f 69 | 70 | const hasPkgProp = props => arrify(props).some(prop => has(pkg, prop)) 71 | 72 | const hasPkgSubProp = pkgProp => props => 73 | hasPkgProp(arrify(props).map(p => `${pkgProp}.${p}`)) 74 | 75 | const ifPkgSubProp = pkgProp => (props, t, f) => 76 | hasPkgSubProp(pkgProp)(props) ? t : f 77 | 78 | const hasScript = hasPkgSubProp('scripts') 79 | const hasPeerDep = hasPkgSubProp('peerDependencies') 80 | const hasDep = hasPkgSubProp('dependencies') 81 | const hasDevDep = hasPkgSubProp('devDependencies') 82 | const hasAnyDep = args => [hasDep, hasDevDep, hasPeerDep].some(fn => fn(args)) 83 | 84 | const ifPeerDep = ifPkgSubProp('peerDependencies') 85 | const ifDep = ifPkgSubProp('dependencies') 86 | const ifDevDep = ifPkgSubProp('devDependencies') 87 | const ifAnyDep = (deps, t, f) => (hasAnyDep(arrify(deps)) ? t : f) 88 | const ifScript = ifPkgSubProp('scripts') 89 | 90 | const hasTypescript = hasAnyDep('typescript') && hasFile('tsconfig.json') 91 | const ifTypescript = (t, f) => (hasTypescript ? t : f) 92 | 93 | function parseEnv(name, def) { 94 | if (envIsSet(name)) { 95 | try { 96 | return JSON.parse(process.env[name]) 97 | } catch (err) { 98 | return process.env[name] 99 | } 100 | } 101 | return def 102 | } 103 | 104 | function envIsSet(name) { 105 | return ( 106 | process.env.hasOwnProperty(name) && 107 | process.env[name] && 108 | process.env[name] !== 'undefined' 109 | ) 110 | } 111 | 112 | function getConcurrentlyArgs(scripts, {killOthers = true} = {}) { 113 | const colors = [ 114 | 'bgBlue', 115 | 'bgGreen', 116 | 'bgMagenta', 117 | 'bgCyan', 118 | 'bgWhite', 119 | 'bgRed', 120 | 'bgBlack', 121 | 'bgYellow', 122 | ] 123 | scripts = Object.entries(scripts).reduce((all, [name, script]) => { 124 | if (script) { 125 | all[name] = script 126 | } 127 | return all 128 | }, {}) 129 | const prefixColors = Object.keys(scripts) 130 | .reduce( 131 | (pColors, _s, i) => 132 | pColors.concat([`${colors[i % colors.length]}.bold.white`]), 133 | [], 134 | ) 135 | .join(',') 136 | 137 | // prettier-ignore 138 | return [ 139 | killOthers ? '--kill-others-on-fail' : null, 140 | '--prefix', '[{name}]', 141 | '--names', Object.keys(scripts).join(','), 142 | '--prefix-colors', prefixColors, 143 | ...Object.values(scripts).map(s => JSON.stringify(s)), // stringify escapes quotes ✨ 144 | ].filter(Boolean) 145 | } 146 | 147 | function uniq(arr) { 148 | return Array.from(new Set(arr)) 149 | } 150 | 151 | function writeExtraEntry(name, {cjs, esm}, clean = true) { 152 | if (clean) { 153 | rimraf.sync(fromRoot(name)) 154 | } 155 | mkdirp.sync(fromRoot(name)) 156 | 157 | const pkgJson = fromRoot(`${name}/package.json`) 158 | const entryDir = fromRoot(name) 159 | 160 | fs.writeFileSync( 161 | pkgJson, 162 | JSON.stringify( 163 | { 164 | main: path.relative(entryDir, cjs), 165 | 'jsnext:main': path.relative(entryDir, esm), 166 | module: path.relative(entryDir, esm), 167 | }, 168 | null, 169 | 2, 170 | ), 171 | ) 172 | } 173 | 174 | function hasLocalConfig(moduleName, searchOptions = {}) { 175 | const explorerSync = cosmiconfigSync(moduleName, searchOptions) 176 | const result = explorerSync.search(pkgPath) 177 | 178 | return result !== null 179 | } 180 | 181 | async function generateTypeDefs(outputDir) { 182 | const result = spawn.sync( 183 | resolveBin('typescript', {executable: 'tsc'}), 184 | // prettier-ignore 185 | [ 186 | '--declaration', 187 | '--emitDeclarationOnly', 188 | '--noEmit', 'false', 189 | '--outDir', outputDir, 190 | ], 191 | {stdio: 'inherit'}, 192 | ) 193 | if (result.status !== 0) return result 194 | 195 | await cpy('**/*.d.ts', '../dist', {cwd: fromRoot('src'), parents: true}) 196 | return result 197 | } 198 | 199 | function getRollupInputs() { 200 | const buildInputGlob = 201 | process.env.BUILD_INPUT || 202 | (hasTypescript ? 'src/index.{js,ts,tsx}' : 'src/index.js') 203 | const input = glob.sync(toPOSIX(fromRoot(buildInputGlob))) 204 | if (!input.length) { 205 | throw new Error(`Unable to find files with this glob: ${buildInputGlob}`) 206 | } 207 | return input 208 | } 209 | 210 | function getRollupOutput(format = process.env.BUILD_FORMAT) { 211 | const minify = parseEnv('BUILD_MINIFY', false) 212 | const filenameSuffix = process.env.BUILD_FILENAME_SUFFIX || '' 213 | const ext = 214 | { 215 | esm: '.mjs', 216 | cjs: '.cjs', 217 | }[format] || '.js' 218 | const filename = [ 219 | pkg.name, 220 | filenameSuffix, 221 | `.${format}`, 222 | minify ? '.min' : null, 223 | ext, 224 | ] 225 | .filter(Boolean) 226 | .join('') 227 | 228 | const isPreact = parseEnv('BUILD_PREACT', false) 229 | const filenamePrefix = 230 | process.env.BUILD_FILENAME_PREFIX || (isPreact ? 'preact/' : '') 231 | const dirpath = path.join(...[filenamePrefix, 'dist'].filter(Boolean)) 232 | return {dirpath, filename} 233 | } 234 | 235 | module.exports = { 236 | appDirectory, 237 | fromRoot, 238 | toPOSIX, 239 | getConcurrentlyArgs, 240 | hasFile, 241 | hasLocalConfig, 242 | hasPkgProp, 243 | hasScript, 244 | hasAnyDep, 245 | hasDep, 246 | ifAnyDep, 247 | ifDep, 248 | ifDevDep, 249 | ifFile, 250 | ifPeerDep, 251 | ifScript, 252 | hasTypescript, 253 | ifTypescript, 254 | parseEnv, 255 | pkg, 256 | resolveBin, 257 | resolveKcdScripts, 258 | uniq, 259 | writeExtraEntry, 260 | generateTypeDefs, 261 | getRollupInputs, 262 | getRollupOutput, 263 | } 264 | --------------------------------------------------------------------------------