├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── .releaserc.json ├── .scriptlintrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── scriptlint-logo.ai ├── scriptlint-logo.png └── scriptlint-logo.svg ├── jest-setup.js ├── jest.config.ts ├── package-lock.json ├── package.json ├── sandbox └── package.json ├── src ├── cli.ts ├── cliConfig.ts ├── cliModule.ts ├── consoleReporter.ts ├── constants.ts ├── defaultRuleSets.ts ├── editJson.ts ├── errors.ts ├── execute.ts ├── index.ts ├── loadRules.ts ├── module.ts ├── rules │ ├── alphabetic-order.ts │ ├── correct-casing.ts │ ├── index.ts │ ├── mandatoryScriptFactory.ts │ ├── no-aliases.ts │ ├── no-default-test.ts │ ├── noShellSpecificsFactory.ts │ ├── prepost-trigger-defined.ts │ └── uses-allowed-namespace.ts ├── types.ts ├── userConfig.ts ├── userPackageScripts.ts └── utils.ts ├── tests ├── __mocks__ │ ├── cosmiconfig.js │ ├── fs.js │ └── path.js ├── __snapshots__ │ ├── editJson.test.ts.snap │ ├── index.test.ts.snap │ ├── loadRules.test.ts.snap │ └── userConfig.test.ts.snap ├── cli.test.ts ├── cliConfig.test.ts ├── consoleReporter.test.ts ├── editJson.test.ts ├── execute.test.ts ├── index.test.ts ├── loadRules.test.ts ├── rules │ ├── alphabetic-order.test.ts │ ├── correct-casing.test.ts │ ├── mandatoryScriptFactory.test.ts │ ├── no-aliases.test.ts │ ├── no-default-test.test.ts │ ├── noShellSpecificsFactory.test.ts │ ├── prepost-trigger-defined.test.ts │ └── uses-allowed-namespace.test.ts ├── userConfig.test.ts ├── userPackageScripts.test.ts └── utils.test.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/__mocks__ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["peerigon/presets/prettier-typescript-node.js"], 3 | root: true, 4 | rules: { 5 | "no-console": "warn", 6 | "import/no-anonymous-default-export": "off", 7 | "node/no-unsupported-features/es-syntax": "off", 8 | "node/no-missing-import": "off", 9 | }, 10 | parserOptions: { 11 | project: "./tsconfig.json", 12 | sourceType: "module", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # pin@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@04c56d2f954f1e4c69436aa54cfef261a018f458 # pin@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | npm install 22 | npm run build --if-present 23 | npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | prepare: 8 | runs-on: ubuntu-latest 9 | if: "! contains(github.event.head_commit.message, '[skip ci]')" 10 | steps: 11 | - run: echo "${{ github.event.head_commit.message }}" 12 | publish: 13 | needs: prepare 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # pin@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@04c56d2f954f1e4c69436aa54cfef261a018f458 # pin@v2 22 | with: 23 | node-version: 18 24 | - name: Install dependencies 25 | run: | 26 | npm install 27 | npm run build 28 | - name: Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: npx semantic-release 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist/ 4 | coverage/ 5 | yarn-error.log 6 | .eslintcache 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main", "next"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | [ 8 | "@semantic-release/git", 9 | { 10 | "assets": ["CHANGELOG.md"] 11 | } 12 | ], 13 | "@semantic-release/github", 14 | "@semantic-release/npm" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.scriptlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.0.0](https://github.com/peerigon/scriptlint/compare/v2.2.0...v3.0.0) (2023-04-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * semantic-release needs node 18 ([#66](https://github.com/peerigon/scriptlint/issues/66)) ([2e2cd80](https://github.com/peerigon/scriptlint/commit/2e2cd804dbbe4f97bdb872ab6ce8bb4aba35f232)) 7 | 8 | 9 | ### Features 10 | 11 | * dependencies ([#65](https://github.com/peerigon/scriptlint/issues/65)) ([bbf4764](https://github.com/peerigon/scriptlint/commit/bbf4764ed301d6cd96243c5af4ce81e59c561583)) 12 | 13 | 14 | ### BREAKING CHANGES 15 | 16 | * node version bump 17 | 18 | # [2.2.0](https://github.com/peerigon/scriptlint/compare/v2.1.10...v2.2.0) (2022-06-13) 19 | 20 | 21 | ### Features 22 | 23 | * **namespaces:** additional "script" namespace ([#59](https://github.com/peerigon/scriptlint/issues/59)) ([d9380c7](https://github.com/peerigon/scriptlint/commit/d9380c789556b07513505cdefba3f745426ef3eb)) 24 | 25 | ## [2.1.10](https://github.com/peerigon/scriptlint/compare/v2.1.9...v2.1.10) (2022-03-28) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * bump minimist from 1.2.5 to 1.2.6 ([#55](https://github.com/peerigon/scriptlint/issues/55)) ([783d374](https://github.com/peerigon/scriptlint/commit/783d37437eaf6fea349fc1c71da2c4898b0b7f30)) 31 | 32 | ## [2.1.9](https://github.com/peerigon/scriptlint/compare/v2.1.8...v2.1.9) (2021-12-20) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * update and pin action dependencies ([#54](https://github.com/peerigon/scriptlint/issues/54)) ([4a85555](https://github.com/peerigon/scriptlint/commit/4a8555537d802c9f9ecfbe0d1416d2226dbba52f)) 38 | 39 | ## [2.1.8](https://github.com/peerigon/scriptlint/compare/v2.1.7...v2.1.8) (2021-09-23) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * dependencies and dev improvements ([a79a095](https://github.com/peerigon/scriptlint/commit/a79a095908658b3a8a4b230c5623ff65a8a1be67)) 45 | 46 | ## [2.1.7](https://github.com/peerigon/scriptlint/compare/v2.1.6...v2.1.7) (2021-08-12) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **deps:** bump path-parse from 1.0.6 to 1.0.7 ([#50](https://github.com/peerigon/scriptlint/issues/50)) ([dff7d22](https://github.com/peerigon/scriptlint/commit/dff7d2208d3aa96fca578e884f7e1f491872d5b2)) 52 | 53 | ## [2.1.6](https://github.com/peerigon/scriptlint/compare/v2.1.5...v2.1.6) (2021-06-14) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **deps:** bump normalize-url from 4.5.0 to 4.5.1 ([#49](https://github.com/peerigon/scriptlint/issues/49)) ([ab0ada5](https://github.com/peerigon/scriptlint/commit/ab0ada53b0f01e82b500bbdfb3aa6bbe0b33a9f0)) 59 | 60 | ## [2.1.5](https://github.com/peerigon/scriptlint/compare/v2.1.4...v2.1.5) (2021-05-30) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **deps:** bump ws from 7.4.4 to 7.4.6 ([#48](https://github.com/peerigon/scriptlint/issues/48)) ([6b9f675](https://github.com/peerigon/scriptlint/commit/6b9f67564f6b9343a7f67a4f87d915f90ea65156)) 66 | 67 | ## [2.1.4](https://github.com/peerigon/scriptlint/compare/v2.1.3...v2.1.4) (2021-05-11) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * **deps:** bump hosted-git-info from 2.8.8 to 2.8.9 ([#46](https://github.com/peerigon/scriptlint/issues/46)) ([f14d229](https://github.com/peerigon/scriptlint/commit/f14d2296b92294e177cd020c3af1fcc0cb444777)) 73 | 74 | ## [2.1.3](https://github.com/peerigon/scriptlint/compare/v2.1.2...v2.1.3) (2021-04-01) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * README ([#43](https://github.com/peerigon/scriptlint/issues/43)) ([33a8119](https://github.com/peerigon/scriptlint/commit/33a8119cd5f007c0e9b7c2f88bafd92ff750b90b)) 80 | * update dependencies ([#44](https://github.com/peerigon/scriptlint/issues/44)) ([b76b2db](https://github.com/peerigon/scriptlint/commit/b76b2dbd60028df2a7eb4e31dfe59fe64d3157ff)) 81 | * **trigger:** manually trigger release after dependency patch ([0092dce](https://github.com/peerigon/scriptlint/commit/0092dce377d55cd0d65880e2be6057a240621929)) 82 | 83 | ## [2.1.2](https://github.com/peerigon/scriptlint/compare/v2.1.1...v2.1.2) (2020-09-10) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * prettier && eslint --fix ([8f3aa72](https://github.com/peerigon/scriptlint/commit/8f3aa72de8bc84b287d1da4e20a1b6897a5be351)) 89 | * run prettier and hopefully release to npm ([1820e06](https://github.com/peerigon/scriptlint/commit/1820e06b7d17c11cda1e364043669105db03a2b0)) 90 | 91 | ## [2.1.1](https://github.com/peerigon/scriptlint/compare/v2.1.0...v2.1.1) (2020-03-25) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * **dependencies:** move from yarn to npm over lack of `audit fix` 🤬 ([160b717](https://github.com/peerigon/scriptlint/commit/160b717)) 97 | 98 | # [2.1.0](https://github.com/peerigon/scriptlint/compare/v2.0.5...v2.1.0) (2020-03-25) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * **tests:** jest config typo ([b378e5e](https://github.com/peerigon/scriptlint/commit/b378e5e)) 104 | 105 | 106 | ### Features 107 | 108 | * add lint namespace ([a04b1ce](https://github.com/peerigon/scriptlint/commit/a04b1ce)) 109 | * Merge pull request [#34](https://github.com/peerigon/scriptlint/issues/34) from peerigon/lint-namespace ([4eaa726](https://github.com/peerigon/scriptlint/commit/4eaa726)) 110 | 111 | ## [2.0.5](https://github.com/peerigon/scriptlint/compare/v2.0.4...v2.0.5) (2020-03-19) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * **dependencies:** updtr guided updates ([04af746](https://github.com/peerigon/scriptlint/commit/04af746)) 117 | * **dependencies:** updtr guided updates ([1c92697](https://github.com/peerigon/scriptlint/commit/1c92697)) 118 | 119 | ## [2.0.4](https://github.com/peerigon/scriptlint/compare/v2.0.3...v2.0.4) (2020-03-05) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **license:** Add missing license ([0dbb51b](https://github.com/peerigon/scriptlint/commit/0dbb51b)) 125 | 126 | ## [2.0.3](https://github.com/peerigon/scriptlint/compare/v2.0.2...v2.0.3) (2020-02-26) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * **documentation:** Fix typo in README ([bc2d943](https://github.com/peerigon/scriptlint/commit/bc2d943)) 132 | 133 | ## [2.0.2](https://github.com/peerigon/scriptlint/compare/v2.0.1...v2.0.2) (2020-02-26) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * docs and design stuff ([#26](https://github.com/peerigon/scriptlint/issues/26)) ([c0a2bf9](https://github.com/peerigon/scriptlint/commit/c0a2bf9)) 139 | * **documentation:** overall documentation rewrite ([f586cc0](https://github.com/peerigon/scriptlint/commit/f586cc0)) 140 | 141 | ## [2.0.1](https://github.com/peerigon/scriptlint/compare/v2.0.0...v2.0.1) (2020-02-24) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * **build:** Fix CLI usage :scream: ([cb24eb9](https://github.com/peerigon/scriptlint/commit/cb24eb9)) 147 | 148 | # [2.0.0](https://github.com/peerigon/scriptlint/compare/v1.4.0...v2.0.0) (2020-02-24) 149 | 150 | 151 | * Release (#20) ([c9a58be](https://github.com/peerigon/scriptlint/commit/c9a58be)), closes [#20](https://github.com/peerigon/scriptlint/issues/20) [#21](https://github.com/peerigon/scriptlint/issues/21) [#22](https://github.com/peerigon/scriptlint/issues/22) 152 | 153 | 154 | ### BREAKING CHANGES 155 | 156 | * you may need to load your configurations differently 157 | 158 | * chore(tests): improved tests 159 | 160 | * feat(core): scripts can be passed to the module directly as a parameter 161 | 162 | * fix(code): eslint --fix 163 | 164 | * chore(readme): Update README 165 | 166 | # [1.4.0](https://github.com/peerigon/scriptlint/compare/v1.3.2...v1.4.0) (2020-02-11) 167 | 168 | 169 | ### Bug Fixes 170 | 171 | * **refactoring:** move tests into seperate dir ([e6c83b9](https://github.com/peerigon/scriptlint/commit/e6c83b9)) 172 | * Improve typing and defaults of CLI command handling ([febdf8d](https://github.com/peerigon/scriptlint/commit/febdf8d)) 173 | 174 | 175 | ### Features 176 | 177 | * **CLI:** opationally take package.json or project dir as argument and fallback to process.cwd() ([2b36142](https://github.com/peerigon/scriptlint/commit/2b36142)) 178 | * **structure:** make scriptlint usable as a required module ([048e35d](https://github.com/peerigon/scriptlint/commit/048e35d)) 179 | 180 | ## [1.3.2](https://github.com/peerigon/scriptlint/compare/v1.3.1...v1.3.2) (2020-02-03) 181 | 182 | 183 | ### Bug Fixes 184 | 185 | * **dependencies:** update scriptlint dependency to test re-release 😜 ([1158222](https://github.com/peerigon/scriptlint/commit/1158222)) 186 | 187 | ## [1.3.1](https://github.com/peerigon/scriptlint/compare/v1.3.0...v1.3.1) (2020-02-03) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * **readme:** Add missing parts of the README ([08726a9](https://github.com/peerigon/scriptlint/commit/08726a9)) 193 | 194 | # [1.3.0](https://github.com/peerigon/scriptlint/compare/v1.2.0...v1.3.0) (2020-02-03) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * **CLI:** add `-V` option to CLI ([4925cc9](https://github.com/peerigon/scriptlint/commit/4925cc9)) 200 | 201 | 202 | ### Features 203 | 204 | * **cli:** JSON output and config inspection (-j -c) ([0ada14f](https://github.com/peerigon/scriptlint/commit/0ada14f)) 205 | 206 | # [1.2.0](https://github.com/peerigon/scriptlint/compare/v1.1.1...v1.2.0) (2020-01-30) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * **dev:** whoops, forgot chmod in new package.json script ([850f0c0](https://github.com/peerigon/scriptlint/commit/850f0c0)) 212 | * **rules:** improve reporting on no-aliases ([c2e406b](https://github.com/peerigon/scriptlint/commit/c2e406b)) 213 | * **rules:** improve reporting on prepost-trigger-defined ([94360f0](https://github.com/peerigon/scriptlint/commit/94360f0)) 214 | * **tests:** fail test for invalid rule implementation ([e9c1568](https://github.com/peerigon/scriptlint/commit/e9c1568)) 215 | 216 | 217 | ### Features 218 | 219 | * **rules:** forbid certain script contents by regex ([0e54bac](https://github.com/peerigon/scriptlint/commit/0e54bac)) 220 | 221 | ## [1.1.1](https://github.com/peerigon/scriptlint/compare/v1.1.0...v1.1.1) (2020-01-27) 222 | 223 | 224 | ### Bug Fixes 225 | 226 | * **release:** make release action build stuff first :D ([9b34329](https://github.com/peerigon/scriptlint/commit/9b34329)) 227 | 228 | # [1.1.0](https://github.com/peerigon/scriptlint/compare/v1.0.0...v1.1.0) (2020-01-27) 229 | 230 | 231 | ### Features 232 | 233 | * add more badges to the README ([7e950ab](https://github.com/peerigon/scriptlint/commit/7e950ab)) 234 | 235 | # 1.0.0 (2020-01-27) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * bug with default npm scripts ([969ce88](https://github.com/peerigon/scriptlint/commit/969ce88)) 241 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![scriptlint status](https://img.shields.io/endpoint?url=https://scriptlint.peerigon.io/api/shield/scriptlint/latest)](https://scriptlint.peerigon.io/issues/scriptlint/latest) 4 | [![npm version badge](https://img.shields.io/npm/v/scriptlint?style=flat-square)](https://npmjs.com/package/scriptlint) 5 | [![dependency badge](https://img.shields.io/librariesio/release/npm/scriptlint?style=flat-square)](https://libraries.io/npm/scriptlint) 6 | [![Issue badge](https://img.shields.io/github/issues/peerigon/scriptlint?style=flat-square)](https://github.com/peerigon/scriptlint/issues) 7 | [![CI badge](https://github.com/peerigon/scriptlint/workflows/ci/badge.svg)](https://github.com/peerigon/scriptlint/actions?query=workflow%3Aci) 8 | 9 | # scriptlint 10 | 11 | Enforceable standards for your package.json scripts – like eslint for `npm run` 12 | 13 | ⚠️ Requires nodejs >= 14.x.x 14 | 15 | ## Intro 16 | 17 | `package.json` scripts are an integral part of the Node dev experience: we use them to start our projects, run our dev environments and for all kinds of formatting, linting and tooling in general. They are just as important as our code. Yet we don't treat them with the same meticulous attention to detail. **Scripts need :heart: too!** 18 | 19 | One of the main goals for scriptlint was to enable people to use memorable and consistent script names across their projects. Tools like [nps](https://github.com/sezna/nps) are great when you have to organize scripts with a certain level of complexity, but they don't help you with the structure and naming of your scripts. 20 | 21 | This is where the scriptlint CLI shines: it makes best practices outlined in this documentation enforceable throughout your project(s). Think of it as eslint for your `"scripts"` section. 22 | 23 | ## Rules 24 | 25 | Here's the tl;dr of all the best practices we consider the "`scriptlint` standard" 26 | 27 | Your `package.json`'s `"scripts"` section should… 28 | 29 | - have a `test` script that is not the default script from `npm init` 30 | - have a `dev` script and a `start` script 31 | - _abstract script names from their implementation (`test`, not `jest`)_ 32 | - _use namespaces to categorize scripts (`"test:unit": "jest"`)_ 33 | - _use `:` as a namespace separator_ 34 | - _have the scripts in alphabetic order_ 35 | - _have a trigger script for all hooks (ex: if you have `prefoobar`, there must be a `foobar` script)_ 36 | - _use `camelCase` for all script names_ 37 | - _not alias `devDependencies` (no `"jest": "jest"`)_ 38 | - _not use `&&` or `&` for sequential or parallel script execution_ 39 | 40 | (_italic = strict rule_) 41 | 42 | [Read more about the standard rules here](https://github.com/peerigon/scriptlint/wiki/The-scriptlint-%22standard%22-tl%3Bdr) 43 | 44 | ## Usage 45 | 46 | Install locally: 47 | 48 | `npm install scriptlint -D` (or `yarn add scriptlint -D`) 49 | 50 | … then run `npx scriptlint --strict` 51 | 52 | [Read about configuration here](https://github.com/peerigon/scriptlint/wiki/Configuration) 53 | 54 | # Documentation 55 | 56 |
    57 |
  1. Motivation
  2. 58 |
  3. The 59 | scriptlint "standard" tl;dr
  4. 60 |
  5. 61 | The 62 | scriptlint 63 | "standard" 64 |
      65 |
    1. Rules enforceable via the scriptlint CLI 66 |
        67 |
      1. 68 | Minimum 70 | rules 71 |
          72 |
        1. mandatory-start 74 |
        2. 75 |
        3. mandatory-dev 77 |
        4. 78 |
        5. mandatory-test 80 |
        6. 81 |
        7. no-default-test 83 |
        8. 84 |
        85 |
      2. 86 |
      3. Strict rules 87 |
          88 |
        1. uses-allowed-namespace 90 |
        2. 91 |
        3. alphabetic-order 93 |
        4. 94 |
        5. correct-casing 95 |
        6. 96 |
        7. no-aliases 97 |
        8. 98 |
        9. prepost-trigger-defined 100 |
        10. 101 |
        11. no-unix-double-ampersand 103 |
        12. 104 |
        13. no-unix-single-ampersand 106 |
        14. 107 |
        108 |
      4. 109 |
      110 |
    2. 111 |
    3. Best 112 | practices
    4. 113 |
    114 |
  6. 115 |
  7. The scriptlint CLI 116 |
      117 |
    1. Installation
    2. 118 |
    3. Usage
    4. 119 |
    5. Configuration
    6. 120 |
    7. Extending 121 |
    8. 122 |
    9. Use as a 123 | JavaScript module
    10. 124 |
    125 |
  8. 126 |
  9. Contributing to 127 | scriptlint
  10. 128 |
129 | 130 | ## Badge 131 | 132 | Would you like a scriptlint badge for your project readme? No problem: have a look at https://scriptlint.peerigon.io/ or adapt the snippet below: 133 | 134 | ```markdown 135 | [![scriptlint status](https://img.shields.io/endpoint?url=https://scriptlint.peerigon.io/api/shield/scriptlint/latest)](https://scriptlint.peerigon.io/issues/scriptlint/latest) 136 | ``` 137 | 138 | --- 139 | 140 | ## Sponsors 141 | 142 | [](https://peerigon.com) 143 | -------------------------------------------------------------------------------- /assets/scriptlint-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/scriptlint/055de5564dbb6f610c61728567b134e4fecd0106/assets/scriptlint-logo.ai -------------------------------------------------------------------------------- /assets/scriptlint-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/scriptlint/055de5564dbb6f610c61728567b134e4fecd0106/assets/scriptlint-logo.png -------------------------------------------------------------------------------- /assets/scriptlint-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | global.console = { 2 | log: jest.fn(), 3 | 4 | // Keep native behaviour for other methods, use those to print out things in your own tests, not `console.log` 5 | error: console.error, 6 | warn: console.warn, 7 | info: console.info, 8 | debug: console.debug 9 | }; 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | 3 | const config: Config = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | modulePathIgnorePatterns: ["out-ts", "dist"], 7 | setupFilesAfterEnv: ["/jest-setup.js"], 8 | collectCoverageFrom: [ 9 | "src/**/*.ts", 10 | "!src/index.ts", 11 | "!src/cli.ts", 12 | "!src/errors.ts", 13 | ], 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptlint", 3 | "version": "0.0.0-released-semantically", 4 | "description": "an enforcable script naming standard for package.json", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "https://github.com/peerigon/scriptlint", 8 | "author": "Peerigon GmbH", 9 | "license": "Unlicense", 10 | "bin": { 11 | "scriptlint": "./dist/cli.js" 12 | }, 13 | "files": [ 14 | "/dist", 15 | "/README.md", 16 | "/LICENSE" 17 | ], 18 | "scripts": { 19 | "build": "run-s build:clean build:scripts build:chmod", 20 | "build:chmod": "chmod +x ./dist/cli.js", 21 | "build:clean": "rimraf ./dist", 22 | "build:dev": "tsc -p tsconfig.build.json", 23 | "build:scripts": "tsc -p tsconfig.build.json", 24 | "dev": "run-s build other:watch", 25 | "lint": "run-p test:self:fix test:lint:fix lint:fix", 26 | "lint:fix": "prettier --write \"./tests/*.ts\" \"./src/*.ts\"", 27 | "other:watch": "nodemon -e js,ts --watch src --exec 'run-p build:dev'", 28 | "prepublishOnly": "run-p build", 29 | "pretest": "run-s build", 30 | "start": "node dist/index.js", 31 | "test": "run-s test:exports test:lint test:types test:unit test:self", 32 | "test:exports": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", 33 | "test:lint": "eslint --max-warnings 0 --cache ./src ./tests --ext js,ts,tsx", 34 | "test:lint:fix": "eslint ./src ./tests --ext js,ts,tsx --fix", 35 | "test:self": "./dist/cli.js", 36 | "test:self:fix": "./dist/cli.js --fix", 37 | "test:types": "tsc -p tsconfig.json --noEmit true", 38 | "test:unit": "jest --coverage", 39 | "test:update": "jest -u", 40 | "test:watch": "jest --watch" 41 | }, 42 | "devDependencies": { 43 | "@semantic-release/changelog": "^6.0.3", 44 | "@semantic-release/git": "^10.0.1", 45 | "@types/jest": "^29.5.1", 46 | "@types/node": "^18.16.0", 47 | "@typescript-eslint/eslint-plugin": "^5.59.1", 48 | "@typescript-eslint/parser": "^5.59.1", 49 | "eslint": "^8.39.0", 50 | "eslint-config-peerigon": "^33.3.0", 51 | "eslint-plugin-node": "^11.1.0", 52 | "jest": "^29.5.0", 53 | "jest-mock-console": "^2.0.0", 54 | "nodemon": "^2.0.22", 55 | "npm-run-all": "^4.1.5", 56 | "prettier": "^2.8.8", 57 | "rimraf": "^5.0.0", 58 | "semantic-release": "^21.0.1", 59 | "ts-jest": "^29.1.0", 60 | "ts-node": "^10.9.1", 61 | "ts-unused-exports": "^9.0.4", 62 | "typescript": "^5.0.4" 63 | }, 64 | "dependencies": { 65 | "chalk": "^4.1.2", 66 | "commander": "^10.0.1", 67 | "cosmiconfig": "^8.1.3", 68 | "detect-indent": "^6.1.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptlint-sandbox", 3 | "version": "1.0.0", 4 | "description": "Test your changes here", 5 | "main": "index.js", 6 | "scripts": { 7 | "test-lint-parallel": "scriptlint & jest", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "scriptlint": "scriptlint", 10 | "clean": "rm -rf ./cache", 11 | "test-lint": "scriptlint && echo 'ok!'" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // eslint-disable-next-line node/shebang 3 | import cliModule from "./cliModule"; 4 | 5 | cliModule(process.argv); 6 | -------------------------------------------------------------------------------- /src/cliConfig.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const { version } = require("../package.json"); 4 | 5 | type CliConfig = { 6 | packageFile?: string; 7 | fix?: boolean; 8 | strict?: boolean; 9 | json?: boolean; 10 | config?: boolean; 11 | }; 12 | 13 | export default (argv: Array): CliConfig => { 14 | const program = new commander.Command(); 15 | let packageFile; 16 | 17 | program 18 | .version(`${version}`) 19 | .arguments("[packageFile]") 20 | .action((arg: string) => { 21 | packageFile = arg; 22 | }) 23 | .option("-s, --strict", "strict mode") 24 | .option("-j, --json", "JSON output") 25 | .option("-c, --config", "inspect the config") 26 | .option("-f, --fix", "autofixing"); 27 | 28 | program.parse(argv); 29 | 30 | const options = program.opts(); 31 | 32 | const cliConfig: CliConfig = { packageFile }; 33 | 34 | if (options.fix !== undefined) { 35 | cliConfig.fix = options.fix; 36 | } 37 | 38 | if (options.strict !== undefined) { 39 | cliConfig.strict = options.strict; 40 | } 41 | 42 | if (options.json !== undefined) { 43 | cliConfig.json = options.json; 44 | } 45 | 46 | if (options.config !== undefined) { 47 | cliConfig.config = options.config; 48 | } 49 | 50 | return cliConfig; 51 | }; 52 | -------------------------------------------------------------------------------- /src/cliModule.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | import loadUserConfig from "./userConfig"; 3 | import loadCliConfig from "./cliConfig"; 4 | import { makePackageFilePath } from "./utils"; 5 | import userPackageScriptContext from "./userPackageScripts"; 6 | import { loadRulesFromRuleConfig } from "./loadRules"; 7 | import execute from "./execute"; 8 | import { success, warning, dump, error } from "./consoleReporter"; 9 | import { 10 | DEFAULT_CONFIG, 11 | PROCESS_EXIT_ERROR, 12 | PROCESS_EXIT_OK, 13 | } from "./constants"; 14 | 15 | /* istanbul ignore next */ 16 | const processExit = (code: number) => { 17 | if (typeof process.env.JEST_WORKER_ID === "undefined") { 18 | // eslint-disable-next-line node/no-process-exit 19 | process.exit(code); 20 | } 21 | }; 22 | 23 | export default (argv: Array) => { 24 | try { 25 | /** 26 | * config assembly 27 | */ 28 | 29 | const userConfig = loadUserConfig(); 30 | const cliConfig = loadCliConfig(argv); 31 | const config = { 32 | ...DEFAULT_CONFIG, 33 | ...{ json: false }, 34 | ...userConfig, 35 | ...cliConfig, 36 | }; 37 | 38 | // output the config (--config) but only if we don't want JSON output on the CLI 39 | if (!config.json && config.config) { 40 | const json = JSON.stringify(config, null, 2); 41 | 42 | // eslint-disable-next-line no-console 43 | console.log(`\n\n${json}\n\n`); 44 | } 45 | 46 | /** 47 | * package.json 48 | */ 49 | 50 | const { writePackageScripts, readPackageScripts } = 51 | userPackageScriptContext( 52 | makePackageFilePath(config.packageFile ?? process.cwd()) 53 | ); 54 | 55 | const scripts = readPackageScripts(config.ignoreScripts); 56 | 57 | /** 58 | * rule loading 59 | */ 60 | 61 | const rules = loadRulesFromRuleConfig( 62 | config.strict, 63 | config.rules, 64 | config.customRules 65 | ); 66 | 67 | /** 68 | * and go! 69 | */ 70 | const [issues, fixedScripts, issuesFixed] = execute( 71 | rules, 72 | scripts, 73 | warning, 74 | config.fix 75 | ); 76 | 77 | if (config.fix && issuesFixed > 0) { 78 | writePackageScripts(fixedScripts); 79 | success(`Fixed ${issuesFixed} issue${issuesFixed > 1 ? "s" : ""}!`); 80 | } 81 | 82 | if (issues.length - issuesFixed === 0) { 83 | success("✨ All good"); 84 | 85 | dump(config.json); 86 | 87 | processExit(PROCESS_EXIT_OK); 88 | } 89 | 90 | dump(config.json); 91 | processExit(PROCESS_EXIT_ERROR); 92 | } catch (err) { 93 | error((err as Error).message); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/consoleReporter.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { Values, MessageBuffer, MessageType } from "./types"; 3 | import { makeMessage } from "./utils"; 4 | 5 | const PREFIX = "⧙։⧘"; 6 | let stashed: MessageBuffer = []; 7 | 8 | const stash = (message: string, type: MessageType): void => { 9 | stashed.push({ 10 | message, 11 | type, 12 | }); 13 | }; 14 | 15 | export const warning = (template: string, values?: Values): void => { 16 | const message = makeMessage(template, values); 17 | 18 | stash(message, "warning"); 19 | }; 20 | 21 | export const success = (template: string, values?: Values): void => { 22 | const message = makeMessage(template, values); 23 | 24 | stash(message, "success"); 25 | }; 26 | 27 | export const dump = (jsonOutput: boolean): number => { 28 | const problemCount = stashed.length; 29 | 30 | if (jsonOutput) { 31 | // eslint-disable-next-line no-console 32 | console.log(JSON.stringify(stashed, null, "\t")); 33 | } else { 34 | stashed.forEach(({ message, type }) => { 35 | print(type, message); 36 | }); 37 | } 38 | 39 | stashed = []; 40 | 41 | return problemCount; 42 | }; 43 | 44 | export const error = (message: string): void => { 45 | print("error", message); 46 | }; 47 | 48 | const print = (type: MessageType, message: string) => { 49 | switch (type) { 50 | case "error": { 51 | // eslint-disable-next-line no-console 52 | console.log(chalk.bold.red(`${PREFIX} [error] ${message}`)); 53 | break; 54 | } 55 | 56 | case "warning": { 57 | const notUnderlined = chalk.yellow.bold(`${PREFIX} [${type}]`); 58 | const underlined = chalk.yellow.underline(message); 59 | 60 | // eslint-disable-next-line no-console 61 | console.log(`${notUnderlined} ${underlined}`); 62 | break; 63 | } 64 | case "success": { 65 | // eslint-disable-next-line no-console 66 | console.log(chalk.bold.gray(`${PREFIX} [✔️] ${message}`)); 67 | break; 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./types"; 2 | 3 | export const PROCESS_EXIT_ERROR = 1; 4 | export const PROCESS_EXIT_OK = 0; 5 | 6 | export const DEFAULT_CONFIG: Config = { 7 | strict: false, 8 | fix: false, 9 | json: false, 10 | config: false, 11 | packageFile: undefined, 12 | rules: {}, 13 | customRules: [], 14 | ignoreScripts: [], 15 | }; 16 | 17 | export const PROJECT_NAME = "scriptlint"; 18 | 19 | export const NAME_REGEX = /^[\d:A-Za-z]+$/; 20 | 21 | export const NAMESPACES = [ 22 | "build", 23 | "dev", 24 | "lint", 25 | "format", 26 | "lint", 27 | "other", 28 | "report", 29 | "script", 30 | "setup", 31 | "start", 32 | "test", 33 | ]; 34 | 35 | export const DEFAULT_NPM_HOOKS = [ 36 | "install", 37 | "postinstall", 38 | "postpack", 39 | "postpublish", 40 | "postrestart", 41 | "postshrinkwrap", 42 | "poststart", 43 | "poststop", 44 | "posttest", 45 | "postuninstall", 46 | "postversion", 47 | "preinstall", 48 | "precommit", 49 | "prepack", 50 | "prepare", 51 | "prepublish", 52 | "prepublishOnly", 53 | "prerestart", 54 | "preshrinkwrap", 55 | "prestart", 56 | "prestop", 57 | "pretest", 58 | "preuninstall", 59 | "preversion", 60 | "publish", 61 | "release", 62 | "restart", 63 | "shrinkwrap", 64 | "stop", 65 | "uninstall", 66 | "version", 67 | ]; 68 | -------------------------------------------------------------------------------- /src/defaultRuleSets.ts: -------------------------------------------------------------------------------- 1 | import defaultRules from "./rules"; 2 | import { Rule } from "./types"; 3 | 4 | const ruleSets = { 5 | default: [ 6 | "mandatory-test", 7 | "mandatory-start", 8 | "mandatory-dev", 9 | "no-default-test", 10 | ], 11 | strict: defaultRules.map((r: Rule) => r.name), 12 | }; 13 | 14 | export default ruleSets; 15 | -------------------------------------------------------------------------------- /src/editJson.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import detectIndent, { Indent } from "detect-indent"; 3 | import { PackageFile } from "./types"; 4 | import { PackageFileNotFoundError } from "./errors"; 5 | 6 | export default class { 7 | path: string; 8 | indent: Indent; 9 | package: PackageFile; 10 | fileContents?: string; 11 | 12 | constructor(path: string) { 13 | this.path = path; 14 | this.fileContents = fs.readFileSync(path, "utf-8"); 15 | if (!this.fileContents) { 16 | throw new PackageFileNotFoundError(path); 17 | } 18 | 19 | const fileParsed = JSON.parse(this.fileContents); 20 | 21 | if (!fileParsed.scripts) { 22 | fileParsed.scripts = {}; 23 | } 24 | 25 | this.package = fileParsed; 26 | this.indent = detectIndent(this.fileContents); 27 | } 28 | 29 | get(): PackageFile { 30 | return this.package; 31 | } 32 | 33 | set(path: string, content: Record) { 34 | this.package[path] = content; 35 | 36 | return this; 37 | } 38 | 39 | save() { 40 | const json = JSON.stringify(this.package, null, this.indent.indent); 41 | 42 | return fs.writeFileSync(this.path, json); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ConfigError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | Object.setPrototypeOf(this, ConfigError.prototype); 5 | } 6 | } 7 | 8 | export class ValidationFunctionInvalidError extends Error { 9 | constructor(name: string) { 10 | super(`Rule validation function is not a function (${name})`); 11 | Object.setPrototypeOf(this, ValidationFunctionInvalidError.prototype); 12 | } 13 | } 14 | 15 | export class PackageFileNotFoundError extends Error { 16 | constructor(path: string) { 17 | super("package.json could not be found at this location: " + path); 18 | Object.setPrototypeOf(this, PackageFileNotFoundError.prototype); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/execute.ts: -------------------------------------------------------------------------------- 1 | import { Rule, PackageScripts, JsonMessage } from "./types"; 2 | import { makeMessage, patchScriptObjectEntry } from "./utils"; 3 | import { ValidationFunctionInvalidError } from "./errors"; 4 | 5 | const execute = ( 6 | rules: Array, 7 | scripts: PackageScripts, 8 | warning?: (template: string, values?: string | Array) => void, 9 | configFix = false 10 | ): [Array, PackageScripts, number] => { 11 | /** 12 | * keep track of everything 13 | */ 14 | 15 | const issues: Array = []; 16 | let issuesFixed = 0; 17 | 18 | const patchScripts = (newScripts: PackageScripts) => { 19 | issuesFixed++; 20 | scripts = newScripts; 21 | }; 22 | 23 | /** 24 | * execution functions 25 | */ 26 | 27 | const executeObjectRule = ({ validate, message, name, fix }: Rule) => { 28 | if (typeof validate !== "function") { 29 | throw new ValidationFunctionInvalidError( 30 | `Rule validation function is not a function (${name})` 31 | ); 32 | } 33 | 34 | const validationResult = validate(scripts); 35 | 36 | const fixable = typeof fix === "function"; 37 | 38 | /** 39 | * validate 40 | */ 41 | 42 | const valid = 43 | typeof validationResult === "boolean" 44 | ? validationResult 45 | : validationResult.length < 1; 46 | 47 | if (valid) { 48 | return; 49 | } 50 | 51 | /** 52 | * warn of invalidity 53 | */ 54 | const warningMessage = 55 | typeof validationResult === "boolean" 56 | ? `${message} (${name})` 57 | : makeMessage(`${message} (${name})`, { 58 | names: validationResult.join(", "), 59 | }); 60 | 61 | /** 62 | * potentially fix 63 | */ 64 | 65 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 66 | if (configFix && fixable && fix) { 67 | patchScripts(fix(scripts)); 68 | 69 | return; 70 | } 71 | 72 | /** 73 | * keep track of everything 74 | */ 75 | 76 | issues.push({ 77 | name, 78 | affected: undefined, 79 | type: "warning", // at some point we should really use this 80 | message: warningMessage, 81 | }); 82 | 83 | if (typeof warning === "function") { 84 | warning(warningMessage, validationResult); 85 | } 86 | }; 87 | 88 | const executeEntryRule = ({ validate, message, name, fix }: Rule) => { 89 | if (typeof validate !== "function") { 90 | throw new ValidationFunctionInvalidError(name); 91 | } 92 | 93 | const fixable = typeof fix === "function"; 94 | const pairs = Object.entries(scripts); 95 | 96 | /** 97 | * iterate all the scripts 98 | */ 99 | 100 | pairs.forEach(([key, value]) => { 101 | const valid = validate(key, value, scripts); 102 | 103 | if (valid) { 104 | return; 105 | } 106 | 107 | const warningMessage = makeMessage(`${message} (${name})`, { 108 | name: key, 109 | }); 110 | 111 | /** 112 | * potentially fix 113 | */ 114 | 115 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 116 | if (configFix && fixable && fix) { 117 | const [toKey, fixedValue] = fix(key, value); 118 | 119 | const fixedScripts = patchScriptObjectEntry( 120 | scripts, 121 | key, 122 | toKey, 123 | fixedValue 124 | ); 125 | 126 | patchScripts(fixedScripts); 127 | 128 | return; 129 | } 130 | 131 | /** 132 | * keep track of everything 133 | */ 134 | 135 | issues.push({ 136 | name, 137 | affected: key, 138 | type: "warning", // at some point we should really use this 139 | message: warningMessage, 140 | }); 141 | 142 | if (typeof warning === "function") { 143 | warning(warningMessage, key); 144 | } 145 | }); 146 | }; 147 | 148 | /** 149 | * and go! 150 | */ 151 | 152 | rules.forEach((rule) => { 153 | if (rule.isObjectRule) { 154 | executeObjectRule(rule); 155 | } else { 156 | executeEntryRule(rule); 157 | } 158 | }); 159 | 160 | return [issues, scripts, issuesFixed]; 161 | }; 162 | 163 | export default execute; 164 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ·---------· 3 | | {scr:pt | 4 | | l:nt} | 5 | | | 6 | | | 7 | ·---------· 8 | */ 9 | 10 | import module from "./module"; 11 | 12 | export = module; 13 | -------------------------------------------------------------------------------- /src/loadRules.ts: -------------------------------------------------------------------------------- 1 | import defaultRuleSets from "./defaultRuleSets"; 2 | import defaultRules from "./rules"; 3 | // Types 4 | import { Rule } from "./types"; 5 | 6 | type RulesConfig = { 7 | [key: string]: boolean; 8 | }; 9 | 10 | export const getRuleByName = ( 11 | rules: Array, 12 | name: string 13 | ): Rule | null => { 14 | const filtered = rules.filter((r: Rule) => r.name === name); 15 | 16 | if (filtered.length < 1) { 17 | return null; 18 | } 19 | 20 | return filtered[0]; 21 | }; 22 | 23 | const loadDefaultRulesFromSet = (strict: boolean): Array => { 24 | if (strict) { 25 | return defaultRuleSets.strict 26 | .map((name: string) => getRuleByName(defaultRules, name)) 27 | .filter((r): r is Rule => r !== null); 28 | } 29 | 30 | return defaultRuleSets.default 31 | .map((name: string) => getRuleByName(defaultRules, name)) 32 | .filter((r): r is Rule => r !== null); 33 | }; 34 | 35 | export const loadRulesFromRuleConfig = ( 36 | strict: boolean, 37 | rulesConfig?: RulesConfig, 38 | customRules?: Array 39 | ): Array => { 40 | const rules = loadDefaultRulesFromSet(strict); 41 | 42 | const loadedCustomRules = 43 | rulesConfig && customRules 44 | ? customRules.filter((cr: Rule) => rulesConfig[cr.name]) 45 | : []; 46 | 47 | const loadedRules = [...loadedCustomRules, ...rules]; 48 | 49 | if (!rulesConfig) { 50 | return loadedRules; 51 | } 52 | 53 | return loadedRules 54 | .map((rule: Rule) => { 55 | if (rule.name in rulesConfig && rulesConfig[rule.name] === false) { 56 | return null; 57 | } 58 | 59 | return rule; 60 | }) 61 | .filter((r): r is Rule => r !== null); 62 | }; 63 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // eslint-disable-next-line node/shebang 3 | import userPackageScriptContext from "./userPackageScripts"; 4 | import { loadRulesFromRuleConfig } from "./loadRules"; 5 | import { ConfigError } from "./errors"; 6 | import execute from "./execute"; 7 | import { DEFAULT_CONFIG } from "./constants"; 8 | import { Config } from "./types"; 9 | import { makePackageFilePath } from "./utils"; 10 | 11 | export default (moduleConfig: Partial) => { 12 | /** 13 | * validate config 14 | */ 15 | 16 | if (moduleConfig.packageFile && moduleConfig.packageScripts) { 17 | throw new ConfigError( 18 | "Either specify a package.json location or a scripts object but not both" + 19 | JSON.stringify(moduleConfig) 20 | ); 21 | } 22 | 23 | if (!moduleConfig.packageFile && !moduleConfig.packageScripts) { 24 | throw new ConfigError( 25 | "You have to specify a package.json location or a scripts object" 26 | ); 27 | } 28 | 29 | const config = { ...DEFAULT_CONFIG, ...moduleConfig }; 30 | 31 | /** 32 | * find scripts 33 | */ 34 | let scripts = moduleConfig.packageScripts ?? {}; 35 | 36 | if (!moduleConfig.packageScripts && moduleConfig.packageFile) { 37 | const { readPackageScripts } = userPackageScriptContext( 38 | makePackageFilePath(config.packageFile ?? "") 39 | ); 40 | 41 | scripts = readPackageScripts(config.ignoreScripts); 42 | } 43 | 44 | /** 45 | * ok go! 46 | */ 47 | const rules = loadRulesFromRuleConfig( 48 | config.strict, 49 | config.rules, 50 | config.customRules 51 | ); 52 | 53 | const [issues, fixedScripts] = execute( 54 | rules, 55 | scripts, 56 | () => {}, 57 | config.fix 58 | ); 59 | 60 | return { 61 | issues, 62 | scripts: config.fix ? fixedScripts : scripts, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /src/rules/alphabetic-order.ts: -------------------------------------------------------------------------------- 1 | import { PackageScripts } from "../types"; 2 | 3 | export const sortScripts = (scripts: PackageScripts): PackageScripts => 4 | Object.entries(scripts) 5 | .sort((a, b) => { 6 | return a < b ? -1 : 1; 7 | }) 8 | .reduce((r, [k, v]) => ({ ...r, [k]: v }), {}); 9 | 10 | export default { 11 | name: "alphabetic-order", 12 | isObjectRule: true, 13 | message: "scripts must be in alphabetic order", 14 | validate: (scripts: PackageScripts) => { 15 | const sorted = sortScripts(scripts); 16 | 17 | return Object.keys(sorted).join("|") === Object.keys(scripts).join("|"); 18 | }, 19 | fix: (scripts: PackageScripts) => sortScripts(scripts), 20 | }; 21 | -------------------------------------------------------------------------------- /src/rules/correct-casing.ts: -------------------------------------------------------------------------------- 1 | import { NAME_REGEX } from "../constants"; 2 | 3 | export default { 4 | name: "correct-casing", 5 | isObjectRule: false, 6 | message: 'script name "{{name}}" must be camel case', 7 | validate: (key: string) => NAME_REGEX.test(key), 8 | }; 9 | -------------------------------------------------------------------------------- /src/rules/index.ts: -------------------------------------------------------------------------------- 1 | import makeMandatory from "./mandatoryScriptFactory"; 2 | import makeForbidUnixOperators from "./noShellSpecificsFactory"; 3 | import noDefaultTest from "./no-default-test"; 4 | import correctCasing from "./correct-casing"; 5 | import noAliases from "./no-aliases"; 6 | import prePostTriggerDefined from "./prepost-trigger-defined"; 7 | import usesAllowedNamespace from "./uses-allowed-namespace"; 8 | import alphabeticOrder from "./alphabetic-order"; 9 | import { Rule } from "../types"; 10 | 11 | const rules: Array = [ 12 | makeMandatory("test"), 13 | makeMandatory("start"), 14 | makeMandatory("dev"), 15 | noDefaultTest, 16 | correctCasing, 17 | noAliases, 18 | usesAllowedNamespace, 19 | prePostTriggerDefined, 20 | alphabeticOrder, 21 | makeForbidUnixOperators(/rm /, "rm -rf", "rimraf"), 22 | makeForbidUnixOperators( 23 | / && /, 24 | "unix double ampersand (&&)", 25 | "npm-run-all/run-s" 26 | ), 27 | makeForbidUnixOperators( 28 | / & /, 29 | "unix single ampersand (&)", 30 | "npm-run-all/run-p" 31 | ), 32 | ]; 33 | 34 | export default rules; 35 | -------------------------------------------------------------------------------- /src/rules/mandatoryScriptFactory.ts: -------------------------------------------------------------------------------- 1 | import { PackageScripts, Rule } from "../types"; 2 | 3 | export default (name: string): Rule => ({ 4 | isObjectRule: true, 5 | name: `mandatory-${name}`, 6 | message: `must contain a "${name}" script`, 7 | validate: (scripts: PackageScripts) => Object.keys(scripts).includes(name), 8 | }); 9 | -------------------------------------------------------------------------------- /src/rules/no-aliases.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_NPM_HOOKS } from "../constants"; 2 | 3 | export default { 4 | name: "no-aliases", 5 | isObjectRule: false, 6 | message: "don't alias binaries, use npx/yarn instead ({{name}})", 7 | validate: (key: string, value: string) => 8 | key !== value || DEFAULT_NPM_HOOKS.includes(key), 9 | }; 10 | -------------------------------------------------------------------------------- /src/rules/no-default-test.ts: -------------------------------------------------------------------------------- 1 | import { PackageScripts } from "../types"; 2 | 3 | export default { 4 | name: "no-default-test", 5 | isObjectRule: true, 6 | message: "`test` script can't be the default script", 7 | validate: (scripts: PackageScripts) => { 8 | if (Object.keys(scripts).includes("test")) { 9 | return scripts.test !== 'echo "Error: no test specified" && exit 1'; 10 | } 11 | 12 | return true; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/rules/noShellSpecificsFactory.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from "../types"; 2 | import { slugify } from "../utils"; 3 | 4 | export default (regex: RegExp, name: string, alternative?: string): Rule => { 5 | const slug = slugify(name); 6 | 7 | return { 8 | name: `no-${slug}`, 9 | isObjectRule: false, 10 | message: `Use of ${name} in script '{{name}}' is not allowed, consider using ${alternative}`, 11 | validate: (_: string, script: string): boolean | string => { 12 | return !regex.test(script); 13 | }, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/rules/prepost-trigger-defined.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_NPM_HOOKS } from "../constants"; 2 | import { PackageScripts } from "../types"; 3 | import { filterPackageScriptsByKeys } from "../utils"; 4 | 5 | const getMissingHooks = (prefix: string, scripts: PackageScripts) => { 6 | const keys = Object.keys(scripts); 7 | const hooks = keys.filter((k: string) => k.startsWith(prefix)); 8 | const hooksMissing: Array = []; 9 | 10 | hooks.forEach((hookName: string) => { 11 | const triggerName = hookName.substr(prefix.length); 12 | 13 | if (!keys.includes(triggerName)) { 14 | hooksMissing.push(triggerName); 15 | } 16 | }); 17 | 18 | return hooksMissing; 19 | }; 20 | 21 | export default { 22 | name: "prepost-trigger-defined", 23 | isObjectRule: true, 24 | message: 25 | "some custom hooks ({{names}}) are missing their trigger script(s)", 26 | validate: (scripts: PackageScripts): boolean | Array => { 27 | scripts = filterPackageScriptsByKeys(scripts, DEFAULT_NPM_HOOKS); 28 | const preHooksMissing = getMissingHooks("pre", scripts); 29 | const postHooksMissing = getMissingHooks("post", scripts); 30 | 31 | const allMissing = [ 32 | ...preHooksMissing.map((s) => `pre${s}`), 33 | ...postHooksMissing.map((s) => `post${s}`), 34 | ]; 35 | 36 | return allMissing.length < 1 ? true : allMissing; 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/rules/uses-allowed-namespace.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACES, DEFAULT_NPM_HOOKS } from "../constants"; 2 | 3 | const validate = (key: string): boolean => 4 | NAMESPACES.some((n) => key === n) || 5 | NAMESPACES.some((n) => key.startsWith(`${n}:`)) || 6 | DEFAULT_NPM_HOOKS.includes(key) || 7 | (key.startsWith("pre") && validate(key.slice(3))) || 8 | (key.startsWith("post") && validate(key.slice(4))); 9 | 10 | export default { 11 | name: "uses-allowed-namespace", 12 | isObjectRule: false, 13 | message: 14 | 'script name "{{name}}" should start with one of the allowed namespaces', 15 | validate, 16 | fix: (key: string, value: string) => [`other:${key}`, value], 17 | }; 18 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Rule = { 2 | isObjectRule: boolean; 3 | name: string; 4 | message: string; 5 | validate: unknown; 6 | 7 | fix?: ( 8 | a: unknown, 9 | b?: unknown, 10 | c?: unknown 11 | ) => PackageScripts & [string, string]; 12 | }; 13 | 14 | export type PackageScripts = { 15 | [key: string]: string; 16 | }; 17 | 18 | export type Config = { 19 | strict: boolean; 20 | packageFile?: string; 21 | packageScripts?: PackageScripts; 22 | fix: boolean; 23 | json: boolean; 24 | config: boolean; 25 | rules: { 26 | [key: string]: boolean; 27 | }; 28 | customRules: Array; 29 | ignoreScripts: Array; 30 | }; 31 | 32 | export type PackageFile = { 33 | [key: string]: unknown; 34 | scripts: PackageScripts; 35 | }; 36 | 37 | export type Values = 38 | | undefined 39 | | string 40 | | Array 41 | | { 42 | [key: string]: string; 43 | }; 44 | 45 | export type MessageType = "error" | "warning" | "success"; 46 | 47 | type Message = { 48 | message: string; 49 | type: MessageType; 50 | }; 51 | 52 | export type MessageBuffer = Array; 53 | 54 | export type JsonMessage = { 55 | name: string; 56 | type: string; 57 | message: string; 58 | affected: Values; 59 | }; 60 | -------------------------------------------------------------------------------- /src/userConfig.ts: -------------------------------------------------------------------------------- 1 | import { cosmiconfigSync } from "cosmiconfig"; 2 | import { Config as CosmiconfigConfig } from "cosmiconfig/dist/types"; 3 | import { PROJECT_NAME, DEFAULT_CONFIG } from "./constants"; 4 | import { ConfigError } from "./errors"; 5 | // Types 6 | import { Config } from "./types"; 7 | 8 | export const sanitizeConfig = (loadedConfig: CosmiconfigConfig): Config => { 9 | const sanitized = { ...DEFAULT_CONFIG, ...loadedConfig }; 10 | 11 | Object.keys(sanitized).forEach((key: string) => { 12 | const keyExists = Object.keys(DEFAULT_CONFIG).includes(key); 13 | 14 | if (!keyExists) { 15 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 16 | delete sanitized[key]; 17 | throw new ConfigError(`unknown config key "${key}"`); 18 | } 19 | }); 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 22 | return sanitized; 23 | }; 24 | 25 | const loadConfig = (name = PROJECT_NAME): Config => { 26 | const explorerSync = cosmiconfigSync(name); 27 | const searchedFor = explorerSync.search(); 28 | 29 | if (!searchedFor) { 30 | return DEFAULT_CONFIG; 31 | } 32 | 33 | return sanitizeConfig(searchedFor.config); 34 | }; 35 | 36 | export default loadConfig; 37 | -------------------------------------------------------------------------------- /src/userPackageScripts.ts: -------------------------------------------------------------------------------- 1 | import EditJson from "./editJson"; 2 | import { PackageScripts } from "./types"; 3 | import { filterPackageScriptsByKeys } from "./utils"; 4 | 5 | export default (wd: string) => { 6 | const file = new EditJson(wd); 7 | 8 | return { 9 | readPackageScripts: (ignores: Array): PackageScripts => { 10 | const { scripts } = file.get(); 11 | 12 | return filterPackageScriptsByKeys(scripts, ignores); 13 | }, 14 | 15 | writePackageScripts: (scripts: PackageScripts) => { 16 | file.set("scripts", scripts); 17 | 18 | return file.save(); 19 | }, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { PackageFileNotFoundError } from "./errors"; 4 | import { PackageScripts, Values } from "./types"; 5 | 6 | export const makePackageFilePath = (packageFile: string): string => { 7 | // resolve package.json path 8 | packageFile = packageFile.endsWith("/package.json") 9 | ? packageFile 10 | : packageFile + "/package.json"; 11 | 12 | packageFile = path.resolve(packageFile); 13 | 14 | // does it exist? 15 | if (!fs.existsSync(packageFile)) { 16 | throw new PackageFileNotFoundError(packageFile); 17 | } 18 | 19 | return packageFile; 20 | }; 21 | 22 | export const slugify = (str: string): string => 23 | str 24 | .trim() 25 | .toLowerCase() 26 | .replace(/[^A-Za-z-]/g, "-") 27 | .replace(/^-/g, "") 28 | .replace(/-+/g, "-") 29 | .replace(/-$/g, ""); 30 | 31 | export const filterPackageScriptsByKeys = ( 32 | raw: PackageScripts, 33 | removes: Array 34 | ): PackageScripts => { 35 | return Object.keys(raw) 36 | .filter((k) => !removes.includes(k)) 37 | .reduce((obj: PackageScripts, key: string) => { 38 | obj[key] = raw[key]; 39 | 40 | return obj; 41 | }, {}); 42 | }; 43 | 44 | export const makeMessage = (template: string, values: Values): string => { 45 | let message = template; 46 | 47 | if (values !== undefined) { 48 | const pairs = Object.entries(values); 49 | 50 | pairs.forEach(([key, value]) => { 51 | message = message.replace(`{{${key}}}`, value); 52 | }); 53 | } 54 | 55 | return message; 56 | }; 57 | 58 | const fromEntries = (iterable: Array<[string, string]>): PackageScripts => { 59 | return [...iterable].reduce((obj: PackageScripts, [key, val]) => { 60 | obj[key] = val; 61 | 62 | return obj; 63 | }, {}); 64 | }; 65 | 66 | export const patchScriptObjectEntry = ( 67 | scripts: PackageScripts, 68 | fromKey: string, 69 | toKey: string, 70 | value: string 71 | ) => 72 | fromEntries( 73 | Object.entries(scripts).map(([k, v]) => { 74 | return k === fromKey ? [toKey, value] : [k, v]; 75 | }) 76 | ); 77 | -------------------------------------------------------------------------------- /tests/__mocks__/cosmiconfig.js: -------------------------------------------------------------------------------- 1 | const search = () => { 2 | return { 3 | config: { 4 | strict: true, 5 | ignoreScripts: ["foobar"], 6 | rules: { 7 | "mandatory-dev": false 8 | } 9 | } 10 | }; 11 | }; 12 | 13 | const configs = { 14 | scriptlint: { 15 | search 16 | }, 17 | missing: { 18 | search: () => null 19 | } 20 | }; 21 | 22 | module.exports = { 23 | cosmiconfigSync: name => { 24 | return configs[name]; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /tests/__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const fs = jest.genMockFromModule("fs"); 4 | 5 | const packageFile = { 6 | scripts: { 7 | foo: "bar" 8 | } 9 | }; 10 | 11 | const packageFileFixable = { 12 | scripts: { 13 | "start": "echo 1", 14 | "test": "echo 1", 15 | "dev": "echo 1", 16 | } 17 | }; 18 | 19 | const packageFileFixable2 = { 20 | scripts: { 21 | "start": "echo 1", 22 | "foo": "echo 1", 23 | "test": "echo 1", 24 | "dev": "echo 1", 25 | } 26 | }; 27 | 28 | const packageFileCorrect = { 29 | scripts: { 30 | "dev": "echo 1", 31 | "other:foo": "echo 1", 32 | "start": "echo 1", 33 | "test": "echo 1", 34 | } 35 | }; 36 | 37 | const mockFiles = { 38 | "real/existing/path/package.json": JSON.stringify( 39 | { ...packageFile }, 40 | null, 41 | 2 42 | ), 43 | "real/existing/path/correct/package.json": JSON.stringify( 44 | { ...packageFileCorrect }, 45 | null, 46 | 2 47 | ), 48 | "real/existing/path/fixable/package.json": JSON.stringify( 49 | { ...packageFileFixable }, 50 | null, 51 | 2 52 | ), 53 | "real/existing/path/fixable2/package.json": JSON.stringify( 54 | { ...packageFileFixable2 }, 55 | null, 56 | 2 57 | ), 58 | "real/existing/path/package-without-scripts.json": JSON.stringify( 59 | {}, 60 | null, 61 | 2 62 | ), 63 | "real/existing/path/package-with-tabs.json": JSON.stringify( 64 | { ...packageFile }, 65 | null, 66 | "\t" 67 | ) 68 | }; 69 | 70 | function readFileSync(directoryPath) { 71 | return mockFiles[directoryPath]; 72 | } 73 | 74 | function writeFileSync(path, content) { 75 | return { 76 | written: true, 77 | path, 78 | content 79 | }; 80 | } 81 | 82 | function existsSync(directoryPath) { 83 | return Object.keys(mockFiles).includes(directoryPath); 84 | } 85 | 86 | fs.readFileSync = readFileSync; 87 | fs.writeFileSync = writeFileSync; 88 | fs.existsSync = existsSync; 89 | 90 | module.exports = fs; 91 | -------------------------------------------------------------------------------- /tests/__mocks__/path.js: -------------------------------------------------------------------------------- 1 | const path = jest.genMockFromModule("path"); 2 | path.resolve = (...p) => p.join("/"); 3 | 4 | module.exports = path; 5 | -------------------------------------------------------------------------------- /tests/__snapshots__/editJson.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`editJson.ts reads files 1`] = ` 4 | default_1 { 5 | "fileContents": "{ 6 | "scripts": { 7 | "foo": "bar" 8 | } 9 | }", 10 | "indent": { 11 | "amount": 2, 12 | "indent": " ", 13 | "type": "space", 14 | }, 15 | "package": { 16 | "scripts": { 17 | "foo": "bar", 18 | }, 19 | }, 20 | "path": "real/existing/path/package.json", 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`index.ts should lint files 1`] = ` 4 | { 5 | "issues": [ 6 | { 7 | "affected": undefined, 8 | "message": "must contain a "test" script (mandatory-test)", 9 | "name": "mandatory-test", 10 | "type": "warning", 11 | }, 12 | { 13 | "affected": undefined, 14 | "message": "must contain a "start" script (mandatory-start)", 15 | "name": "mandatory-start", 16 | "type": "warning", 17 | }, 18 | { 19 | "affected": undefined, 20 | "message": "must contain a "dev" script (mandatory-dev)", 21 | "name": "mandatory-dev", 22 | "type": "warning", 23 | }, 24 | ], 25 | "scripts": { 26 | "foo": "bar", 27 | }, 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /tests/__snapshots__/loadRules.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`loadRules.ts getRuleByName() nulls on unknown name 1`] = ` 4 | { 5 | "isObjectRule": true, 6 | "message": "must contain a "dev" script", 7 | "name": "mandatory-dev", 8 | "validate": [Function], 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /tests/__snapshots__/userConfig.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`userConfig.ts loadConfig() 1`] = ` 4 | { 5 | "config": false, 6 | "customRules": [], 7 | "fix": false, 8 | "ignoreScripts": [ 9 | "foobar", 10 | ], 11 | "json": false, 12 | "packageFile": undefined, 13 | "rules": { 14 | "mandatory-dev": false, 15 | }, 16 | "strict": true, 17 | } 18 | `; 19 | 20 | exports[`userConfig.ts should sanitize configs: invalid keys 1`] = `"unknown config key "invalid""`; 21 | -------------------------------------------------------------------------------- /tests/cli.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import mockConsole from "jest-mock-console"; 3 | import cli from "../src/cliModule"; 4 | 5 | jest.mock("fs"); 6 | jest.mock("path"); 7 | 8 | let restoreConsole: any; 9 | const processArgv = [process.argv[0], process.argv[1]]; 10 | 11 | beforeEach(() => { 12 | restoreConsole = mockConsole(); 13 | }); 14 | 15 | afterAll(() => { 16 | if (restoreConsole) { 17 | restoreConsole(); 18 | } 19 | }); 20 | 21 | describe("cli.ts", () => { 22 | it("should lint files", () => { 23 | cli([...processArgv, "real/existing/path/package.json"]); 24 | 25 | expect((console.log as any).mock.calls.length).toEqual(3); 26 | }); 27 | 28 | it("should catch errors", () => { 29 | expect(() => { 30 | cli([...processArgv, "not/existing/package.json"]); 31 | }).not.toThrow(); 32 | }); 33 | 34 | it("should fix 1 issue", () => { 35 | cli([ 36 | ...processArgv, 37 | "real/existing/path/fixable/package.json", 38 | "--fix", 39 | ]); 40 | 41 | expect((console.log as any).mock.calls[0][0]).toMatch(/Fixed 1 issue/); 42 | }); 43 | 44 | it("should fix 2 issues", () => { 45 | cli([ 46 | ...processArgv, 47 | "real/existing/path/fixable2/package.json", 48 | "--fix", 49 | ]); 50 | 51 | expect((console.log as any).mock.calls[0][0]).toMatch(/Fixed 2 issues/); 52 | }); 53 | 54 | it("should be all good man", () => { 55 | cli([ 56 | ...processArgv, 57 | "real/existing/path/correct/package.json", 58 | "--fix", 59 | ]); 60 | 61 | expect((console.log as any).mock.calls[0][0]).toMatch(/All good/); 62 | }); 63 | 64 | it("prints --json", () => { 65 | cli([...processArgv, "real/existing/path/package.json", "--json"]); 66 | 67 | const calls = (console.log as any).mock.calls; 68 | 69 | expect(calls.length).toEqual(1); 70 | expect(JSON.parse(calls[0]).length).toEqual(3); 71 | }); 72 | 73 | it("prints --config", () => { 74 | cli([...processArgv, "real/existing/path/package.json", "--config"]); 75 | 76 | const calls = (console.log as any).mock.calls; 77 | 78 | expect(JSON.parse(calls[0]).customRules).toEqual([]); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/cliConfig.test.ts: -------------------------------------------------------------------------------- 1 | import cliConfig from "../src/cliConfig"; 2 | 3 | describe("cliConfig.ts", () => { 4 | it("should parse CLI params", () => { 5 | const { packageFile, ...config } = cliConfig([ 6 | "foo/bin/node", 7 | "/usr/local/bin/scriptlint", 8 | "-f", 9 | "--json", 10 | "foo/bar/baz", 11 | "--strict", 12 | "-c", 13 | ]); 14 | 15 | expect(packageFile).toBe("foo/bar/baz"); 16 | 17 | expect(config).toEqual({ 18 | config: true, 19 | fix: true, 20 | json: true, 21 | strict: true, 22 | }); 23 | }); 24 | it("should parse CLI params #2", () => { 25 | const pJName = "foo/bar/baz/package.json"; 26 | 27 | const { packageFile, ...config } = cliConfig([ 28 | "foo/bin/node", 29 | "/usr/local/bin/scriptlint", 30 | "-f", 31 | "--json", 32 | "--strict", 33 | "-c", 34 | pJName, 35 | ]); 36 | 37 | expect(packageFile).toBe(pJName); 38 | 39 | expect(config).toEqual({ 40 | config: true, 41 | fix: true, 42 | json: true, 43 | strict: true, 44 | }); 45 | }); 46 | 47 | it("should parse CLI params #3", () => { 48 | const { packageFile, ...config } = cliConfig([ 49 | "foo/bin/node", 50 | "/usr/local/bin/scriptlint", 51 | "-f", 52 | ]); 53 | 54 | expect(packageFile).not.toBeDefined(); 55 | 56 | expect(config).toEqual({ 57 | fix: true, 58 | }); 59 | }); 60 | 61 | it("should parse CLI params #3", () => { 62 | const { packageFile, ...config } = cliConfig(["foo", "bar"]); 63 | 64 | expect(config).toEqual({}); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/consoleReporter.test.ts: -------------------------------------------------------------------------------- 1 | import mockConsole from "jest-mock-console"; 2 | import { warning, dump, success, error } from "../src/consoleReporter"; 3 | 4 | describe("consoleReporter.ts", () => { 5 | test("should console.log", () => { 6 | warning("foo"); 7 | warning("foo"); 8 | warning("foo"); 9 | 10 | expect(dump(false)).toBe(3); 11 | }); 12 | 13 | test("error()", () => { 14 | const restoreConsole = mockConsole(); 15 | 16 | error("foobar"); 17 | // eslint-disable-next-line no-console 18 | expect(console.log).toHaveBeenCalled(); 19 | restoreConsole(); 20 | }); 21 | 22 | test("success()", () => { 23 | const restoreConsole = mockConsole(); 24 | 25 | success("foobar"); 26 | dump(false); 27 | // eslint-disable-next-line no-console 28 | expect(console.log).toHaveBeenCalled(); 29 | restoreConsole(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/editJson.test.ts: -------------------------------------------------------------------------------- 1 | import detectIndent from "detect-indent"; 2 | import EditJson from "../src/editJson"; 3 | import { PackageFileNotFoundError } from "../src/errors"; 4 | 5 | jest.mock("fs"); 6 | 7 | describe("editJson.ts", () => { 8 | describe("it throws on file not found", () => { 9 | expect(() => new EditJson("foo/bar/baz")).toThrow( 10 | PackageFileNotFoundError 11 | ); 12 | }); 13 | 14 | it("has a default scripts section", () => { 15 | const fileWithoutScripts = new EditJson( 16 | "real/existing/path/package-without-scripts.json" 17 | ); 18 | 19 | expect(fileWithoutScripts.get().scripts).toEqual({}); 20 | }); 21 | 22 | const file = new EditJson("real/existing/path/package.json"); 23 | 24 | it("reads files", () => { 25 | expect(file).toMatchSnapshot(); 26 | }); 27 | test("get()", () => { 28 | expect(file.get()).toEqual({ scripts: { foo: "bar" } }); 29 | }); 30 | 31 | test("set()", () => { 32 | expect(file.set("scripts", { foo: "bar" }).get()).toEqual({ 33 | scripts: { foo: "bar" }, 34 | }); 35 | }); 36 | 37 | it("should preserve indentation", () => { 38 | expect(file.indent).toEqual({ amount: 2, indent: " ", type: "space" }); 39 | }); 40 | 41 | const saved = file.save(); 42 | 43 | test("save()", () => { 44 | expect((saved as any).written).toBe(true); 45 | }); 46 | 47 | it("respects indentation 1", () => { 48 | expect(detectIndent((saved as any).content).type).toEqual("space"); 49 | }); 50 | 51 | it("respects indentation 2", () => { 52 | const file2 = new EditJson("real/existing/path/package-with-tabs.json"); 53 | 54 | file2.set("scripts", { foo: "bar" }); 55 | const saved2 = file2.save(); 56 | 57 | expect(detectIndent((saved2 as any).content).type).toEqual("tab"); 58 | }); 59 | }); 60 | 61 | export {}; 62 | -------------------------------------------------------------------------------- /tests/execute.test.ts: -------------------------------------------------------------------------------- 1 | import execute from "../src/execute"; 2 | import { loadRulesFromRuleConfig } from "../src/loadRules"; 3 | import { ValidationFunctionInvalidError } from "../src/errors"; 4 | 5 | const rulesNonStrict = loadRulesFromRuleConfig(false); 6 | const rulesStrict = loadRulesFromRuleConfig(true); 7 | 8 | describe("execute.ts", () => { 9 | it("errors by default on empty scripts", () => { 10 | const [issues, fixed] = execute(rulesNonStrict, {}); 11 | 12 | expect(fixed).toEqual({}); 13 | expect(issues.map((i) => i.name)).toEqual([ 14 | "mandatory-test", 15 | "mandatory-start", 16 | "mandatory-dev", 17 | ]); 18 | }); 19 | 20 | describe("errors for invalid validation functions", () => { 21 | const rules = [ 22 | { 23 | isObjectRule: true, 24 | name: "foo", 25 | message: "bar", 26 | validate: "unknown", 27 | }, 28 | ]; 29 | 30 | const scripts = { 31 | foo: "bar", 32 | }; 33 | 34 | test("object rule", () => { 35 | expect(() => { 36 | execute(rules, scripts); 37 | }).toThrowError(ValidationFunctionInvalidError); 38 | }); 39 | 40 | test("entry rule", () => { 41 | rules[0].isObjectRule = false; 42 | expect(() => { 43 | execute(rules, scripts); 44 | }).toThrowError(ValidationFunctionInvalidError); 45 | }); 46 | }); 47 | 48 | it("doesn't complain about correct scripts (default, fixing)", () => { 49 | const scripts = { 50 | dev: "echo 1", 51 | start: "echo 1", 52 | test: "echo 1", 53 | }; 54 | 55 | const [issues, fixed] = execute( 56 | rulesNonStrict, 57 | scripts, 58 | () => {}, 59 | true 60 | ); 61 | 62 | expect(fixed).toEqual(scripts); 63 | expect(issues).toEqual([]); 64 | }); 65 | 66 | it("complains about rule violations (strict) #1", () => { 67 | const [issues] = execute(rulesStrict, { 68 | foo: "echo 1", 69 | }); 70 | 71 | expect(issues.map((i) => i.name)).toEqual([ 72 | "mandatory-test", 73 | "mandatory-start", 74 | "mandatory-dev", 75 | "uses-allowed-namespace", 76 | ]); 77 | }); 78 | 79 | it("complains about rule violations (strict) #2", () => { 80 | const mockWarningFn = jest.fn(); 81 | 82 | const [issues] = execute( 83 | rulesStrict, 84 | { 85 | dev: "echo 1", 86 | start: "echo 1", 87 | test: "echo 1", 88 | "preother:foobar": "echo 1", 89 | }, 90 | mockWarningFn 91 | ); 92 | 93 | expect(issues.map((i) => i.name)).toEqual([ 94 | "prepost-trigger-defined", 95 | "alphabetic-order", 96 | ]); 97 | expect(mockWarningFn).toHaveBeenCalled(); 98 | }); 99 | 100 | it("complains about rule violations (strict, fixed) #3", () => { 101 | const scripts = { 102 | "wrong-place-no-category-wrong-case": "echo 1", 103 | dev: "echo 1", 104 | start: "echo 1", 105 | test: "echo 1", 106 | }; 107 | 108 | const fixedShouldBe = { 109 | dev: "echo 1", 110 | "other:wrong-place-no-category-wrong-case": "echo 1", 111 | start: "echo 1", 112 | test: "echo 1", 113 | }; 114 | 115 | const mockWarningFn = jest.fn(); 116 | const [issues, fixed] = execute( 117 | rulesStrict, 118 | scripts, 119 | mockWarningFn, 120 | true 121 | ); 122 | 123 | expect(mockWarningFn).toHaveBeenCalled(); 124 | expect(issues.map((i) => i.name)).toEqual(["correct-casing"]); 125 | 126 | expect(fixed).toEqual(fixedShouldBe); 127 | 128 | expect(Object.keys(fixed).join(", ")).toEqual( 129 | Object.keys(fixedShouldBe).join(", ") 130 | ); 131 | }); 132 | 133 | it("doesn't complain about correct scripts (default)", () => { 134 | const [issues] = execute(rulesStrict, { 135 | dev: "echo 1", 136 | prepublishOnly: "echo 1", 137 | start: "echo 1", 138 | test: "echo 1", 139 | }); 140 | 141 | expect(issues).toEqual([]); 142 | }); 143 | }); 144 | 145 | export {}; 146 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import scriptlint from "../src/module"; 2 | 3 | jest.mock("fs"); 4 | jest.mock("path"); 5 | 6 | describe("index.ts", () => { 7 | it("should export a function", () => { 8 | expect(typeof scriptlint).toBe("function"); 9 | }); 10 | 11 | it("should throw with too much config", () => { 12 | expect(() => { 13 | scriptlint({ 14 | packageFile: "foo/bar/baz", 15 | packageScripts: { 16 | foo: "bar", 17 | }, 18 | }); 19 | }).toThrowError(/not both/); 20 | }); 21 | 22 | it("should throw with too little config", () => { 23 | expect(() => { 24 | scriptlint({}); 25 | }).toThrowError(/location or a scripts/); 26 | }); 27 | 28 | it("should lint files", () => { 29 | expect( 30 | scriptlint({ 31 | packageFile: "real/existing/path/package.json", 32 | }) 33 | ).toMatchSnapshot(); 34 | }); 35 | 36 | it("should lint correctly", () => { 37 | expect( 38 | scriptlint({ 39 | strict: true, 40 | packageScripts: {}, 41 | }).issues.length 42 | ).toBe(3); 43 | }); 44 | 45 | it("should lint correctly", () => { 46 | expect( 47 | scriptlint({ 48 | strict: false, 49 | packageScripts: { 50 | dev: "echo 1", 51 | test: "echo 1", 52 | start: "echo 1", 53 | }, 54 | }).issues.length 55 | ).toBe(0); 56 | }); 57 | 58 | it("should fix correctly", () => { 59 | expect( 60 | Object.keys( 61 | scriptlint({ 62 | fix: true, 63 | strict: true, 64 | packageScripts: { 65 | start: "echo 1", 66 | test: "echo 1", 67 | dev: "echo 1", 68 | foo: "bar", 69 | }, 70 | }).scripts 71 | ) 72 | ).toEqual(["dev", "other:foo", "start", "test"]); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/loadRules.test.ts: -------------------------------------------------------------------------------- 1 | import { loadRulesFromRuleConfig, getRuleByName } from "../src/loadRules"; 2 | import defaultRules from "../src/rules"; 3 | 4 | describe("loadRules.ts", () => { 5 | const defaultRulesLoaded = loadRulesFromRuleConfig(false); 6 | const strictRulesLoaded = loadRulesFromRuleConfig(true); 7 | 8 | it("loads custom rules", () => { 9 | const customRule = { 10 | name: "test-custom-rule-foobar", 11 | isObjectRule: false, 12 | message: "foobar", 13 | validate: () => true, 14 | }; 15 | 16 | const rulesWithCustomRule = loadRulesFromRuleConfig( 17 | false, 18 | { 19 | [customRule.name]: true, 20 | }, 21 | [customRule] 22 | ); 23 | 24 | expect( 25 | rulesWithCustomRule 26 | .map((r) => r.name) 27 | .filter((r) => r === customRule.name)[0] 28 | ).toBe(customRule.name); 29 | }); 30 | 31 | it("loads correct amount of rules", () => { 32 | expect(strictRulesLoaded.length).toBe(defaultRules.length); 33 | }); 34 | 35 | test("getRuleByName() nulls on unknown name", () => { 36 | expect(getRuleByName(defaultRulesLoaded, "foo")).toBe(null); 37 | expect( 38 | getRuleByName(defaultRulesLoaded, "mandatory-dev") 39 | ).toMatchSnapshot(); 40 | }); 41 | 42 | test("loadRulesFromSet() excludes rules by config", () => { 43 | const rules = loadRulesFromRuleConfig(true, { 44 | "mandatory-dev": false, 45 | "mandatory-start": false, 46 | "mandatory-test": false, 47 | }); 48 | 49 | expect(rules[0].name).toEqual("no-default-test"); 50 | }); 51 | 52 | test("loadRulesFromSet() adds custom rules", () => { 53 | const rules = loadRulesFromRuleConfig( 54 | true, 55 | { 56 | foobarbaz: true, 57 | }, 58 | [ 59 | { 60 | name: "foobarbaz", 61 | isObjectRule: true, 62 | message: "barbazfoo", 63 | validate: () => true, 64 | }, 65 | ] 66 | ); 67 | 68 | expect(rules[0].name).toEqual("foobarbaz"); 69 | }); 70 | 71 | test("loadRulesFromSet() ignores custom rules that are not loaded", () => { 72 | const rules = loadRulesFromRuleConfig(false, {}, [ 73 | { 74 | name: "foobarbaz", 75 | isObjectRule: true, 76 | message: "barbazfoo", 77 | validate: () => true, 78 | }, 79 | ]); 80 | 81 | expect(rules.map((r) => r.name).includes("foobarbaz")).toBe(false); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/rules/alphabetic-order.test.ts: -------------------------------------------------------------------------------- 1 | import rule, { sortScripts } from "../../src/rules/alphabetic-order"; 2 | 3 | describe("alphabetic-order.ts", () => { 4 | const invalidScripts = { 5 | foo: "bar", 6 | aba: "cus", 7 | }; 8 | 9 | describe("validate()", () => { 10 | it("should validate correctly", () => { 11 | expect(rule.validate({})).toBe(true); 12 | expect(rule.validate(invalidScripts)).toBe(false); 13 | }); 14 | }); 15 | 16 | it("should fix issues", () => { 17 | expect(rule.validate(rule.fix(invalidScripts))).toBe(true); 18 | }); 19 | 20 | describe("sortScripts()", () => { 21 | it("should sort correctly", () => { 22 | expect(sortScripts({})).toEqual({}); 23 | expect( 24 | Object.keys( 25 | sortScripts({ 26 | x: "-1", 27 | a: "1", 28 | 0: "2", 29 | b: "0", 30 | }) 31 | ) 32 | ).toEqual(["0", "a", "b", "x"]); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/rules/correct-casing.test.ts: -------------------------------------------------------------------------------- 1 | import rule from "../../src/rules/correct-casing"; 2 | 3 | describe("correct-casing.ts", () => { 4 | it("should validate correctly", () => { 5 | expect(rule.validate("foobar")).toBe(true); 6 | expect(rule.validate("fooBar")).toBe(true); 7 | expect(rule.validate("foo:bar")).toBe(true); 8 | expect(rule.validate("foo-bar")).toBe(false); 9 | expect(rule.validate("foo_bar")).toBe(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/rules/mandatoryScriptFactory.test.ts: -------------------------------------------------------------------------------- 1 | import factory from "../../src/rules/mandatoryScriptFactory"; 2 | 3 | describe("mandatoryScriptFactory.ts", () => { 4 | it("should validate correctly", () => { 5 | const rule = factory("dev"); 6 | const validate = rule.validate; 7 | 8 | expect(typeof validate).toBe("function"); 9 | 10 | if (typeof validate !== "function") { 11 | return; 12 | } 13 | 14 | expect( 15 | validate({ 16 | foo: "bar", 17 | }) 18 | ).toBe(false); 19 | 20 | expect( 21 | validate({ 22 | dev: "bar", 23 | }) 24 | ).toBe(true); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/rules/no-aliases.test.ts: -------------------------------------------------------------------------------- 1 | import rule from "../../src/rules/no-aliases"; 2 | 3 | describe("no-aliases.ts", () => { 4 | it("should validate correctly", () => { 5 | expect(rule.validate("foo", "bar")).toBe(true); 6 | expect(rule.validate("release", "release")).toBe(true); 7 | expect(rule.validate("bar", "bar")).toBe(false); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/rules/no-default-test.test.ts: -------------------------------------------------------------------------------- 1 | import rule from "../../src/rules/no-default-test"; 2 | 3 | describe("no-default-test.ts", () => { 4 | it("should validate correctly", () => { 5 | expect(rule.validate({})).toBe(true); 6 | expect( 7 | rule.validate({ 8 | test: "echo 1", 9 | }) 10 | ).toBe(true); 11 | expect( 12 | rule.validate({ 13 | test: 'echo "Error: no test specified" && exit 1', 14 | }) 15 | ).toBe(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/rules/noShellSpecificsFactory.test.ts: -------------------------------------------------------------------------------- 1 | import factory from "../../src/rules/noShellSpecificsFactory"; 2 | 3 | describe("mandatoryScriptFactory.ts", () => { 4 | it("should validate correctly 1", () => { 5 | const rule = factory(/foobar/, "foobar", "barfoo"); 6 | const validate = rule.validate; 7 | 8 | expect(typeof validate).toBe("function"); 9 | 10 | if (typeof validate !== "function") { 11 | return; 12 | } 13 | 14 | expect(validate("scriptName123", "foobar --param 1 2 3")).toBe(false); 15 | 16 | expect(validate("scriptName123", "barfoo --param 1 2 3")).toBe(true); 17 | }); 18 | 19 | it("should validate correctly 2", () => { 20 | const rule = factory( 21 | / && /, 22 | "unix operators (&&)", 23 | "npm-run-all/run-s" 24 | ); 25 | 26 | const validate = rule.validate; 27 | 28 | expect(typeof validate).toBe("function"); 29 | 30 | if (typeof validate !== "function") { 31 | return; 32 | } 33 | 34 | expect(validate("scriptName123", "eslint && jest")).toBe(false); 35 | 36 | expect(validate("scriptName123", "run-s eslint jest")).toBe(true); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/rules/prepost-trigger-defined.test.ts: -------------------------------------------------------------------------------- 1 | import rule from "../../src/rules/prepost-trigger-defined"; 2 | 3 | describe("prepost-trigger-defined.ts", () => { 4 | it("should validate correctly", () => { 5 | expect(rule.validate({})).toBe(true); 6 | expect( 7 | rule.validate({ 8 | prefoo: "echo 1", 9 | foo: "echo 1", 10 | }) 11 | ).toBe(true); 12 | 13 | expect( 14 | rule.validate({ 15 | prefoo: "echo 1", 16 | }) 17 | ).toEqual(["prefoo"]); 18 | 19 | expect( 20 | rule.validate({ 21 | postfoo: "echo 1", 22 | }) 23 | ).toEqual(["postfoo"]); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/rules/uses-allowed-namespace.test.ts: -------------------------------------------------------------------------------- 1 | import rule from "../../src/rules/uses-allowed-namespace"; 2 | 3 | describe("uses-allowed-namespace.ts", () => { 4 | it("should validate correctly", () => { 5 | expect(rule.validate("foo")).toBe(false); 6 | expect(rule.validate("other:foo")).toBe(true); 7 | expect(rule.validate("script:cms")).toBe(true); 8 | expect(rule.validate("preFoo")).toBe(false); 9 | expect(rule.validate("postFoo")).toBe(false); 10 | expect(rule.validate("preother:foo")).toBe(true); 11 | expect(rule.validate("postother:foo")).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/userConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CONFIG } from "../src/constants"; 2 | import loadConfig, { sanitizeConfig } from "../src/userConfig"; 3 | 4 | const validConfig = { 5 | strict: true, 6 | fix: false, 7 | json: false, 8 | config: false, 9 | rules: { foo: "bar" }, 10 | ignoreScripts: ["foo"], 11 | customRules: [ 12 | { 13 | isObjectRule: false, 14 | name: "foobar", 15 | message: "barbaz", 16 | validate: () => true, 17 | }, 18 | ], 19 | }; 20 | 21 | const invalidConfig = { 22 | invalid: 3, 23 | }; 24 | 25 | describe("userConfig.ts", () => { 26 | it("should sanitize configs: empty config => default", () => { 27 | expect(sanitizeConfig({})).toEqual(DEFAULT_CONFIG); 28 | }); 29 | it("should sanitize configs: null => default", () => { 30 | expect(sanitizeConfig(null)).toEqual(DEFAULT_CONFIG); 31 | }); 32 | it("should sanitize configs: invalid keys", () => { 33 | expect(() => { 34 | sanitizeConfig(invalidConfig); 35 | }).toThrowErrorMatchingSnapshot(); 36 | }); 37 | it("should sanitize configs: valid => valid", () => { 38 | expect(sanitizeConfig(validConfig)).toEqual(validConfig); 39 | }); 40 | 41 | test("loadConfig()", () => { 42 | const loaded = loadConfig(); 43 | 44 | expect(loaded).toMatchSnapshot(); 45 | }); 46 | 47 | test("loadConfig() with config missing", () => { 48 | const loaded = loadConfig("missing"); 49 | 50 | expect(loaded).toBe(DEFAULT_CONFIG); 51 | }); 52 | }); 53 | 54 | export {}; 55 | -------------------------------------------------------------------------------- /tests/userPackageScripts.test.ts: -------------------------------------------------------------------------------- 1 | import context from "../src/userPackageScripts"; 2 | 3 | const { readPackageScripts, writePackageScripts } = context( 4 | "real/existing/path/package.json" 5 | ); 6 | 7 | const setMock = jest.fn(); 8 | const saveMock = jest.fn(); 9 | 10 | jest.mock("../src/editJson", () => 11 | jest.fn(() => ({ 12 | get: () => ({ 13 | scripts: { 14 | foo: "bar", 15 | }, 16 | }), 17 | set: (path: string, content: Record) => 18 | setMock(path, content), 19 | save: () => saveMock(), 20 | })) 21 | ); 22 | 23 | describe("userPackageScripts.ts", () => { 24 | it("reads package.json files", () => { 25 | const thisPackageJson = readPackageScripts([]); 26 | 27 | expect(Object.keys(thisPackageJson).length > 0).toBe(true); 28 | }); 29 | 30 | it("fails when it no package.json was found", () => { 31 | try { 32 | readPackageScripts([]); 33 | 34 | expect(true).toBe(false); 35 | } catch (e) { 36 | expect((e as Error).message).toBeDefined(); 37 | } 38 | }); 39 | 40 | it("fails when package.json has no scripts", () => { 41 | try { 42 | readPackageScripts([]); 43 | 44 | expect(true).toBe(false); 45 | } catch (e) { 46 | expect((e as Error).message).toBeDefined(); 47 | } 48 | }); 49 | 50 | it("writes back to the file", () => { 51 | writePackageScripts({ 52 | foo: "bar", 53 | }); 54 | expect(setMock).toHaveBeenCalledWith("scripts", { 55 | foo: "bar", 56 | }); 57 | expect(saveMock).toHaveBeenCalled(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | slugify, 3 | filterPackageScriptsByKeys, 4 | makeMessage, 5 | makePackageFilePath, 6 | patchScriptObjectEntry, 7 | } from "../src/utils"; 8 | import { PackageFileNotFoundError } from "../src/errors"; 9 | 10 | jest.mock("fs"); 11 | jest.mock("path"); 12 | 13 | describe("patchScriptObjectEntry()", () => { 14 | expect( 15 | patchScriptObjectEntry( 16 | { 17 | bar: "1", 18 | foo: "2", 19 | }, 20 | "bar", 21 | "xxx", 22 | "5" 23 | ) 24 | ).toEqual({ 25 | xxx: "5", 26 | foo: "2", 27 | }); 28 | }); 29 | 30 | describe("slugify()", () => { 31 | it("should leave empty strings alone", () => { 32 | expect(slugify("")).toEqual(""); 33 | }); 34 | 35 | it("should already slugged strings alone", () => { 36 | expect(slugify("this-is-fine")).toEqual("this-is-fine"); 37 | }); 38 | 39 | it("should lowercase some stuff", () => { 40 | expect(slugify("LOWERCASE-THIS")).toEqual("lowercase-this"); 41 | }); 42 | 43 | it("should sluggify correctly 1", () => { 44 | expect(slugify("this is NOT cool! ")).toEqual("this-is-not-cool"); 45 | }); 46 | 47 | it("should sluggify correctly 2", () => { 48 | expect(slugify("!APPARENTLY\tSOMETHING WENT 🚨 WRONG!")).toEqual( 49 | "apparently-something-went-wrong" 50 | ); 51 | }); 52 | 53 | it("should sluggify correctly 3", () => { 54 | expect(slugify("rm -rf")).toEqual("rm-rf"); 55 | }); 56 | 57 | it("should sluggify correctly 4", () => { 58 | expect(slugify("unix-operators (&)")).toEqual("unix-operators"); 59 | }); 60 | }); 61 | 62 | describe("filterPackageScriptsByKeys()", () => { 63 | it("filters objects by keys correctly", () => { 64 | expect( 65 | filterPackageScriptsByKeys( 66 | { foo: "echo 1", bar: "echo 2", baz: "echo 3" }, 67 | ["bar"] 68 | ) 69 | ).toEqual({ 70 | baz: "echo 3", 71 | foo: "echo 1", 72 | }); 73 | expect(filterPackageScriptsByKeys({ foo: "echo 1" }, [])).toEqual({ 74 | foo: "echo 1", 75 | }); 76 | expect(filterPackageScriptsByKeys({}, [])).toEqual({}); 77 | }); 78 | }); 79 | 80 | describe("makePackageFilePath()", () => { 81 | it("throws on file not found", () => { 82 | expect(() => { 83 | makePackageFilePath("foo/bar/baz"); 84 | }).toThrowError(PackageFileNotFoundError); 85 | }); 86 | 87 | it("works with package.json", () => { 88 | const path = makePackageFilePath("real/existing/path/package.json"); 89 | 90 | expect(path).toEqual("real/existing/path/package.json"); 91 | }); 92 | 93 | it("works without package.json", () => { 94 | const path = makePackageFilePath("real/existing/path"); 95 | 96 | expect(path).toEqual("real/existing/path/package.json"); 97 | }); 98 | }); 99 | 100 | describe("makeMessage()", () => { 101 | it("compiles correctly", () => { 102 | expect(makeMessage("foo {{bar}}", { bar: "test" })).toEqual("foo test"); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": ["es2015"], 8 | "module": "commonjs", 9 | "noImplicitAny": true, 10 | "outDir": "./dist", 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "strictFunctionTypes": true, 14 | "strictNullChecks": true, 15 | "target": "ES2015", 16 | }, 17 | "include": ["./src", "./tests", "./jest.config.ts"] 18 | } 19 | --------------------------------------------------------------------------------