├── .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 |
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 |
--------------------------------------------------------------------------------