├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── action.yml ├── action ├── index.js └── index.ts ├── assets ├── cover.png └── logo.png ├── fixtures ├── basic │ ├── docs │ │ └── package.json │ ├── package.json │ └── packages │ │ ├── abc │ │ └── package.json │ │ └── def │ │ └── package.json ├── dependencies-nested-star │ ├── package.json │ └── packages │ │ ├── docs │ │ └── package.json │ │ └── other │ │ ├── abc │ │ └── package.json │ │ └── def │ │ └── package.json ├── dependencies-star │ ├── docs │ │ └── package.json │ ├── package.json │ └── packages │ │ ├── abc │ │ └── package.json │ │ └── def │ │ └── package.json ├── dependencies │ ├── docs │ │ └── package.json │ ├── package.json │ └── packages │ │ ├── abc │ │ └── package.json │ │ └── def │ │ └── package.json ├── empty │ └── none ├── ignore-paths │ ├── docs │ │ └── package.json │ ├── package.json │ ├── packages │ │ ├── a │ │ │ ├── b │ │ │ │ ├── d │ │ │ │ │ └── package.json │ │ │ │ └── e │ │ │ │ │ └── package.json │ │ │ └── c │ │ │ │ └── package.json │ │ ├── abc │ │ │ └── package.json │ │ ├── def │ │ │ └── package.json │ │ └── ghi │ │ │ └── package.json │ └── pnpm-workspace.yaml ├── install │ ├── apps │ │ ├── abc │ │ │ └── package.json │ │ └── def │ │ │ └── package.json │ ├── package-lock.json │ └── package.json ├── no-workspace-pnpm │ └── package.json ├── pnpm-glob │ ├── .github │ │ └── workflows │ │ │ └── ci.yml │ ├── @ui │ │ └── package.json │ ├── @web │ │ └── package.json │ ├── package.json │ └── pnpm-workspace.yaml ├── pnpm │ ├── docs │ │ └── package.json │ ├── package.json │ ├── packages │ │ ├── abc │ │ │ └── package.json │ │ └── def │ │ │ └── package.json │ └── pnpm-workspace.yaml ├── root-issues-fixed │ └── package.json ├── root-issues │ └── package.json ├── unordered │ ├── docs │ │ └── package.json │ └── package.json ├── unsync │ ├── package.json │ └── packages │ │ ├── abc │ │ └── package.json │ │ └── def │ │ └── package.json ├── without-package-json │ ├── docs │ │ └── none │ ├── package.json │ └── packages │ │ ├── .npm │ │ └── none │ │ ├── abc │ │ └── none │ │ └── def │ │ └── package.json └── yarn-nohoist │ ├── docs │ └── package.json │ ├── package.json │ └── packages │ ├── abc │ └── package.json │ └── def │ └── package.json ├── npm ├── app │ ├── index.js │ └── package.json └── package.json.tmpl ├── package.json ├── pnpm-lock.yaml ├── src ├── args.rs ├── collect.rs ├── install.rs ├── json.rs ├── main.rs ├── packages │ ├── mod.rs │ ├── root.rs │ └── semversion.rs ├── plural.rs ├── printer.rs └── rules │ ├── empty_dependencies.rs │ ├── mod.rs │ ├── multiple_dependency_versions.rs │ ├── non_existant_packages.rs │ ├── packages_without_package_json.rs │ ├── root_package_dependencies.rs │ ├── root_package_manager_field.rs │ ├── root_package_private_field.rs │ ├── snapshots │ ├── sherif__rules__empty_dependencies__test__dependency_kind-2.snap │ ├── sherif__rules__empty_dependencies__test__dependency_kind-3.snap │ ├── sherif__rules__empty_dependencies__test__dependency_kind-4.snap │ ├── sherif__rules__empty_dependencies__test__dependency_kind.snap │ ├── sherif__rules__multiple_dependency_versions__test__dedupe.snap │ ├── sherif__rules__multiple_dependency_versions__test__exact_and_range.snap │ ├── sherif__rules__multiple_dependency_versions__test__order_multiple.snap │ ├── sherif__rules__multiple_dependency_versions__test__order_prerelease.snap │ ├── sherif__rules__multiple_dependency_versions__test__order_single.snap │ ├── sherif__rules__multiple_dependency_versions__test__root.snap │ ├── sherif__rules__non_existant_packages__test__package_workspace.snap │ ├── sherif__rules__non_existant_packages__test__pnpm_workspace.snap │ ├── sherif__rules__non_existant_packages__test__test.snap │ ├── sherif__rules__root_package_dependencies__test__test.snap │ ├── sherif__rules__root_package_manager_field__test__test.snap │ ├── sherif__rules__root_package_private_field__test__private_field_not_set.snap │ ├── sherif__rules__root_package_private_field__test__private_field_set_not_true.snap │ ├── sherif__rules__types_in_dependencies__test__test.snap │ ├── sherif__rules__unordered_dependencies__test__dependency_kind-2.snap │ ├── sherif__rules__unordered_dependencies__test__dependency_kind-3.snap │ ├── sherif__rules__unordered_dependencies__test__dependency_kind-4.snap │ ├── sherif__rules__unordered_dependencies__test__dependency_kind.snap │ └── sherif__rules__unsync_similar_dependencies__tests__basic.snap │ ├── types_in_dependencies.rs │ ├── unordered_dependencies.rs │ └── unsync_similar_dependencies.rs └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Install toolchain 15 | uses: dtolnay/rust-toolchain@stable 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Check 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | args: --locked --verbose 23 | 24 | test: 25 | name: Test 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - name: Install toolchain 29 | uses: dtolnay/rust-toolchain@stable 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Test 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: test 36 | args: --verbose -- --test-threads=1 37 | 38 | lint: 39 | name: Lint 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - name: Install toolchain 43 | uses: dtolnay/rust-toolchain@stable 44 | with: 45 | components: clippy 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | - name: Lint 49 | uses: actions-rs/cargo@v1 50 | with: 51 | command: clippy 52 | args: --tests --verbose -- -D warnings 53 | 54 | format: 55 | name: Format 56 | runs-on: ubuntu-22.04 57 | steps: 58 | - name: Install toolchain 59 | uses: dtolnay/rust-toolchain@stable 60 | with: 61 | components: rustfmt 62 | - name: Checkout 63 | uses: actions/checkout@v4 64 | - name: Format 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: fmt 68 | args: --all -- --check --verbose 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | publish-npm-binaries: 14 | name: Publish NPM binaries 15 | runs-on: ${{ matrix.build.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | build: 20 | - { 21 | NAME: linux-x64-glibc, 22 | OS: ubuntu-20.04, 23 | TOOLCHAIN: stable, 24 | TARGET: x86_64-unknown-linux-gnu, 25 | BIN: sherif 26 | } 27 | - { 28 | NAME: linux-arm64-glibc, 29 | OS: ubuntu-20.04, 30 | TOOLCHAIN: stable, 31 | TARGET: aarch64-unknown-linux-gnu, 32 | BIN: sherif 33 | } 34 | - { 35 | NAME: win32-x64-msvc, 36 | OS: windows-2022, 37 | TOOLCHAIN: stable, 38 | TARGET: x86_64-pc-windows-msvc, 39 | BIN: sherif.exe 40 | } 41 | - { 42 | NAME: win32-arm64-msvc, 43 | OS: windows-2022, 44 | TOOLCHAIN: stable, 45 | TARGET: aarch64-pc-windows-msvc, 46 | BIN: sherif.exe 47 | } 48 | - { 49 | NAME: darwin-x64, 50 | OS: macos-13, 51 | TOOLCHAIN: stable, 52 | TARGET: x86_64-apple-darwin, 53 | BIN: sherif 54 | } 55 | - { 56 | NAME: darwin-arm64, 57 | OS: macos-13, 58 | TOOLCHAIN: stable, 59 | TARGET: aarch64-apple-darwin, 60 | BIN: sherif 61 | } 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Set the release version 67 | shell: bash 68 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 69 | 70 | - name: Install Rust toolchain 71 | uses: actions-rs/toolchain@v1 72 | with: 73 | toolchain: ${{ matrix.build.TOOLCHAIN }} 74 | target: ${{ matrix.build.TARGET }} 75 | override: true 76 | 77 | - name: Build 78 | uses: actions-rs/cargo@v1 79 | with: 80 | command: build 81 | args: --release --locked --target ${{ matrix.build.TARGET }} 82 | use-cross: ${{ matrix.build.OS == 'ubuntu-20.04' }} # use `cross` for Linux builds 83 | 84 | - name: Install node 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: 20 88 | registry-url: "https://registry.npmjs.org" 89 | 90 | - name: Publish to NPM 91 | shell: bash 92 | run: | 93 | cd npm 94 | # derive the OS and architecture from the build matrix name 95 | # note: when split by a hyphen, first part is the OS and the second is the architecture 96 | node_os=$(echo "${{ matrix.build.NAME }}" | cut -d '-' -f1) 97 | export node_os 98 | node_arch=$(echo "${{ matrix.build.NAME }}" | cut -d '-' -f2) 99 | export node_arch 100 | # set the version 101 | export node_version="${{ env.RELEASE_VERSION }}" 102 | # set the package name 103 | # note: use 'windows' as OS name instead of 'win32' 104 | if [ "${{ matrix.build.OS }}" = "windows-2022" ]; then 105 | export node_pkg="sherif-windows-${node_arch}" 106 | else 107 | export node_pkg="sherif-${node_os}-${node_arch}" 108 | fi 109 | # create the package directory 110 | mkdir -p "${node_pkg}/bin" 111 | # generate package.json from the template 112 | envsubst < package.json.tmpl > "${node_pkg}/package.json" 113 | cp "../target/${{ matrix.build.TARGET }}/release/${{ matrix.build.BIN }}" "${node_pkg}/bin" 114 | cp ../README.md "${node_pkg}" 115 | # publish the package 116 | cd "${node_pkg}" 117 | npm publish --access public 118 | env: 119 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 120 | 121 | - name: Upload Artifact 122 | uses: actions/upload-artifact@v4 123 | with: 124 | name: sherif-${{ matrix.build.TARGET }} 125 | path: target/${{ matrix.build.TARGET }}/release/${{ matrix.build.BIN }} 126 | 127 | publish-npm-base: 128 | name: Publish NPM package 129 | needs: publish-npm-binaries 130 | runs-on: ubuntu-20.04 131 | steps: 132 | - name: Checkout 133 | uses: actions/checkout@v4 134 | 135 | - name: Install node 136 | uses: actions/setup-node@v4 137 | with: 138 | node-version: 20 139 | registry-url: "https://registry.npmjs.org" 140 | 141 | - name: Publish the package 142 | shell: bash 143 | run: | 144 | cd npm/app 145 | cp ../../README.md . 146 | npm publish --access public 147 | env: 148 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | node_modules/ 3 | 4 | .DS_Store 5 | *.log 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.5.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.3" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 48 | dependencies = [ 49 | "windows-sys 0.48.0", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "2.1.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys 0.48.0", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.75" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 67 | 68 | [[package]] 69 | name = "autocfg" 70 | version = "1.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "1.3.2" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 79 | 80 | [[package]] 81 | name = "bitflags" 82 | version = "2.4.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 85 | 86 | [[package]] 87 | name = "cc" 88 | version = "1.0.83" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 91 | dependencies = [ 92 | "libc", 93 | ] 94 | 95 | [[package]] 96 | name = "cfg-if" 97 | version = "1.0.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 100 | 101 | [[package]] 102 | name = "clap" 103 | version = "4.4.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" 106 | dependencies = [ 107 | "clap_builder", 108 | "clap_derive", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_builder" 113 | version = "4.4.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" 116 | dependencies = [ 117 | "anstream", 118 | "anstyle", 119 | "clap_lex", 120 | "strsim", 121 | ] 122 | 123 | [[package]] 124 | name = "clap_derive" 125 | version = "4.4.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 128 | dependencies = [ 129 | "heck", 130 | "proc-macro2", 131 | "quote", 132 | "syn", 133 | ] 134 | 135 | [[package]] 136 | name = "clap_lex" 137 | version = "0.5.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 140 | 141 | [[package]] 142 | name = "colorchoice" 143 | version = "1.0.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 146 | 147 | [[package]] 148 | name = "colored" 149 | version = "2.0.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 152 | dependencies = [ 153 | "is-terminal", 154 | "lazy_static", 155 | "windows-sys 0.48.0", 156 | ] 157 | 158 | [[package]] 159 | name = "console" 160 | version = "0.15.7" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 163 | dependencies = [ 164 | "encode_unicode", 165 | "lazy_static", 166 | "libc", 167 | "windows-sys 0.45.0", 168 | ] 169 | 170 | [[package]] 171 | name = "crossterm" 172 | version = "0.25.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 175 | dependencies = [ 176 | "bitflags 1.3.2", 177 | "crossterm_winapi", 178 | "libc", 179 | "mio", 180 | "parking_lot", 181 | "signal-hook", 182 | "signal-hook-mio", 183 | "winapi", 184 | ] 185 | 186 | [[package]] 187 | name = "crossterm_winapi" 188 | version = "0.9.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 191 | dependencies = [ 192 | "winapi", 193 | ] 194 | 195 | [[package]] 196 | name = "debugless-unwrap" 197 | version = "0.0.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "f400d0750c0c069e8493f2256cb4da6f604b6d2eeb69a0ca8863acde352f8400" 200 | 201 | [[package]] 202 | name = "detect-indent" 203 | version = "0.1.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "9ae11867b75e44bacc8baf64be8abe6501c6571bbf33fed819a0a90623c82d1b" 206 | dependencies = [ 207 | "lazy_static", 208 | "regex", 209 | ] 210 | 211 | [[package]] 212 | name = "detect-newline-style" 213 | version = "0.1.2" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "1124f25c3615ab547669f878088cef84850679327f79eccc70412c25a6643749" 216 | dependencies = [ 217 | "regex", 218 | ] 219 | 220 | [[package]] 221 | name = "dyn-clone" 222 | version = "1.0.16" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" 225 | 226 | [[package]] 227 | name = "encode_unicode" 228 | version = "0.3.6" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 231 | 232 | [[package]] 233 | name = "equivalent" 234 | version = "1.0.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 237 | 238 | [[package]] 239 | name = "errno" 240 | version = "0.3.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" 243 | dependencies = [ 244 | "errno-dragonfly", 245 | "libc", 246 | "windows-sys 0.48.0", 247 | ] 248 | 249 | [[package]] 250 | name = "errno-dragonfly" 251 | version = "0.1.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 254 | dependencies = [ 255 | "cc", 256 | "libc", 257 | ] 258 | 259 | [[package]] 260 | name = "hashbrown" 261 | version = "0.14.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 264 | 265 | [[package]] 266 | name = "heck" 267 | version = "0.4.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 270 | 271 | [[package]] 272 | name = "hermit-abi" 273 | version = "0.3.2" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 276 | 277 | [[package]] 278 | name = "indexmap" 279 | version = "2.0.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 282 | dependencies = [ 283 | "equivalent", 284 | "hashbrown", 285 | "serde", 286 | ] 287 | 288 | [[package]] 289 | name = "inquire" 290 | version = "0.6.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" 293 | dependencies = [ 294 | "bitflags 1.3.2", 295 | "crossterm", 296 | "dyn-clone", 297 | "lazy_static", 298 | "newline-converter", 299 | "thiserror", 300 | "unicode-segmentation", 301 | "unicode-width", 302 | ] 303 | 304 | [[package]] 305 | name = "insta" 306 | version = "1.32.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea" 309 | dependencies = [ 310 | "console", 311 | "lazy_static", 312 | "linked-hash-map", 313 | "similar", 314 | "yaml-rust", 315 | ] 316 | 317 | [[package]] 318 | name = "is-terminal" 319 | version = "0.4.9" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 322 | dependencies = [ 323 | "hermit-abi", 324 | "rustix", 325 | "windows-sys 0.48.0", 326 | ] 327 | 328 | [[package]] 329 | name = "itoa" 330 | version = "1.0.9" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 333 | 334 | [[package]] 335 | name = "lazy_static" 336 | version = "1.4.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 339 | 340 | [[package]] 341 | name = "libc" 342 | version = "0.2.150" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 345 | 346 | [[package]] 347 | name = "linked-hash-map" 348 | version = "0.5.6" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 351 | 352 | [[package]] 353 | name = "linux-raw-sys" 354 | version = "0.4.7" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" 357 | 358 | [[package]] 359 | name = "lock_api" 360 | version = "0.4.11" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 363 | dependencies = [ 364 | "autocfg", 365 | "scopeguard", 366 | ] 367 | 368 | [[package]] 369 | name = "log" 370 | version = "0.4.20" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 373 | 374 | [[package]] 375 | name = "memchr" 376 | version = "2.7.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 379 | 380 | [[package]] 381 | name = "mio" 382 | version = "0.8.9" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" 385 | dependencies = [ 386 | "libc", 387 | "log", 388 | "wasi", 389 | "windows-sys 0.48.0", 390 | ] 391 | 392 | [[package]] 393 | name = "newline-converter" 394 | version = "0.2.2" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" 397 | dependencies = [ 398 | "unicode-segmentation", 399 | ] 400 | 401 | [[package]] 402 | name = "parking_lot" 403 | version = "0.12.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 406 | dependencies = [ 407 | "lock_api", 408 | "parking_lot_core", 409 | ] 410 | 411 | [[package]] 412 | name = "parking_lot_core" 413 | version = "0.9.9" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 416 | dependencies = [ 417 | "cfg-if", 418 | "libc", 419 | "redox_syscall", 420 | "smallvec", 421 | "windows-targets 0.48.5", 422 | ] 423 | 424 | [[package]] 425 | name = "proc-macro2" 426 | version = "1.0.67" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 429 | dependencies = [ 430 | "unicode-ident", 431 | ] 432 | 433 | [[package]] 434 | name = "quote" 435 | version = "1.0.33" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 438 | dependencies = [ 439 | "proc-macro2", 440 | ] 441 | 442 | [[package]] 443 | name = "redox_syscall" 444 | version = "0.4.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 447 | dependencies = [ 448 | "bitflags 1.3.2", 449 | ] 450 | 451 | [[package]] 452 | name = "regex" 453 | version = "1.10.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 456 | dependencies = [ 457 | "aho-corasick", 458 | "memchr", 459 | "regex-automata", 460 | "regex-syntax", 461 | ] 462 | 463 | [[package]] 464 | name = "regex-automata" 465 | version = "0.4.5" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 468 | dependencies = [ 469 | "aho-corasick", 470 | "memchr", 471 | "regex-syntax", 472 | ] 473 | 474 | [[package]] 475 | name = "regex-syntax" 476 | version = "0.8.2" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 479 | 480 | [[package]] 481 | name = "rustix" 482 | version = "0.38.13" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" 485 | dependencies = [ 486 | "bitflags 2.4.0", 487 | "errno", 488 | "libc", 489 | "linux-raw-sys", 490 | "windows-sys 0.48.0", 491 | ] 492 | 493 | [[package]] 494 | name = "ryu" 495 | version = "1.0.15" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 498 | 499 | [[package]] 500 | name = "scopeguard" 501 | version = "1.2.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 504 | 505 | [[package]] 506 | name = "semver" 507 | version = "1.0.18" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" 510 | 511 | [[package]] 512 | name = "serde" 513 | version = "1.0.188" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 516 | dependencies = [ 517 | "serde_derive", 518 | ] 519 | 520 | [[package]] 521 | name = "serde_derive" 522 | version = "1.0.188" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 525 | dependencies = [ 526 | "proc-macro2", 527 | "quote", 528 | "syn", 529 | ] 530 | 531 | [[package]] 532 | name = "serde_json" 533 | version = "1.0.107" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 536 | dependencies = [ 537 | "indexmap", 538 | "itoa", 539 | "ryu", 540 | "serde", 541 | ] 542 | 543 | [[package]] 544 | name = "serde_yaml" 545 | version = "0.9.25" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" 548 | dependencies = [ 549 | "indexmap", 550 | "itoa", 551 | "ryu", 552 | "serde", 553 | "unsafe-libyaml", 554 | ] 555 | 556 | [[package]] 557 | name = "sherif" 558 | version = "1.5.0" 559 | dependencies = [ 560 | "anyhow", 561 | "clap", 562 | "colored", 563 | "debugless-unwrap", 564 | "detect-indent", 565 | "detect-newline-style", 566 | "indexmap", 567 | "inquire", 568 | "insta", 569 | "semver", 570 | "serde", 571 | "serde_json", 572 | "serde_yaml", 573 | ] 574 | 575 | [[package]] 576 | name = "signal-hook" 577 | version = "0.3.17" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 580 | dependencies = [ 581 | "libc", 582 | "signal-hook-registry", 583 | ] 584 | 585 | [[package]] 586 | name = "signal-hook-mio" 587 | version = "0.2.3" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 590 | dependencies = [ 591 | "libc", 592 | "mio", 593 | "signal-hook", 594 | ] 595 | 596 | [[package]] 597 | name = "signal-hook-registry" 598 | version = "1.4.1" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 601 | dependencies = [ 602 | "libc", 603 | ] 604 | 605 | [[package]] 606 | name = "similar" 607 | version = "2.2.1" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" 610 | 611 | [[package]] 612 | name = "smallvec" 613 | version = "1.11.2" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 616 | 617 | [[package]] 618 | name = "strsim" 619 | version = "0.10.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 622 | 623 | [[package]] 624 | name = "syn" 625 | version = "2.0.33" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "unicode-ident", 632 | ] 633 | 634 | [[package]] 635 | name = "thiserror" 636 | version = "1.0.50" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 639 | dependencies = [ 640 | "thiserror-impl", 641 | ] 642 | 643 | [[package]] 644 | name = "thiserror-impl" 645 | version = "1.0.50" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 648 | dependencies = [ 649 | "proc-macro2", 650 | "quote", 651 | "syn", 652 | ] 653 | 654 | [[package]] 655 | name = "unicode-ident" 656 | version = "1.0.12" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 659 | 660 | [[package]] 661 | name = "unicode-segmentation" 662 | version = "1.10.1" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 665 | 666 | [[package]] 667 | name = "unicode-width" 668 | version = "0.1.11" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 671 | 672 | [[package]] 673 | name = "unsafe-libyaml" 674 | version = "0.2.9" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" 677 | 678 | [[package]] 679 | name = "utf8parse" 680 | version = "0.2.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 683 | 684 | [[package]] 685 | name = "wasi" 686 | version = "0.11.0+wasi-snapshot-preview1" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 689 | 690 | [[package]] 691 | name = "winapi" 692 | version = "0.3.9" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 695 | dependencies = [ 696 | "winapi-i686-pc-windows-gnu", 697 | "winapi-x86_64-pc-windows-gnu", 698 | ] 699 | 700 | [[package]] 701 | name = "winapi-i686-pc-windows-gnu" 702 | version = "0.4.0" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 705 | 706 | [[package]] 707 | name = "winapi-x86_64-pc-windows-gnu" 708 | version = "0.4.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 711 | 712 | [[package]] 713 | name = "windows-sys" 714 | version = "0.45.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 717 | dependencies = [ 718 | "windows-targets 0.42.2", 719 | ] 720 | 721 | [[package]] 722 | name = "windows-sys" 723 | version = "0.48.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 726 | dependencies = [ 727 | "windows-targets 0.48.5", 728 | ] 729 | 730 | [[package]] 731 | name = "windows-targets" 732 | version = "0.42.2" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 735 | dependencies = [ 736 | "windows_aarch64_gnullvm 0.42.2", 737 | "windows_aarch64_msvc 0.42.2", 738 | "windows_i686_gnu 0.42.2", 739 | "windows_i686_msvc 0.42.2", 740 | "windows_x86_64_gnu 0.42.2", 741 | "windows_x86_64_gnullvm 0.42.2", 742 | "windows_x86_64_msvc 0.42.2", 743 | ] 744 | 745 | [[package]] 746 | name = "windows-targets" 747 | version = "0.48.5" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 750 | dependencies = [ 751 | "windows_aarch64_gnullvm 0.48.5", 752 | "windows_aarch64_msvc 0.48.5", 753 | "windows_i686_gnu 0.48.5", 754 | "windows_i686_msvc 0.48.5", 755 | "windows_x86_64_gnu 0.48.5", 756 | "windows_x86_64_gnullvm 0.48.5", 757 | "windows_x86_64_msvc 0.48.5", 758 | ] 759 | 760 | [[package]] 761 | name = "windows_aarch64_gnullvm" 762 | version = "0.42.2" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 765 | 766 | [[package]] 767 | name = "windows_aarch64_gnullvm" 768 | version = "0.48.5" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 771 | 772 | [[package]] 773 | name = "windows_aarch64_msvc" 774 | version = "0.42.2" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 777 | 778 | [[package]] 779 | name = "windows_aarch64_msvc" 780 | version = "0.48.5" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 783 | 784 | [[package]] 785 | name = "windows_i686_gnu" 786 | version = "0.42.2" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 789 | 790 | [[package]] 791 | name = "windows_i686_gnu" 792 | version = "0.48.5" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 795 | 796 | [[package]] 797 | name = "windows_i686_msvc" 798 | version = "0.42.2" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 801 | 802 | [[package]] 803 | name = "windows_i686_msvc" 804 | version = "0.48.5" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 807 | 808 | [[package]] 809 | name = "windows_x86_64_gnu" 810 | version = "0.42.2" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 813 | 814 | [[package]] 815 | name = "windows_x86_64_gnu" 816 | version = "0.48.5" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 819 | 820 | [[package]] 821 | name = "windows_x86_64_gnullvm" 822 | version = "0.42.2" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 825 | 826 | [[package]] 827 | name = "windows_x86_64_gnullvm" 828 | version = "0.48.5" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 831 | 832 | [[package]] 833 | name = "windows_x86_64_msvc" 834 | version = "0.42.2" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 837 | 838 | [[package]] 839 | name = "windows_x86_64_msvc" 840 | version = "0.48.5" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 843 | 844 | [[package]] 845 | name = "yaml-rust" 846 | version = "0.4.5" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 849 | dependencies = [ 850 | "linked-hash-map", 851 | ] 852 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sherif" 3 | version = "1.5.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Tom Lienard"] 7 | description = "Opinionated, zero-config linter for JavaScript monorepos." 8 | homepage = "https://github.com/QuiiBz/sherif" 9 | repository = "https://github.com/QuiiBz/sherif" 10 | keywords = [ 11 | "cli", 12 | "javascript", 13 | "monorepo", 14 | "linter", 15 | ] 16 | categories = ["development-tools"] 17 | readme = "./README.md" 18 | 19 | [dependencies] 20 | anyhow = "1.0.75" 21 | clap = { version = "4.4.3", features = ["derive"] } 22 | colored = "2.0.4" 23 | detect-indent = "0.1.0" 24 | detect-newline-style = "0.1.2" 25 | indexmap = { version = "2.0.0", features = ["serde"] } 26 | inquire = "0.6.2" 27 | semver = "1.0.18" 28 | serde = { version = "1.0.188", features = ["derive"] } 29 | serde_json = { version = "1.0.107", features = ["preserve_order"] } 30 | serde_yaml = "0.9.25" 31 | 32 | [dev-dependencies] 33 | debugless-unwrap = "0.0.4" 34 | insta = "1.32.0" 35 | 36 | [profile.release] 37 | strip = "symbols" 38 | opt-level = "z" 39 | lto = "thin" 40 | codegen-units = 1 41 | panic = "abort" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tom Lienard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |
6 | Sherif: Opinionated, zero-config linter for JavaScript monorepos 7 |

8 | 9 | --- 10 | 11 | ![Cover](https://github.com/QuiiBz/sherif/blob/main/assets/cover.png) 12 | 13 | ## About 14 | 15 | Sherif is an opinionated, zero-config linter for JavaScript monorepos. It runs fast in any monorepo and enforces rules to provide a better, standardized DX. 16 | 17 | ## Features 18 | 19 | - ✨ **PNPM, NPM, Yarn...**: sherif works with all package managers 20 | - 🔎 **Zero-config**: it just works and prevents regressions 21 | - ⚡ **Fast**: doesn't need `node_modules` installed, written in 🦀 Rust 22 | 23 | ## Installation 24 | 25 | Run `sherif` in the root of your monorepo to list the found issues. Any error will cause Sherif to exit with a code 1: 26 | 27 | ```bash 28 | # PNPM 29 | pnpm dlx sherif@latest 30 | # NPM 31 | npx sherif@latest 32 | ``` 33 | 34 | We recommend running Sherif in your CI once [all errors are fixed](#autofix). Run it by **specifying a version instead of latest**. This is useful to prevent regressions (e.g. when adding a library to a package but forgetting to update the version in other packages of the monorepo). 35 | 36 | When using the GitHub Action, it will search for a `sherif` script in the root `package.json` and use the same arguments automatically to avoid repeating them twice. You can override this behaviour with the `args` parameter. 37 | 38 |
39 | 40 | GitHub Actions example 41 | 42 | ```yaml 43 | # Using the `QuiiBz/sherif` action 44 | name: Sherif 45 | on: 46 | pull_request: 47 | jobs: 48 | check: 49 | name: Run Sherif 50 | runs-on: ubuntu-22.04 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: QuiiBz/sherif@v1 54 | # Optionally, you can specify a version and arguments to run Sherif with: 55 | # with: 56 | # version: 'v1.5.0' 57 | # args: '--ignore-rule root-package-manager-field' 58 | 59 | # Using `npx` to run Sherif 60 | name: Sherif 61 | on: 62 | pull_request: 63 | jobs: 64 | check: 65 | name: Run Sherif 66 | runs-on: ubuntu-22.04 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-node@v3 70 | with: 71 | node-version: 20 72 | - run: npx sherif@1.5.0 73 | ``` 74 | 75 |
76 | 77 | ## Autofix 78 | 79 | Most issues can be automatically fixed by using the `--fix` (or `-f`) flag. Sherif will automatically run your package manager's `install` command (see [No-install mode](#no-install-mode) to disable this behavior) to update the lockfile. Note that autofix is disabled in CI environments (when `$CI` is set): 80 | 81 | ```bash 82 | sherif --fix 83 | ``` 84 | 85 | ### No-install mode 86 | 87 | If you don't want Sherif to run your packager manager's `install` command after running autofix, you can use the `--no-install` flag: 88 | 89 | ```bash 90 | sherif --fix --no-install 91 | ``` 92 | 93 | ## Rules 94 | 95 | You can ignore a specific rule by using `--ignore-rule ` (or `-r `): 96 | 97 | ```bash 98 | # Ignore both rules 99 | sherif -r packages-without-package-json -r root-package-manager-field 100 | ``` 101 | 102 | You can ignore all issues in a package by using `--ignore-package ` (or `-p `): 103 | 104 | ```bash 105 | # Ignore all issues in the `@repo/tools` package 106 | sherif -p @repo/tools 107 | # Ignore all issues for packages inside `./integrations/*` 108 | sherif -p "./integrations/*" 109 | ``` 110 | 111 | > **Note** 112 | > Sherif doesn't have many rules for now, but will likely have more in the future (along with more features). 113 | 114 | #### `empty-dependencies` ❌ 115 | 116 | `package.json` files should not have empty dependencies fields. 117 | 118 | #### `multiple-dependency-versions` ❌ 119 | 120 | A given dependency should use the same version across the monorepo. 121 | 122 | You can ignore this rule for a specific dependency and version or all versions of a dependency if it's expected in your monorepo by using `--ignore-dependency ` / `--ignore-dependency ` (or `-i ` / `-i `): 123 | 124 | ```bash 125 | # Ignore only the specific dependency version mismatch 126 | sherif -i react@17.0.2 -i next@13.2.4 127 | 128 | # Ignore all versions mismatch of dependencies that start with @next/ 129 | sherif -i @next/* 130 | 131 | # Completely ignore all versions mismatch of these dependencies 132 | sherif -i react -i next 133 | ``` 134 | 135 | #### `unsync-similar-dependencies` ❌ 136 | 137 | Similar dependencies in a given `package.json` should use the same version. For example, if you use both `react` and `react-dom` dependencies in the same `package.json`, this rule will enforce that they use the same version. 138 | 139 |
140 | 141 | List of detected similar dependencies 142 | 143 | - `react`, `react-dom` 144 | - `eslint-config-next`, `@next/eslint-plugin-next`, `@next/font` `@next/bundle-analyzer`, `@next/third-parties`, `@next/mdx`, `next` 145 | - `@trpc/client`, `@trpc/server`, `@trpc/next`, `@trpc/react-query` 146 | - `eslint-config-turbo`, `eslint-plugin-turbo`, `@turbo/gen`, `turbo-ignore`, `turbo` 147 | - `@tanstack/eslint-plugin-query`, `@tanstack/query-async-storage-persister`, `@tanstack/query-broadcast-client-experimental`, `@tanstack/query-core`, `@tanstack/query-devtools`, `@tanstack/query-persist-client-core`, `@tanstack/query-sync-storage-persister`, `@tanstack/react-query`, `@tanstack/react-query-devtools`, `@tanstack/react-query-persist-client`, `@tanstack/react-query-next-experimental`, `@tanstack/solid-query`, `@tanstack/solid-query-devtools`, `@tanstack/solid-query-persist-client`, `@tanstack/svelte-query`, `@tanstack/svelte-query-devtools`, `@tanstack/svelte-query-persist-client`, `@tanstack/vue-query`, `@tanstack/vue-query-devtools`, `@tanstack/angular-query-devtools-experimental`, `@tanstack/angular-query-experimental` 148 | - `sb`, `storybook`, `@storybook/codemod`, `@storybook/cli`, `@storybook/channels`, `@storybook/addon-actions`, `@storybook/addon-links`, `@storybook/react`, `@storybook/react-native`, `@storybook/components`, `@storybook/addon-backgrounds`, `@storybook/addon-viewport`, `@storybook/angular`, `@storybook/addon-a11y`, `@storybook/addon-jest`, `@storybook/client-logger`, `@storybook/node-logger`, `@storybook/core`, `@storybook/addon-storysource`, `@storybook/html`, `@storybook/core-events`, `@storybook/svelte`, `@storybook/ember`, `@storybook/addon-ondevice-backgrounds`, `@storybook/addon-ondevice-notes`, `@storybook/preact`, `@storybook/theming`, `@storybook/router`, `@storybook/addon-docs`, `@storybook/addon-ondevice-actions`, `@storybook/source-loader`, `@storybook/preset-create-react-app`, `@storybook/web-components`, `@storybook/addon-essentials`, `@storybook/server`, `@storybook/addon-toolbars`, `@storybook/addon-controls`, `@storybook/core-common`, `@storybook/builder-webpack5`, `@storybook/core-server`, `@storybook/csf-tools`, `@storybook/addon-measure`, `@storybook/addon-outline`, `@storybook/addon-ondevice-controls`, `@storybook/instrumenter`, `@storybook/addon-interactions`, `@storybook/docs-tools`, `@storybook/builder-vite`, `@storybook/telemetry`, `@storybook/core-webpack`, `@storybook/preset-html-webpack`, `@storybook/preset-preact-webpack`, `@storybook/preset-svelte-webpack`, `@storybook/preset-react-webpack`, `@storybook/html-webpack5`, `@storybook/preact-webpack5`, `@storybook/svelte-webpack5`, `@storybook/web-components-webpack5`, `@storybook/preset-server-webpack`, `@storybook/react-webpack5`, `@storybook/server-webpack5`, `@storybook/addon-highlight`, `@storybook/blocks`, `@storybook/builder-manager`, `@storybook/react-vite`, `@storybook/svelte-vite`, `@storybook/web-components-vite`, `@storybook/nextjs`, `@storybook/types`, `@storybook/manager`, `@storybook/csf-plugin`, `@storybook/preview`, `@storybook/manager-api`, `@storybook/preview-api`, `@storybook/html-vite`, `@storybook/sveltekit`, `@storybook/preact-vite`, `@storybook/addon-mdx-gfm`, `@storybook/react-dom-shim`, `create-storybook`, `@storybook/addon-onboarding`, `@storybook/react-native-theming`, `@storybook/addon-themes`, `@storybook/test`, `@storybook/react-native-ui`, `@storybook/experimental-nextjs-vite`, `@storybook/experimental-addon-test`, `@storybook/react-native-web-vite` 149 | - `prisma`, `@prisma/client`, `@prisma/instrumentation` 150 | - `typescript-eslint`, `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser` 151 | - `@stylistic/eslint-plugin-js`, `@stylistic/eslint-plugin-ts`, `@stylistic/eslint-plugin-migrate`, `@stylistic/eslint-plugin`, `@stylistic/eslint-plugin-jsx`, `@stylistic/eslint-plugin-plus` 152 | - `playwright`, `@playwright/test` 153 | 154 |
155 | 156 | #### `non-existant-packages` ⚠️ 157 | 158 | All paths defined in the workspace (the root `package.json`' `workspaces` field or `pnpm-workspace.yaml`) should match at least one package. 159 | 160 | #### `packages-without-package-json` ⚠️ 161 | 162 | All packages matching the workspace (the root `package.json`' `workspaces` field or `pnpm-workspace.yaml`) should have a `package.json` file. 163 | 164 | #### `root-package-dependencies` ⚠️ 165 | 166 | The root `package.json` is private, so making a distinction between `dependencies` and `devDependencies` is useless - only use `devDependencies`. 167 | 168 | #### `root-package-manager-field` ❌ 169 | 170 | The root `package.json` should specify the package manager and version to use. Useful for tools like corepack. 171 | 172 | #### `root-package-private-field` ❌ 173 | 174 | The root `package.json` should be private to prevent accidentaly publishing it to a registry. 175 | 176 | #### `types-in-dependencies` ❌ 177 | 178 | Private packages shouldn't have `@types/*` in `dependencies`, since they don't need it at runtime. Move them to `devDependencies`. 179 | 180 | #### `unordered-dependencies` ❌ 181 | 182 | Dependencies should be ordered alphabetically to prevent complex diffs when installing a new dependency via a package manager. 183 | 184 | ## Credits 185 | 186 | - [dedubcheck](https://github.com/innovatrics/dedubcheck) that given me the idea for Sherif 187 | - [Manypkg](https://github.com/Thinkmill/manypkg) for some of their rules 188 | - [This article](https://blog.orhun.dev/packaging-rust-for-npm/) for the Rust releases on NPM 189 | 190 | ## Sponsors 191 | 192 | ![Sponsors](https://github.com/QuiiBz/dotfiles/blob/main/sponsors.png?raw=true) 193 | 194 | ## License 195 | 196 | [MIT](./LICENSE) 197 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Sherif' 2 | description: 'Setup and run Sherif, an opinionated, zero-config linter for JavaScript monorepos' 3 | 4 | inputs: 5 | version: 6 | description: 'The Sherif version to use (e.g., v1.5.0)' 7 | required: false 8 | default: 'latest' 9 | github-token: 10 | description: 'GitHub token for API requests' 11 | required: false 12 | default: ${{ github.token }} 13 | args: 14 | description: 'Additional arguments to pass to Sherif' 15 | required: false 16 | default: '' 17 | 18 | runs: 19 | using: 'node20' 20 | main: 'action/index.js' 21 | 22 | branding: 23 | icon: 'shield' 24 | color: 'orange' 25 | -------------------------------------------------------------------------------- /action/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as tc from '@actions/tool-cache'; 3 | import * as github from '@actions/github'; 4 | import * as exec from '@actions/exec'; 5 | import * as os from 'os'; 6 | import * as path from 'path'; 7 | import * as fsp from 'fs/promises'; 8 | 9 | async function run(): Promise { 10 | try { 11 | // Get inputs 12 | const version = core.getInput('version'); 13 | const token = core.getInput('github-token'); 14 | let additionalArgs = core.getInput('args'); 15 | 16 | // Initialize octokit 17 | const octokit = github.getOctokit(token); 18 | 19 | // Determine release to download 20 | let releaseTag = version; 21 | if (version === 'latest') { 22 | const latestRelease = await octokit.rest.repos.getLatestRelease({ 23 | owner: 'quiibz', 24 | repo: 'sherif' 25 | }); 26 | releaseTag = latestRelease.data.tag_name; 27 | } 28 | 29 | // Get platform and architecture specific details 30 | const platform = os.platform(); 31 | const arch = os.arch(); 32 | 33 | // Map platform and architecture to release asset names 34 | const platformTargets: Record> = { 35 | 'darwin': { 36 | 'arm64': 'aarch64-apple-darwin', 37 | 'x64': 'x86_64-apple-darwin' 38 | }, 39 | 'win32': { 40 | 'arm64': 'aarch64-pc-windows-msvc', 41 | 'x64': 'x86_64-pc-windows-msvc' 42 | }, 43 | 'linux': { 44 | 'arm64': 'aarch64-unknown-linux-gnu', 45 | 'x64': 'x86_64-unknown-linux-gnu' 46 | } 47 | }; 48 | 49 | const platformTarget = platformTargets[platform]?.[arch]; 50 | if (!platformTarget) { 51 | throw new Error(`Unsupported platform (${platform}) or architecture (${arch})`); 52 | } 53 | 54 | // Construct asset name 55 | const assetName = `sherif-${platformTarget}.zip`; 56 | 57 | // Get release assets 58 | const release = await octokit.rest.repos.getReleaseByTag({ 59 | owner: 'quiibz', 60 | repo: 'sherif', 61 | tag: releaseTag 62 | }); 63 | 64 | const asset = release.data.assets.find(a => a.name === assetName); 65 | if (!asset) { 66 | throw new Error(`Could not find asset ${assetName} in release ${releaseTag}`); 67 | } 68 | 69 | // Download the zip file 70 | core.info(`Downloading Sherif ${releaseTag} for ${platformTarget}`); 71 | const downloadPath = await tc.downloadTool(asset.browser_download_url); 72 | 73 | // Extract the zip file 74 | core.info('Extracting Sherif binary...'); 75 | const extractedPath = await tc.extractZip(downloadPath); 76 | 77 | // Determine binary name based on platform 78 | const binaryName = platform === 'win32' ? 'sherif.exe' : 'sherif'; 79 | const binaryPath = path.join(extractedPath, binaryName); 80 | 81 | // Make binary executable on Unix systems 82 | if (platform !== 'win32') { 83 | await fsp.chmod(binaryPath, '777'); 84 | } 85 | 86 | // Add to PATH 87 | core.addPath(extractedPath); 88 | 89 | // Set output 90 | core.setOutput('sherif-path', binaryPath); 91 | core.info('Sherif has been installed successfully'); 92 | 93 | // Prepare arguments 94 | if (!additionalArgs) { 95 | additionalArgs = (await getArgsFromPackageJson()) || ''; 96 | } 97 | const args = additionalArgs.split(' ').filter(arg => arg !== ''); 98 | 99 | // Configure output options to preserve colors 100 | const options: exec.ExecOptions = { 101 | ignoreReturnCode: true, // We'll handle the return code ourselves 102 | env: { 103 | ...process.env, 104 | FORCE_COLOR: '3' // Force color output 105 | } 106 | }; 107 | 108 | // Execute Sherif 109 | const exitCode = await exec.exec(binaryPath, args, options); 110 | 111 | // Handle exit code 112 | if (exitCode !== 0) { 113 | throw new Error(`Sherif execution failed with exit code ${exitCode}`); 114 | } 115 | 116 | } catch (error) { 117 | if (error instanceof Error) { 118 | core.setFailed(error.message); 119 | } else { 120 | core.setFailed('An unexpected error occurred'); 121 | } 122 | } 123 | } 124 | 125 | async function getArgsFromPackageJson() { 126 | try { 127 | const packageJsonFile = await fsp.readFile( 128 | path.resolve(process.cwd(), 'package.json') 129 | ); 130 | const packageJson = JSON.parse(packageJsonFile.toString()); 131 | 132 | // Extract args from the `sherif` script in package.json, starting after 133 | // `sherif ` and ending before the next `&&` or end of line 134 | const regexResult = /sherif\s([^&&]*)/g.exec( 135 | packageJson.scripts.sherif 136 | ); 137 | if (regexResult && regexResult.length > 1) { 138 | const args = regexResult[1]; 139 | core.info(`Using the arguments "${args}" from the root package.json`); 140 | return args; 141 | } 142 | } catch { 143 | core.info('Failed to extract args from package.json'); 144 | } 145 | } 146 | 147 | run(); 148 | -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/assets/cover.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/assets/logo.png -------------------------------------------------------------------------------- /fixtures/basic/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "workspaces": [ 4 | "packages/*", 5 | "docs", 6 | "examples/*", 7 | "website" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/basic/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/basic/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/dependencies-nested-star/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependencies-nested-star", 3 | "workspaces": [ 4 | "packages/**/*", 5 | "packages/docs" 6 | ], 7 | "private": true, 8 | "packageManager": "pnpm@1.2.3", 9 | "devDependencies": { 10 | "eslint": "1.2.3", 11 | "prettier": "1.2.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/dependencies-nested-star/packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "dependencies": { 4 | "eslint": "7.8.9", 5 | "next": "1.2.3", 6 | "react": "*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/dependencies-nested-star/packages/other/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc", 3 | "dependencies": { 4 | "next": "4.5.6", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/dependencies-nested-star/packages/other/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def", 3 | "dependencies": { 4 | "next": "1.2.3", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/dependencies-star/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "dependencies": { 4 | "eslint": "7.8.9", 5 | "next": "1.2.3", 6 | "react": "*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/dependencies-star/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependencies-star", 3 | "workspaces": [ 4 | "packages/*", 5 | "docs" 6 | ], 7 | "private": true, 8 | "packageManager": "pnpm@1.2.3", 9 | "devDependencies": { 10 | "eslint": "1.2.3", 11 | "prettier": "1.2.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/dependencies-star/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc", 3 | "dependencies": { 4 | "next": "4.5.6", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/dependencies-star/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def", 3 | "dependencies": { 4 | "next": "1.2.3", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/dependencies/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "dependencies": { 4 | "@eslint/js": "7.8.9", 5 | "eslint": "7.8.9", 6 | "next": "1.2.3", 7 | "react": "4.5.6" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependencies", 3 | "workspaces": [ 4 | "packages/*", 5 | "docs" 6 | ], 7 | "private": true, 8 | "packageManager": "pnpm@1.2.3", 9 | "devDependencies": { 10 | "eslint": "1.2.3", 11 | "prettier": "1.2.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/dependencies/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc", 3 | "dependencies": { 4 | "@eslint/js": "9.8.7", 5 | "next": "4.5.6", 6 | "react": "1.2.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/dependencies/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def", 3 | "dependencies": { 4 | "next": "1.2.3", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/empty/none: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/fixtures/empty/none -------------------------------------------------------------------------------- /fixtures/ignore-paths/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ignore-paths", 3 | "private": true, 4 | "packageManager": "pnpm@1.2.3" 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/a/b/d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/a/b/e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/a/c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/packages/ghi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghi" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/ignore-paths/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'docs' 4 | - '!packages/abc' 5 | - '!packages/d*' 6 | - '!packages/a/*' 7 | - 'packages/a/b/*' 8 | -------------------------------------------------------------------------------- /fixtures/install/apps/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/install/apps/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/install/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "install", 8 | "workspaces": [ 9 | "apps/*" 10 | ] 11 | }, 12 | "apps/abc": {}, 13 | "apps/def": {}, 14 | "node_modules/abc": { 15 | "resolved": "apps/abc", 16 | "link": true 17 | }, 18 | "node_modules/def": { 19 | "resolved": "apps/def", 20 | "link": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/install/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install", 3 | "workspaces": [ 4 | "apps/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/no-workspace-pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-workspace-pnpm" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm-glob/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | -------------------------------------------------------------------------------- /fixtures/pnpm-glob/@ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm-glob/@web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm-glob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pnpm-glob", 3 | "private": true, 4 | "packageManager": "pnpm@1.2.3" 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/pnpm-glob/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '@*' 3 | -------------------------------------------------------------------------------- /fixtures/pnpm/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pnpm" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pnpm/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'docs' 4 | - 'examples/*' 5 | - 'website' 6 | -------------------------------------------------------------------------------- /fixtures/root-issues-fixed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root-issues-fixed", 3 | "workspaces": [], 4 | "private": true, 5 | "packageManager": "pnpm@1.2.3", 6 | "devDependencies": { 7 | "eslint": "1.2.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/root-issues/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root-issues", 3 | "workspaces": [], 4 | "dependencies": {}, 5 | "devDependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/unordered/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "devDependencies": { 4 | "x": "1.0.0", 5 | "z": "1.0.0", 6 | "y": "1.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/unordered/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unordered", 3 | "private": true, 4 | "packageManager": "pnpm@7.0.0", 5 | "workspaces": [ 6 | "docs" 7 | ], 8 | "devDependencies": { 9 | "b": "1.0.0", 10 | "a": "1.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/unsync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unsync", 3 | "private": true, 4 | "packageManager": "pnpm@7.0.0", 5 | "workspaces": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/unsync/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc", 3 | "dependencies": { 4 | "react": "2.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/unsync/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def", 3 | "dependencies": { 4 | "react": "1.0.0", 5 | "turbo": "2.0.0", 6 | "turbo-ignore": "3.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/without-package-json/docs/none: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/fixtures/without-package-json/docs/none -------------------------------------------------------------------------------- /fixtures/without-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "without-package-json", 3 | "workspaces": [ 4 | "packages/*", 5 | "docs" 6 | ], 7 | "private": true, 8 | "packageManager": "pnpm@1.2.3", 9 | "devDependencies": { 10 | "eslint": "1.2.3", 11 | "prettier": "1.2.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/without-package-json/packages/.npm/none: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/fixtures/without-package-json/packages/.npm/none -------------------------------------------------------------------------------- /fixtures/without-package-json/packages/abc/none: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiiBz/sherif/c7de874aed596da5701b9470e5487cd34589b5e6/fixtures/without-package-json/packages/abc/none -------------------------------------------------------------------------------- /fixtures/without-package-json/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def", 3 | "dependencies": { 4 | "next": "1.2.3", 5 | "react": "1.2.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/yarn-nohoist/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/yarn-nohoist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn-nohoist", 3 | "workspaces": { 4 | "packages": [ 5 | "packages/*", 6 | "docs", 7 | "examples/*", 8 | "website" 9 | ], 10 | "nohoist": [] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/yarn-nohoist/packages/abc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abc" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/yarn-nohoist/packages/def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "def" 3 | } 4 | -------------------------------------------------------------------------------- /npm/app/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawnSync } = require('child_process') 4 | 5 | /** 6 | * Returns the executable path which is located inside `node_modules` 7 | * The naming convention is app-${os}-${arch} 8 | * If the platform is `win32` or `cygwin`, executable will include a `.exe` extension. 9 | * @see https://nodejs.org/api/os.html#osarch 10 | * @see https://nodejs.org/api/os.html#osplatform 11 | * @example "x/xx/node_modules/app-darwin-arm64" 12 | */ 13 | function getExePath() { 14 | const arch = process.arch 15 | let os = process.platform 16 | let extension = "" 17 | if (['win32', 'cygwin'].includes(process.platform)) { 18 | os = 'windows' 19 | extension = '.exe' 20 | } 21 | 22 | try { 23 | // Since the binary will be located inside `node_modules`, we can simply call `require.resolve` 24 | return require.resolve(`sherif-${os}-${arch}/bin/sherif${extension}`) 25 | } catch (e) { 26 | throw new Error( 27 | `Couldn't find application binary inside node_modules for ${os}-${arch}` 28 | ) 29 | } 30 | } 31 | 32 | /** 33 | * Runs the application with args using nodejs spawn 34 | */ 35 | function run() { 36 | const args = process.argv.slice(2) 37 | const processResult = spawnSync(getExePath(), args, { stdio: 'inherit' }) 38 | process.exit(processResult.status ?? 0) 39 | } 40 | 41 | run() 42 | -------------------------------------------------------------------------------- /npm/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherif", 3 | "version": "1.5.0", 4 | "description": "Opinionated, zero-config linter for JavaScript monorepos", 5 | "bin": { 6 | "sherif": "./index.js" 7 | }, 8 | "keywords": [ 9 | "cli", 10 | "javascript", 11 | "monorepo", 12 | "linter" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/QuiiBz/sherif.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/QuiiBz/sherif/issues" 20 | }, 21 | "homepage": "https://github.com/QuiiBz/sherif#readme", 22 | "license": "MIT", 23 | "optionalDependencies": { 24 | "sherif-linux-x64": "1.5.0", 25 | "sherif-linux-arm64": "1.5.0", 26 | "sherif-darwin-x64": "1.5.0", 27 | "sherif-darwin-arm64": "1.5.0", 28 | "sherif-windows-x64": "1.5.0", 29 | "sherif-windows-arm64": "1.5.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /npm/package.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${node_pkg}", 3 | "version": "${node_version}", 4 | "description": "Opinionated, zero-config linter for JavaScript monorepos", 5 | "keywords": [ 6 | "cli", 7 | "javascript", 8 | "monorepo", 9 | "linter" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/QuiiBz/sherif.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/QuiiBz/sherif/issues" 17 | }, 18 | "homepage": "https://github.com/QuiiBz/sherif#readme", 19 | "license": "MIT", 20 | "os": ["${node_os}"], 21 | "cpu": ["${node_arch}"] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sherif", 3 | "version": "1.5.0", 4 | "description": "Opinionated, zero-config linter for JavaScript monorepos", 5 | "keywords": [ 6 | "cli", 7 | "javascript", 8 | "monorepo", 9 | "linter" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/QuiiBz/sherif.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/QuiiBz/sherif/issues" 17 | }, 18 | "homepage": "https://github.com/QuiiBz/sherif#readme", 19 | "license": "MIT", 20 | "scripts": { 21 | "action": "ncc build action/index.ts -o action" 22 | }, 23 | "dependencies": { 24 | "@actions/core": "^1.10.1", 25 | "@actions/exec": "^1.1.1", 26 | "@actions/github": "^6.0.0", 27 | "@actions/tool-cache": "^2.0.1" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^20.0.0", 31 | "@vercel/ncc": "^0.38.1", 32 | "typescript": "^5.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@actions/core': 9 | specifier: ^1.10.1 10 | version: 1.11.1 11 | '@actions/exec': 12 | specifier: ^1.1.1 13 | version: 1.1.1 14 | '@actions/github': 15 | specifier: ^6.0.0 16 | version: 6.0.0 17 | '@actions/tool-cache': 18 | specifier: ^2.0.1 19 | version: 2.0.1 20 | 21 | devDependencies: 22 | '@types/node': 23 | specifier: ^20.0.0 24 | version: 20.17.6 25 | '@vercel/ncc': 26 | specifier: ^0.38.1 27 | version: 0.38.3 28 | typescript: 29 | specifier: ^5.0.0 30 | version: 5.6.3 31 | 32 | packages: 33 | 34 | /@actions/core@1.11.1: 35 | resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==} 36 | dependencies: 37 | '@actions/exec': 1.1.1 38 | '@actions/http-client': 2.2.3 39 | dev: false 40 | 41 | /@actions/exec@1.1.1: 42 | resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==} 43 | dependencies: 44 | '@actions/io': 1.1.3 45 | dev: false 46 | 47 | /@actions/github@6.0.0: 48 | resolution: {integrity: sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==} 49 | dependencies: 50 | '@actions/http-client': 2.2.3 51 | '@octokit/core': 5.2.0 52 | '@octokit/plugin-paginate-rest': 9.2.1(@octokit/core@5.2.0) 53 | '@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.0) 54 | dev: false 55 | 56 | /@actions/http-client@2.2.3: 57 | resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} 58 | dependencies: 59 | tunnel: 0.0.6 60 | undici: 5.28.4 61 | dev: false 62 | 63 | /@actions/io@1.1.3: 64 | resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==} 65 | dev: false 66 | 67 | /@actions/tool-cache@2.0.1: 68 | resolution: {integrity: sha512-iPU+mNwrbA8jodY8eyo/0S/QqCKDajiR8OxWTnSk/SnYg0sj8Hp4QcUEVC1YFpHWXtrfbQrE13Jz4k4HXJQKcA==} 69 | dependencies: 70 | '@actions/core': 1.11.1 71 | '@actions/exec': 1.1.1 72 | '@actions/http-client': 2.2.3 73 | '@actions/io': 1.1.3 74 | semver: 6.3.1 75 | uuid: 3.4.0 76 | dev: false 77 | 78 | /@fastify/busboy@2.1.1: 79 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 80 | engines: {node: '>=14'} 81 | dev: false 82 | 83 | /@octokit/auth-token@4.0.0: 84 | resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} 85 | engines: {node: '>= 18'} 86 | dev: false 87 | 88 | /@octokit/core@5.2.0: 89 | resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==} 90 | engines: {node: '>= 18'} 91 | dependencies: 92 | '@octokit/auth-token': 4.0.0 93 | '@octokit/graphql': 7.1.0 94 | '@octokit/request': 8.4.0 95 | '@octokit/request-error': 5.1.0 96 | '@octokit/types': 13.6.1 97 | before-after-hook: 2.2.3 98 | universal-user-agent: 6.0.1 99 | dev: false 100 | 101 | /@octokit/endpoint@9.0.5: 102 | resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==} 103 | engines: {node: '>= 18'} 104 | dependencies: 105 | '@octokit/types': 13.6.1 106 | universal-user-agent: 6.0.1 107 | dev: false 108 | 109 | /@octokit/graphql@7.1.0: 110 | resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==} 111 | engines: {node: '>= 18'} 112 | dependencies: 113 | '@octokit/request': 8.4.0 114 | '@octokit/types': 13.6.1 115 | universal-user-agent: 6.0.1 116 | dev: false 117 | 118 | /@octokit/openapi-types@20.0.0: 119 | resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} 120 | dev: false 121 | 122 | /@octokit/openapi-types@22.2.0: 123 | resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} 124 | dev: false 125 | 126 | /@octokit/plugin-paginate-rest@9.2.1(@octokit/core@5.2.0): 127 | resolution: {integrity: sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==} 128 | engines: {node: '>= 18'} 129 | peerDependencies: 130 | '@octokit/core': '5' 131 | dependencies: 132 | '@octokit/core': 5.2.0 133 | '@octokit/types': 12.6.0 134 | dev: false 135 | 136 | /@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.0): 137 | resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==} 138 | engines: {node: '>= 18'} 139 | peerDependencies: 140 | '@octokit/core': '5' 141 | dependencies: 142 | '@octokit/core': 5.2.0 143 | '@octokit/types': 12.6.0 144 | dev: false 145 | 146 | /@octokit/request-error@5.1.0: 147 | resolution: {integrity: sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==} 148 | engines: {node: '>= 18'} 149 | dependencies: 150 | '@octokit/types': 13.6.1 151 | deprecation: 2.3.1 152 | once: 1.4.0 153 | dev: false 154 | 155 | /@octokit/request@8.4.0: 156 | resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==} 157 | engines: {node: '>= 18'} 158 | dependencies: 159 | '@octokit/endpoint': 9.0.5 160 | '@octokit/request-error': 5.1.0 161 | '@octokit/types': 13.6.1 162 | universal-user-agent: 6.0.1 163 | dev: false 164 | 165 | /@octokit/types@12.6.0: 166 | resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} 167 | dependencies: 168 | '@octokit/openapi-types': 20.0.0 169 | dev: false 170 | 171 | /@octokit/types@13.6.1: 172 | resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} 173 | dependencies: 174 | '@octokit/openapi-types': 22.2.0 175 | dev: false 176 | 177 | /@types/node@20.17.6: 178 | resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} 179 | dependencies: 180 | undici-types: 6.19.8 181 | dev: true 182 | 183 | /@vercel/ncc@0.38.3: 184 | resolution: {integrity: sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==} 185 | hasBin: true 186 | dev: true 187 | 188 | /before-after-hook@2.2.3: 189 | resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} 190 | dev: false 191 | 192 | /deprecation@2.3.1: 193 | resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} 194 | dev: false 195 | 196 | /once@1.4.0: 197 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 198 | dependencies: 199 | wrappy: 1.0.2 200 | dev: false 201 | 202 | /semver@6.3.1: 203 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 204 | hasBin: true 205 | dev: false 206 | 207 | /tunnel@0.0.6: 208 | resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} 209 | engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} 210 | dev: false 211 | 212 | /typescript@5.6.3: 213 | resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} 214 | engines: {node: '>=14.17'} 215 | hasBin: true 216 | dev: true 217 | 218 | /undici-types@6.19.8: 219 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 220 | dev: true 221 | 222 | /undici@5.28.4: 223 | resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} 224 | engines: {node: '>=14.0'} 225 | dependencies: 226 | '@fastify/busboy': 2.1.1 227 | dev: false 228 | 229 | /universal-user-agent@6.0.1: 230 | resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} 231 | dev: false 232 | 233 | /uuid@3.4.0: 234 | resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} 235 | deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. 236 | hasBin: true 237 | dev: false 238 | 239 | /wrappy@1.0.2: 240 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 241 | dev: false 242 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug, Parser)] 5 | pub struct Args { 6 | /// Path to the monorepo root. 7 | #[arg(default_value = ".")] 8 | pub path: PathBuf, 9 | 10 | /// Fix the issues automatically, if possible. 11 | #[arg(long, short)] 12 | pub fix: bool, 13 | 14 | /// Don't run your package manager's install command when autofixing. 15 | #[arg(long)] 16 | pub no_install: bool, 17 | 18 | /// Ignore the `multiple-dependency-versions` rule for the given dependency name and/or version. 19 | #[arg(long, short)] 20 | pub ignore_dependency: Vec, 21 | 22 | /// Ignore rules for the given package name or path. 23 | #[arg(long, short = 'p')] 24 | pub ignore_package: Vec, 25 | 26 | /// Ignore the given rule. 27 | #[arg(long, short = 'r')] 28 | pub ignore_rule: Vec, 29 | } 30 | -------------------------------------------------------------------------------- /src/collect.rs: -------------------------------------------------------------------------------- 1 | use crate::args::Args; 2 | use crate::packages::root::RootPackage; 3 | use crate::packages::semversion::SemVersion; 4 | use crate::packages::{Package, PackagesList}; 5 | use crate::printer::print_error; 6 | use crate::rules::multiple_dependency_versions::MultipleDependencyVersionsIssue; 7 | use crate::rules::non_existant_packages::NonExistantPackagesIssue; 8 | use crate::rules::packages_without_package_json::PackagesWithoutPackageJsonIssue; 9 | use crate::rules::types_in_dependencies::TypesInDependenciesIssue; 10 | use crate::rules::unsync_similar_dependencies::{ 11 | SimilarDependency, UnsyncSimilarDependenciesIssue, 12 | }; 13 | use crate::rules::{BoxIssue, IssuesList, PackageType}; 14 | use anyhow::{anyhow, Result}; 15 | use indexmap::IndexMap; 16 | use serde::{Deserialize, Serialize}; 17 | use std::fs::{self}; 18 | use std::path::PathBuf; 19 | 20 | const PNPM_WORKSPACE: &str = "pnpm-workspace.yaml"; 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct PnpmWorkspace { 24 | pub packages: Vec, 25 | } 26 | 27 | pub fn collect_packages(args: &Args) -> Result { 28 | let root_package = RootPackage::new(&args.path)?; 29 | let mut packages = Vec::new(); 30 | let mut packages_list = root_package.get_workspaces(); 31 | let mut excluded_paths = Vec::new(); 32 | let mut non_existant_paths = Vec::new(); 33 | let mut is_pnpm_workspace = false; 34 | 35 | if packages_list.is_none() { 36 | let pnpm_workspace = args.path.join(PNPM_WORKSPACE); 37 | 38 | if !pnpm_workspace.is_file() { 39 | return Err(anyhow!( 40 | "No `workspaces` field in the root `package.json`, or `pnpm-workspace.yaml` file not found in {:?}", 41 | args.path 42 | )); 43 | } 44 | 45 | let root_package = fs::read_to_string(pnpm_workspace)?; 46 | let workspace: PnpmWorkspace = serde_yaml::from_str(&root_package)?; 47 | 48 | packages_list = Some(workspace.packages); 49 | is_pnpm_workspace = true; 50 | } 51 | 52 | let mut packages_issues: Vec = Vec::new(); 53 | 54 | let mut add_package = |packages_issues: &mut Vec, path: PathBuf| { 55 | // Ignore hidden directories, e.g. `.npm`, `.react-email` 56 | if let Some(stem) = path.file_stem() { 57 | if let Some(stem) = stem.to_str() { 58 | if stem.starts_with('.') { 59 | return; 60 | } 61 | } 62 | } 63 | 64 | match Package::new(path.clone()) { 65 | Ok(package) => packages.push(package), 66 | Err(error) => { 67 | if error.to_string().contains("not found") { 68 | packages_issues.push(PackagesWithoutPackageJsonIssue::new( 69 | path.to_string_lossy().to_string(), 70 | )); 71 | } else { 72 | print_error("Failed to collect package", &error.to_string()); 73 | std::process::exit(1); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | if let Some(packages) = &packages_list { 80 | let packages = packages 81 | .iter() 82 | .filter(|package| { 83 | if package.starts_with('!') { 84 | if package.ends_with('*') { 85 | let directory = package 86 | .trim_start_matches('!') 87 | .trim_end_matches('*') 88 | .trim_end_matches('/'); 89 | let directory = args.path.join(directory); 90 | 91 | excluded_paths.push(directory.to_string_lossy().to_string()); 92 | } else { 93 | let directory = package.trim_start_matches('!'); 94 | let directory = args.path.join(directory); 95 | 96 | excluded_paths.push(directory.to_string_lossy().to_string()); 97 | } 98 | 99 | return false; 100 | } 101 | 102 | true 103 | }) 104 | .collect::>(); 105 | 106 | let mut expanded_packages = Vec::new(); 107 | 108 | for package in packages { 109 | if let Some((directory, subdirectory)) = package.split_once("/**/") { 110 | let directory = args.path.join(directory); 111 | 112 | match directory.read_dir() { 113 | Ok(expanded_folders) => { 114 | for expanded_folder in expanded_folders.flatten() { 115 | let expanded_folder = expanded_folder.path(); 116 | 117 | if expanded_folder.is_dir() { 118 | let path = expanded_folder 119 | .to_string_lossy() 120 | .to_string() 121 | .replace(&(args.path.to_string_lossy().to_string() + "/"), "") 122 | + "/" 123 | + subdirectory; 124 | 125 | expanded_packages.push(path); 126 | } 127 | } 128 | } 129 | Err(_) => { 130 | non_existant_paths.push(package.to_string()); 131 | continue; 132 | } 133 | } 134 | } else { 135 | expanded_packages.push(package.to_string()); 136 | } 137 | } 138 | 139 | for package in &expanded_packages { 140 | if package.ends_with('*') { 141 | let directory_match = package.trim_end_matches('*'); 142 | 143 | let packages = match directory_match.ends_with('/') { 144 | true => { 145 | let directory = directory_match.trim_end_matches('/'); 146 | let directory = args.path.join(directory); 147 | 148 | match directory.read_dir() { 149 | Ok(packages) => packages.into_iter().collect::, _>>()?, 150 | Err(_) => { 151 | non_existant_paths.push(package.to_string()); 152 | continue; 153 | } 154 | } 155 | } 156 | false => { 157 | let directory = args.path.join(directory_match); 158 | let directory = directory.parent().unwrap().to_path_buf(); 159 | 160 | match directory.read_dir() { 161 | Ok(packages) => packages 162 | .into_iter() 163 | .filter(|package| { 164 | if let Ok(package) = package { 165 | return package.file_type().unwrap().is_dir() 166 | && package 167 | .file_name() 168 | .to_string_lossy() 169 | .starts_with(directory_match); 170 | } 171 | 172 | true 173 | }) 174 | .collect::, _>>()?, 175 | Err(_) => { 176 | non_existant_paths.push(package.to_string()); 177 | continue; 178 | } 179 | } 180 | } 181 | }; 182 | 183 | for package in packages { 184 | if package.file_type()?.is_dir() { 185 | let path = package.path(); 186 | let real_path = path.to_string_lossy().to_string(); 187 | let mut is_excluded = false; 188 | 189 | for excluded_path in &excluded_paths { 190 | if real_path.starts_with(excluded_path) 191 | && !real_path.replace(excluded_path, "").contains('/') 192 | { 193 | is_excluded = true; 194 | break; 195 | } 196 | } 197 | 198 | if !is_excluded { 199 | add_package(&mut packages_issues, path); 200 | } 201 | } 202 | } 203 | } else { 204 | let path = args.path.join(package); 205 | 206 | match path.is_dir() { 207 | true => add_package(&mut packages_issues, path), 208 | false => non_existant_paths.push(package.to_string()), 209 | } 210 | } 211 | } 212 | 213 | if !non_existant_paths.is_empty() { 214 | packages_issues.push(NonExistantPackagesIssue::new( 215 | is_pnpm_workspace, 216 | packages_list.unwrap(), 217 | non_existant_paths, 218 | )); 219 | } 220 | } 221 | 222 | Ok(PackagesList { 223 | root_package, 224 | packages, 225 | packages_issues, 226 | }) 227 | } 228 | 229 | pub fn collect_issues(args: &Args, packages_list: PackagesList) -> IssuesList<'_> { 230 | let mut issues = IssuesList::new(&args.ignore_rule); 231 | 232 | let PackagesList { 233 | root_package, 234 | packages, 235 | packages_issues, 236 | } = packages_list; 237 | 238 | for package_issue in packages_issues { 239 | issues.add_raw(PackageType::None, package_issue); 240 | } 241 | 242 | issues.add(PackageType::Root, root_package.check_private()); 243 | issues.add(PackageType::Root, root_package.check_package_manager()); 244 | issues.add(PackageType::Root, root_package.check_dependencies()); 245 | issues.add(PackageType::Root, root_package.check_dev_dependencies()); 246 | issues.add(PackageType::Root, root_package.check_peer_dependencies()); 247 | issues.add( 248 | PackageType::Root, 249 | root_package.check_optional_dependencies(), 250 | ); 251 | 252 | let mut all_dependencies = IndexMap::new(); 253 | let mut joined_dependencies = IndexMap::new(); 254 | let mut similar_dependencies_by_package = IndexMap::new(); 255 | 256 | if let Some(dependencies) = root_package.get_dependencies() { 257 | joined_dependencies.extend(dependencies); 258 | } 259 | 260 | if let Some(dev_dependencies) = root_package.get_dev_dependencies() { 261 | joined_dependencies.extend(dev_dependencies); 262 | } 263 | 264 | for (name, version) in joined_dependencies { 265 | if version.is_valid() { 266 | all_dependencies 267 | .entry(name) 268 | .or_insert_with(IndexMap::new) 269 | .insert(root_package.get_path(), version); 270 | } 271 | } 272 | 273 | for package in packages { 274 | if package.is_ignored(&args.ignore_package) { 275 | continue; 276 | } 277 | 278 | let package_type = PackageType::Package(package.get_path()); 279 | 280 | issues.add(package_type.clone(), package.check_dependencies()); 281 | issues.add(package_type.clone(), package.check_dev_dependencies()); 282 | issues.add(package_type.clone(), package.check_peer_dependencies()); 283 | issues.add(package_type.clone(), package.check_optional_dependencies()); 284 | 285 | let mut joined_dependencies = IndexMap::new(); 286 | 287 | if let Some(dependencies) = package.get_dependencies() { 288 | if package.is_private() { 289 | let types_in_dependencies = dependencies 290 | .iter() 291 | .filter(|(name, _)| name.starts_with("@types/")) 292 | .map(|(name, _)| name.to_string()) 293 | .collect::>(); 294 | 295 | if !types_in_dependencies.is_empty() { 296 | issues.add_raw( 297 | package_type.clone(), 298 | TypesInDependenciesIssue::new(types_in_dependencies), 299 | ); 300 | } 301 | } 302 | 303 | joined_dependencies.extend(dependencies); 304 | } 305 | 306 | if let Some(dev_dependencies) = package.get_dev_dependencies() { 307 | joined_dependencies.extend(dev_dependencies); 308 | } 309 | 310 | for (name, version) in joined_dependencies { 311 | if version.is_valid() { 312 | all_dependencies 313 | .entry(name) 314 | .or_insert_with(IndexMap::new) 315 | .insert(package.get_path(), version); 316 | } 317 | } 318 | } 319 | 320 | for (name, versions) in all_dependencies { 321 | if let Ok(similar_dependency) = SimilarDependency::try_from(name.as_str()) { 322 | for (path, version) in versions.iter() { 323 | similar_dependencies_by_package 324 | .entry(path.clone()) 325 | .or_insert_with( 326 | IndexMap::>::new, 327 | ) 328 | .entry(similar_dependency.clone()) 329 | .or_insert_with(IndexMap::new) 330 | .insert(version.clone(), name.clone()); 331 | } 332 | } 333 | 334 | let mut filtered_versions = versions 335 | .iter() 336 | .filter(|(_, version)| { 337 | !args 338 | .ignore_dependency 339 | .contains(&format!("{}@{}", name, version)) 340 | }) 341 | .map(|(path, version)| (path.clone(), version.clone())) 342 | .collect::>(); 343 | 344 | if filtered_versions.len() > 1 345 | && !filtered_versions 346 | .values() 347 | .collect::>() 348 | .windows(2) 349 | .all(|window| window[0] == window[1]) 350 | && !args.ignore_dependency.contains(&name) 351 | && !args.ignore_dependency.iter().any(|dependency| { 352 | if dependency.ends_with('*') { 353 | if dependency.starts_with('*') { 354 | return name 355 | .contains(dependency.trim_start_matches('*').trim_end_matches('*')); 356 | } 357 | return name.starts_with(dependency.trim_end_matches('*')); 358 | } else if dependency.starts_with('*') { 359 | return name.ends_with(dependency.trim_start_matches('*')); 360 | } 361 | false 362 | }) 363 | { 364 | filtered_versions.sort_keys(); 365 | 366 | issues.add_raw( 367 | PackageType::None, 368 | MultipleDependencyVersionsIssue::new(name, filtered_versions), 369 | ); 370 | } 371 | } 372 | 373 | for (path, similar_dependencies) in similar_dependencies_by_package { 374 | for (similar_dependency, versions) in similar_dependencies { 375 | if versions.len() > 1 { 376 | issues.add_raw( 377 | PackageType::Package(path.clone()), 378 | UnsyncSimilarDependenciesIssue::new(similar_dependency, versions), 379 | ); 380 | } 381 | } 382 | } 383 | 384 | issues 385 | } 386 | 387 | #[cfg(test)] 388 | mod test { 389 | use super::*; 390 | use debugless_unwrap::DebuglessUnwrapErr; 391 | 392 | #[test] 393 | fn collect_packages_unknown_dir() { 394 | let args = Args { 395 | path: "unknown".into(), 396 | fix: false, 397 | no_install: true, 398 | ignore_rule: Vec::new(), 399 | ignore_package: Vec::new(), 400 | ignore_dependency: Vec::new(), 401 | }; 402 | 403 | let result = collect_packages(&args); 404 | 405 | assert!(result.is_err()); 406 | assert_eq!( 407 | result.debugless_unwrap_err().to_string(), 408 | "Path \"unknown\" is not a directory" 409 | ); 410 | } 411 | 412 | #[test] 413 | fn collect_packages_empty_dir() { 414 | let args = Args { 415 | path: "fixtures/empty".into(), 416 | fix: false, 417 | no_install: true, 418 | ignore_rule: Vec::new(), 419 | ignore_package: Vec::new(), 420 | ignore_dependency: Vec::new(), 421 | }; 422 | 423 | let result = collect_packages(&args); 424 | 425 | assert!(result.is_err()); 426 | assert_eq!( 427 | result.debugless_unwrap_err().to_string(), 428 | "`package.json` not found in \"fixtures/empty\"" 429 | ); 430 | } 431 | 432 | #[test] 433 | fn collect_packages_basic() { 434 | let args = Args { 435 | path: "fixtures/basic".into(), 436 | fix: false, 437 | no_install: true, 438 | ignore_rule: Vec::new(), 439 | ignore_package: Vec::new(), 440 | ignore_dependency: Vec::new(), 441 | }; 442 | 443 | let result = collect_packages(&args); 444 | 445 | assert!(result.is_ok()); 446 | let PackagesList { 447 | root_package, 448 | packages, 449 | packages_issues, 450 | } = result.unwrap(); 451 | 452 | assert_eq!(root_package.get_name(), "basic"); 453 | assert_eq!(packages.len(), 3); 454 | assert_eq!(packages_issues.len(), 1); 455 | assert!(packages_issues[0].name() == "non-existant-packages"); 456 | } 457 | 458 | #[test] 459 | fn collect_packages_pnpm() { 460 | let args = Args { 461 | path: "fixtures/pnpm".into(), 462 | fix: false, 463 | no_install: true, 464 | ignore_rule: Vec::new(), 465 | ignore_package: Vec::new(), 466 | ignore_dependency: Vec::new(), 467 | }; 468 | 469 | let result = collect_packages(&args); 470 | 471 | assert!(result.is_ok()); 472 | let PackagesList { 473 | root_package, 474 | packages, 475 | packages_issues, 476 | } = result.unwrap(); 477 | 478 | assert_eq!(root_package.get_name(), "pnpm"); 479 | assert_eq!(packages.len(), 3); 480 | assert_eq!(packages_issues.len(), 1); 481 | assert!(packages_issues[0].name() == "non-existant-packages"); 482 | } 483 | 484 | #[test] 485 | fn collect_packages_yarn_nohoist() { 486 | let args = Args { 487 | path: "fixtures/yarn-nohoist".into(), 488 | fix: false, 489 | no_install: true, 490 | ignore_rule: Vec::new(), 491 | ignore_package: Vec::new(), 492 | ignore_dependency: Vec::new(), 493 | }; 494 | 495 | let result = collect_packages(&args); 496 | 497 | assert!(result.is_ok()); 498 | let PackagesList { 499 | root_package, 500 | packages, 501 | packages_issues, 502 | } = result.unwrap(); 503 | 504 | assert_eq!(root_package.get_name(), "yarn-nohoist"); 505 | assert_eq!(packages.len(), 3); 506 | assert_eq!(packages_issues.len(), 1); 507 | assert!(packages_issues[0].name() == "non-existant-packages"); 508 | } 509 | 510 | #[test] 511 | fn collect_packages_no_workspace_pnpm() { 512 | let args = Args { 513 | path: "fixtures/no-workspace-pnpm".into(), 514 | fix: false, 515 | no_install: true, 516 | ignore_rule: Vec::new(), 517 | ignore_package: Vec::new(), 518 | ignore_dependency: Vec::new(), 519 | }; 520 | 521 | let result = collect_packages(&args); 522 | 523 | assert!(result.is_err()); 524 | assert_eq!( 525 | result.debugless_unwrap_err().to_string(), 526 | "No `workspaces` field in the root `package.json`, or `pnpm-workspace.yaml` file not found in \"fixtures/no-workspace-pnpm\"" 527 | ); 528 | } 529 | 530 | #[test] 531 | fn collect_packages_without_package_json() { 532 | let args = Args { 533 | path: "fixtures/without-package-json".into(), 534 | fix: false, 535 | no_install: true, 536 | ignore_rule: Vec::new(), 537 | ignore_package: Vec::new(), 538 | ignore_dependency: Vec::new(), 539 | }; 540 | 541 | let result = collect_packages(&args); 542 | 543 | assert!(result.is_ok()); 544 | let PackagesList { 545 | root_package, 546 | packages, 547 | packages_issues, 548 | } = result.unwrap(); 549 | 550 | assert_eq!(root_package.get_name(), "without-package-json"); 551 | assert_eq!(packages.len(), 1); 552 | assert_eq!(packages_issues.len(), 2); 553 | assert_eq!(packages_issues[0].name(), "packages-without-package-json"); 554 | assert_eq!(packages_issues[1].name(), "packages-without-package-json"); 555 | } 556 | 557 | #[test] 558 | fn collect_packages_ignore_paths() { 559 | let args = Args { 560 | path: "fixtures/ignore-paths".into(), 561 | fix: false, 562 | no_install: true, 563 | ignore_rule: Vec::new(), 564 | ignore_package: Vec::new(), 565 | ignore_dependency: Vec::new(), 566 | }; 567 | 568 | let result = collect_packages(&args); 569 | 570 | assert!(result.is_ok()); 571 | let PackagesList { 572 | root_package, 573 | packages, 574 | .. 575 | } = result.unwrap(); 576 | 577 | assert_eq!(root_package.get_name(), "ignore-paths"); 578 | assert_eq!(packages.len(), 4); 579 | 580 | let mut packages = packages 581 | .into_iter() 582 | .map(|package| package.get_name().clone().unwrap().to_string()) 583 | .collect::>(); 584 | packages.sort(); 585 | 586 | assert_eq!(packages[0], "d"); 587 | assert_eq!(packages[1], "docs"); 588 | assert_eq!(packages[2], "e"); 589 | assert_eq!(packages[3], "ghi"); 590 | } 591 | 592 | #[test] 593 | fn collect_root_issues() { 594 | let args = Args { 595 | path: "fixtures/root-issues".into(), 596 | fix: false, 597 | no_install: true, 598 | ignore_rule: Vec::new(), 599 | ignore_package: Vec::new(), 600 | ignore_dependency: Vec::new(), 601 | }; 602 | 603 | let packages_list = collect_packages(&args).unwrap(); 604 | assert_eq!(packages_list.root_package.get_name(), "root-issues"); 605 | 606 | let issues = collect_issues(&args, packages_list); 607 | assert_eq!(issues.total_len(), 4); 608 | 609 | let issues = issues.into_iter().collect::>(); 610 | assert_eq!( 611 | issues.get(&PackageType::Root).unwrap()[0].name(), 612 | "root-package-private-field" 613 | ); 614 | assert_eq!( 615 | issues.get(&PackageType::Root).unwrap()[1].name(), 616 | "root-package-manager-field" 617 | ); 618 | assert_eq!( 619 | issues.get(&PackageType::Root).unwrap()[2].name(), 620 | "root-package-dependencies" 621 | ); 622 | assert_eq!( 623 | issues.get(&PackageType::Root).unwrap()[3].name(), 624 | "empty-dependencies" 625 | ); 626 | } 627 | 628 | #[test] 629 | fn collect_root_issues_fixed() { 630 | let args = Args { 631 | fix: false, 632 | no_install: true, 633 | path: "fixtures/root-issues-fixed".into(), 634 | ignore_rule: Vec::new(), 635 | ignore_package: Vec::new(), 636 | ignore_dependency: Vec::new(), 637 | }; 638 | 639 | let packages_list = collect_packages(&args).unwrap(); 640 | assert_eq!(packages_list.root_package.get_name(), "root-issues-fixed"); 641 | 642 | let issues = collect_issues(&args, packages_list); 643 | assert_eq!(issues.total_len(), 0); 644 | } 645 | 646 | #[test] 647 | fn collect_dependencies() { 648 | let args = Args { 649 | path: "fixtures/dependencies".into(), 650 | fix: false, 651 | no_install: true, 652 | ignore_rule: Vec::new(), 653 | ignore_package: Vec::new(), 654 | ignore_dependency: Vec::new(), 655 | }; 656 | 657 | let packages_list = collect_packages(&args).unwrap(); 658 | assert_eq!(packages_list.root_package.get_name(), "dependencies"); 659 | 660 | let issues = collect_issues(&args, packages_list); 661 | assert_eq!(issues.total_len(), 4); 662 | 663 | let issues = issues.into_iter().collect::>(); 664 | 665 | assert_eq!( 666 | issues.get(&PackageType::None).unwrap()[0].name(), 667 | "multiple-dependency-versions" 668 | ); 669 | assert_eq!( 670 | issues.get(&PackageType::None).unwrap()[1].name(), 671 | "multiple-dependency-versions" 672 | ); 673 | assert_eq!( 674 | issues.get(&PackageType::None).unwrap()[2].name(), 675 | "multiple-dependency-versions" 676 | ); 677 | assert_eq!( 678 | issues.get(&PackageType::None).unwrap()[3].name(), 679 | "multiple-dependency-versions" 680 | ); 681 | } 682 | 683 | #[test] 684 | fn collect_dependencies_allow() { 685 | let args = Args { 686 | path: "fixtures/dependencies".into(), 687 | fix: false, 688 | no_install: false, 689 | ignore_rule: Vec::new(), 690 | ignore_package: Vec::new(), 691 | ignore_dependency: vec!["next@4.5.6".to_string(), "*eslint*".to_string()], 692 | }; 693 | 694 | let packages_list = collect_packages(&args).unwrap(); 695 | assert_eq!(packages_list.root_package.get_name(), "dependencies"); 696 | 697 | let issues = collect_issues(&args, packages_list); 698 | assert_eq!(issues.total_len(), 1); 699 | 700 | let issues = issues.into_iter().collect::>(); 701 | 702 | assert_eq!( 703 | issues.get(&PackageType::None).unwrap()[0].name(), 704 | "multiple-dependency-versions" 705 | ); 706 | } 707 | 708 | #[test] 709 | fn collect_dependencies_without_star() { 710 | let args = Args { 711 | path: "fixtures/dependencies-star".into(), 712 | fix: false, 713 | no_install: true, 714 | ignore_rule: Vec::new(), 715 | ignore_package: Vec::new(), 716 | ignore_dependency: Vec::new(), 717 | }; 718 | 719 | let packages_list = collect_packages(&args).unwrap(); 720 | assert_eq!(packages_list.root_package.get_name(), "dependencies-star"); 721 | 722 | let issues = collect_issues(&args, packages_list); 723 | assert_eq!(issues.total_len(), 2); 724 | 725 | let issues = issues.into_iter().collect::>(); 726 | 727 | assert_eq!( 728 | issues.get(&PackageType::None).unwrap()[0].name(), 729 | "multiple-dependency-versions" 730 | ); 731 | assert_eq!( 732 | issues.get(&PackageType::None).unwrap()[1].name(), 733 | "multiple-dependency-versions" 734 | ); 735 | } 736 | 737 | #[test] 738 | fn collect_dependencies_nested_star() { 739 | let args = Args { 740 | path: "fixtures/dependencies-nested-star".into(), 741 | fix: false, 742 | no_install: false, 743 | ignore_rule: Vec::new(), 744 | ignore_package: Vec::new(), 745 | ignore_dependency: Vec::new(), 746 | }; 747 | 748 | let packages_list = collect_packages(&args).unwrap(); 749 | assert_eq!( 750 | packages_list.root_package.get_name(), 751 | "dependencies-nested-star" 752 | ); 753 | 754 | let issues = collect_issues(&args, packages_list); 755 | assert_eq!(issues.total_len(), 2); 756 | 757 | let issues = issues.into_iter().collect::>(); 758 | 759 | assert_eq!( 760 | issues.get(&PackageType::None).unwrap()[0].name(), 761 | "multiple-dependency-versions" 762 | ); 763 | assert_eq!( 764 | issues.get(&PackageType::None).unwrap()[1].name(), 765 | "multiple-dependency-versions" 766 | ); 767 | } 768 | 769 | #[test] 770 | fn collect_pnpm_glob() { 771 | let args = Args { 772 | path: "fixtures/pnpm-glob".into(), 773 | fix: false, 774 | no_install: true, 775 | ignore_rule: Vec::new(), 776 | ignore_package: Vec::new(), 777 | ignore_dependency: Vec::new(), 778 | }; 779 | 780 | let packages_list = collect_packages(&args).unwrap(); 781 | assert_eq!(packages_list.root_package.get_name(), "pnpm-glob"); 782 | assert_eq!(packages_list.packages.len(), 2); 783 | 784 | let issues = collect_issues(&args, packages_list); 785 | assert_eq!(issues.total_len(), 0); 786 | } 787 | 788 | #[test] 789 | fn collect_unordered_dependencies() { 790 | let args = Args { 791 | path: "fixtures/unordered".into(), 792 | fix: false, 793 | no_install: false, 794 | ignore_rule: Vec::new(), 795 | ignore_package: Vec::new(), 796 | ignore_dependency: Vec::new(), 797 | }; 798 | 799 | let packages_list = collect_packages(&args).unwrap(); 800 | assert_eq!(packages_list.root_package.get_name(), "unordered"); 801 | assert_eq!(packages_list.packages.len(), 1); 802 | 803 | let issues = collect_issues(&args, packages_list); 804 | assert_eq!(issues.total_len(), 2); 805 | 806 | let issues = issues.into_iter().collect::>(); 807 | 808 | assert_eq!( 809 | issues.get(&PackageType::Root).unwrap()[0].name(), 810 | "unordered-dependencies" 811 | ); 812 | assert_eq!( 813 | issues 814 | .get(&PackageType::Package("fixtures/unordered/docs".to_string())) 815 | .unwrap()[0] 816 | .name(), 817 | "unordered-dependencies" 818 | ); 819 | } 820 | 821 | #[test] 822 | fn collect_unsync_similar_dependencies() { 823 | let args = Args { 824 | path: "fixtures/unsync".into(), 825 | fix: false, 826 | no_install: false, 827 | ignore_rule: Vec::new(), 828 | ignore_package: Vec::new(), 829 | ignore_dependency: Vec::new(), 830 | }; 831 | 832 | let packages_list = collect_packages(&args).unwrap(); 833 | assert_eq!(packages_list.root_package.get_name(), "unsync"); 834 | assert_eq!(packages_list.packages.len(), 2); 835 | 836 | let issues = collect_issues(&args, packages_list); 837 | assert_eq!(issues.total_len(), 2); 838 | 839 | let issues = issues.into_iter().collect::>(); 840 | 841 | assert_eq!( 842 | issues 843 | .get(&PackageType::Package( 844 | "fixtures/unsync/packages/def".to_string() 845 | )) 846 | .unwrap()[0] 847 | .name(), 848 | "unsync-similar-dependencies" 849 | ); 850 | } 851 | } 852 | -------------------------------------------------------------------------------- /src/install.rs: -------------------------------------------------------------------------------- 1 | use crate::printer::get_render_config; 2 | use anyhow::{anyhow, Result}; 3 | use colored::Colorize; 4 | use inquire::Select; 5 | use std::{fmt::Display, fs, process::Command, process::Stdio}; 6 | 7 | const PACKAGE_MANAGERS: [&str; 4] = ["npm", "yarn", "pnpm", "bun"]; 8 | 9 | #[derive(Debug, PartialEq)] 10 | enum PackageManager { 11 | Npm, 12 | Yarn, 13 | Pnpm, 14 | Bun, 15 | } 16 | 17 | impl PackageManager { 18 | pub fn resolve() -> Result { 19 | if fs::metadata("package-lock.json").is_ok() { 20 | return Ok(PackageManager::Npm); 21 | } else if fs::metadata("bun.lockb").is_ok() || fs::metadata("bun.lock").is_ok() { 22 | return Ok(PackageManager::Bun); 23 | } else if fs::metadata("yarn.lock").is_ok() { 24 | return Ok(PackageManager::Yarn); 25 | } else if fs::metadata("pnpm-lock.yaml").is_ok() { 26 | return Ok(PackageManager::Pnpm); 27 | } 28 | 29 | let package_manager = 30 | Select::new("Select a package manager to use", PACKAGE_MANAGERS.to_vec()) 31 | .with_render_config(get_render_config()) 32 | .with_help_message("Enter to select") 33 | .prompt(); 34 | 35 | match package_manager { 36 | Ok("npm") => Ok(PackageManager::Npm), 37 | Ok("yarn") => Ok(PackageManager::Yarn), 38 | Ok("pnpm") => Ok(PackageManager::Pnpm), 39 | Ok("bun") => Ok(PackageManager::Bun), 40 | _ => Err(anyhow!("No package manager selected")), 41 | } 42 | } 43 | } 44 | 45 | impl Display for PackageManager { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | match self { 48 | PackageManager::Npm => write!(f, "npm"), 49 | PackageManager::Yarn => write!(f, "yarn"), 50 | PackageManager::Pnpm => write!(f, "pnpm"), 51 | PackageManager::Bun => write!(f, "bun"), 52 | } 53 | } 54 | } 55 | 56 | pub fn install() -> Result<()> { 57 | let package_manager = PackageManager::resolve()?; 58 | 59 | println!( 60 | " {}", 61 | format!("Note: running install command using {}...", package_manager).bright_black(), 62 | ); 63 | println!(); 64 | 65 | let mut command = Command::new(package_manager.to_string()) 66 | .arg("install") 67 | .stdout(Stdio::inherit()) 68 | .stderr(Stdio::inherit()) 69 | .spawn()?; 70 | 71 | let status = command.wait()?; 72 | if !status.success() { 73 | return Err(anyhow!("Install command failed")); 74 | } 75 | 76 | println!(); 77 | Ok(()) 78 | } 79 | 80 | #[cfg(test)] 81 | mod test { 82 | use crate::{args::Args, collect::collect_packages}; 83 | use serde_json::Value; 84 | use std::fs; 85 | 86 | #[test] 87 | fn test_detect_package_manager() { 88 | use super::*; 89 | use std::fs; 90 | 91 | fs::File::create("package-lock.json").unwrap(); 92 | assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Npm); 93 | fs::remove_file("package-lock.json").unwrap(); 94 | 95 | fs::File::create("bun.lockb").unwrap(); 96 | assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Bun); 97 | fs::remove_file("bun.lockb").unwrap(); 98 | 99 | fs::File::create("bun.lock").unwrap(); 100 | assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Bun); 101 | fs::remove_file("bun.lock").unwrap(); 102 | 103 | fs::File::create("yarn.lock").unwrap(); 104 | assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Yarn); 105 | fs::remove_file("yarn.lock").unwrap(); 106 | 107 | assert_eq!(PackageManager::resolve().unwrap(), PackageManager::Pnpm); 108 | } 109 | 110 | #[test] 111 | fn test_install_run() { 112 | let args = Args { 113 | path: "fixtures/install".into(), 114 | fix: false, 115 | no_install: false, 116 | ignore_rule: Vec::new(), 117 | ignore_package: Vec::new(), 118 | ignore_dependency: Vec::new(), 119 | }; 120 | 121 | let _ = collect_packages(&args); 122 | 123 | std::env::set_current_dir("fixtures/install").unwrap(); 124 | super::install().unwrap(); 125 | 126 | // Test if the previously empty package-lock.json now contains the "install" name to indicate that the install command was run 127 | let file = fs::File::open("package-lock.json"); 128 | let json: Result = serde_json::from_reader(file.unwrap()); 129 | assert_eq!(json.unwrap()["name"], "install"); 130 | 131 | std::env::set_current_dir("../../").unwrap(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use detect_indent::{detect_indent, Indent}; 3 | use detect_newline_style::LineEnding; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::ser::PrettyFormatter; 6 | 7 | pub fn deserialize<'a, T>(value: &'a str) -> Result<(T, Indent, LineEnding)> 8 | where 9 | T: Deserialize<'a>, 10 | { 11 | let json = serde_json::from_str::(value)?; 12 | let indent = detect_indent(value); 13 | let lineending = LineEnding::find_or_use_lf(value); 14 | 15 | Ok((json, indent, lineending)) 16 | } 17 | 18 | pub fn serialize(value: &T, indent: Indent, lineending: LineEnding) -> Result 19 | where 20 | T: Serialize, 21 | { 22 | let mut buf = Vec::new(); 23 | let formatter = PrettyFormatter::with_indent(indent.indent().as_bytes()); 24 | let mut serializer = serde_json::Serializer::with_formatter(&mut buf, formatter); 25 | 26 | value.serialize(&mut serializer)?; 27 | let mut json = String::from_utf8(buf)?; 28 | json += match lineending { 29 | LineEnding::CR => "\r", 30 | LineEnding::LF => "\n", 31 | LineEnding::CRLF => "\r\n", 32 | }; 33 | 34 | Ok(json) 35 | } 36 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::printer::print_success; 2 | use crate::rules::IssueLevel; 3 | use crate::{args::Args, printer::print_error}; 4 | use clap::Parser; 5 | use collect::{collect_issues, collect_packages}; 6 | use printer::{print_footer, print_issues}; 7 | use std::time::Instant; 8 | 9 | mod args; 10 | mod collect; 11 | mod install; 12 | mod json; 13 | mod packages; 14 | mod plural; 15 | mod printer; 16 | mod rules; 17 | 18 | fn is_ci() -> bool { 19 | std::env::var("CI").is_ok() 20 | } 21 | 22 | fn main() { 23 | let now = Instant::now(); 24 | let args = Args::parse(); 25 | 26 | if args.fix && is_ci() { 27 | print_error( 28 | "Failed to fix issues", 29 | "Cannot fix issues inside a CI environment", 30 | ); 31 | std::process::exit(1); 32 | } 33 | 34 | let packages_list = match collect_packages(&args) { 35 | Ok(result) => result, 36 | Err(error) => { 37 | print_error("Failed to collect packages", error.to_string().as_str()); 38 | std::process::exit(1); 39 | } 40 | }; 41 | 42 | let total_packages = packages_list.packages.len(); 43 | let mut issues = collect_issues(&args, packages_list); 44 | 45 | if args.fix { 46 | if let Err(error) = issues.fix() { 47 | print_error("Failed to fix issues", error.to_string().as_str()); 48 | std::process::exit(1); 49 | } 50 | } 51 | 52 | let total_issues = issues.total_len(); 53 | 54 | if total_issues == 0 { 55 | print_success(); 56 | return; 57 | } 58 | 59 | let warnings = issues.len_by_level(IssueLevel::Warning); 60 | let errors = issues.len_by_level(IssueLevel::Error); 61 | let fixed = issues.len_by_level(IssueLevel::Fixed); 62 | 63 | // Only run the install command if we allow it and we fixed some issues. 64 | if args.fix && !args.no_install && fixed > 0 { 65 | if let Err(error) = install::install() { 66 | print_error("Failed to install packages", error.to_string().as_str()); 67 | std::process::exit(1); 68 | } 69 | } 70 | 71 | if let Err(error) = print_issues(issues) { 72 | print_error("Failed to print issues", error.to_string().as_str()); 73 | std::process::exit(1); 74 | } 75 | 76 | print_footer(total_issues, total_packages, warnings, errors, fixed, now); 77 | 78 | if errors > 0 { 79 | std::process::exit(1); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/packages/mod.rs: -------------------------------------------------------------------------------- 1 | use self::semversion::SemVersion; 2 | use crate::rules::{ 3 | empty_dependencies::{DependencyKind, EmptyDependenciesIssue}, 4 | unordered_dependencies::UnorderedDependenciesIssue, 5 | BoxIssue, 6 | }; 7 | use anyhow::{anyhow, Result}; 8 | use indexmap::IndexMap; 9 | use root::RootPackage; 10 | use serde::Deserialize; 11 | use std::{fs, path::PathBuf}; 12 | 13 | pub mod root; 14 | pub mod semversion; 15 | 16 | pub struct PackagesList { 17 | pub root_package: RootPackage, 18 | pub packages: Vec, 19 | pub packages_issues: Vec, 20 | } 21 | 22 | #[derive(Deserialize, Debug)] 23 | #[serde(untagged)] 24 | pub enum Workspaces { 25 | Default(Vec), 26 | /// https://classic.yarnpkg.com/blog/2018/02/15/nohoist 27 | Yarn { 28 | packages: Vec, 29 | }, 30 | } 31 | 32 | #[derive(Deserialize, Debug)] 33 | struct PackageInner { 34 | name: Option, 35 | private: Option, 36 | workspaces: Option, 37 | #[serde(rename = "packageManager")] 38 | package_manager: Option, 39 | dependencies: Option>, 40 | #[serde(rename = "devDependencies")] 41 | dev_dependencies: Option>, 42 | #[serde(rename = "peerDependencies")] 43 | peer_dependencies: Option>, 44 | #[serde(rename = "optionalDependencies")] 45 | optional_dependencies: Option>, 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct Package { 50 | path: PathBuf, 51 | inner: PackageInner, 52 | } 53 | 54 | impl Package { 55 | pub fn new(path: PathBuf) -> Result { 56 | if !path.is_dir() { 57 | return Err(anyhow!("Path {:?} is not a directory", path)); 58 | } 59 | 60 | let package_path = path.join("package.json"); 61 | 62 | if !package_path.is_file() { 63 | return Err(anyhow!("`package.json` not found in {:?}", path)); 64 | } 65 | 66 | let root_package = fs::read_to_string(&package_path)?; 67 | let package: PackageInner = match serde_json::from_str(&root_package) { 68 | Ok(package) => package, 69 | Err(err) => return Err(anyhow!("Error while parsing {:?}: {}", package_path, err)), 70 | }; 71 | 72 | Ok(Self { 73 | path, 74 | inner: package, 75 | }) 76 | } 77 | 78 | pub fn get_name(&self) -> &Option { 79 | &self.inner.name 80 | } 81 | 82 | pub fn get_path(&self) -> String { 83 | self.path.to_string_lossy().to_string() 84 | } 85 | 86 | pub fn is_private(&self) -> bool { 87 | self.inner.private.unwrap_or(false) 88 | } 89 | 90 | fn check_deps( 91 | &self, 92 | deps: &Option>, 93 | dependency_kind: DependencyKind, 94 | ) -> Option { 95 | if let Some(dependencies) = deps { 96 | if dependencies.is_empty() { 97 | return Some(EmptyDependenciesIssue::new(dependency_kind)); 98 | } 99 | 100 | let mut sorted_dependencies = dependencies.clone(); 101 | sorted_dependencies.sort_keys(); 102 | 103 | if sorted_dependencies.keys().ne(dependencies.keys()) { 104 | return Some(UnorderedDependenciesIssue::new(dependency_kind)); 105 | } 106 | } 107 | 108 | None 109 | } 110 | 111 | pub fn check_dependencies(&self) -> Option { 112 | self.check_deps(&self.inner.dependencies, DependencyKind::Dependencies) 113 | } 114 | 115 | pub fn check_dev_dependencies(&self) -> Option { 116 | self.check_deps( 117 | &self.inner.dev_dependencies, 118 | DependencyKind::DevDependencies, 119 | ) 120 | } 121 | 122 | pub fn check_peer_dependencies(&self) -> Option { 123 | self.check_deps( 124 | &self.inner.peer_dependencies, 125 | DependencyKind::PeerDependencies, 126 | ) 127 | } 128 | 129 | pub fn check_optional_dependencies(&self) -> Option { 130 | self.check_deps( 131 | &self.inner.optional_dependencies, 132 | DependencyKind::OptionalDependencies, 133 | ) 134 | } 135 | 136 | fn get_deps( 137 | &self, 138 | deps: &Option>, 139 | ) -> Option> { 140 | if let Some(dependencies) = deps { 141 | let mut versioned_dependencies = 142 | IndexMap::::with_capacity(dependencies.len()); 143 | 144 | for (name, version) in dependencies { 145 | if let Ok(version) = SemVersion::parse(version) { 146 | versioned_dependencies.insert(name.clone(), version); 147 | } 148 | } 149 | 150 | return Some(versioned_dependencies); 151 | } 152 | 153 | None 154 | } 155 | 156 | pub fn get_dependencies(&self) -> Option> { 157 | self.get_deps(&self.inner.dependencies) 158 | } 159 | 160 | pub fn get_dev_dependencies(&self) -> Option> { 161 | self.get_deps(&self.inner.dev_dependencies) 162 | } 163 | 164 | pub fn is_ignored(&self, ignored_packages: &[String]) -> bool { 165 | match self.get_name() { 166 | Some(name) => ignored_packages.iter().any(|ignored_package| { 167 | match ignored_package.ends_with('*') { 168 | true => { 169 | let ignored_package = ignored_package.trim_end_matches('*'); 170 | 171 | name.starts_with(ignored_package) 172 | || self.get_path().starts_with(ignored_package) 173 | } 174 | false => ignored_package == name || ignored_package == &self.get_path(), 175 | } 176 | }), 177 | None => false, 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/packages/root.rs: -------------------------------------------------------------------------------- 1 | use super::{semversion::SemVersion, Package, Workspaces}; 2 | use crate::rules::{ 3 | root_package_dependencies::RootPackageDependenciesIssue, 4 | root_package_manager_field::RootPackageManagerFieldIssue, 5 | root_package_private_field::RootPackagePrivateFieldIssue, BoxIssue, 6 | }; 7 | use anyhow::Result; 8 | use indexmap::IndexMap; 9 | use std::path::Path; 10 | 11 | #[derive(Debug)] 12 | pub struct RootPackage(Package); 13 | 14 | impl RootPackage { 15 | pub fn new(path: &Path) -> Result { 16 | let package = Package::new(path.to_path_buf())?; 17 | 18 | Ok(Self(package)) 19 | } 20 | 21 | #[cfg(test)] 22 | pub fn get_name(&self) -> String { 23 | self.0.get_name().clone().unwrap_or_default() 24 | } 25 | 26 | pub fn get_path(&self) -> String { 27 | self.0.get_path() 28 | } 29 | 30 | pub fn get_workspaces(&self) -> Option> { 31 | match &self.0.inner.workspaces { 32 | Some(workspaces) => match workspaces { 33 | Workspaces::Default(workspaces) => Some(workspaces.clone()), 34 | Workspaces::Yarn { packages, .. } => Some(packages.clone()), 35 | }, 36 | None => None, 37 | } 38 | } 39 | 40 | pub fn check_private(&self) -> Option { 41 | match self.0.inner.private { 42 | Some(true) => None, 43 | _ => Some(RootPackagePrivateFieldIssue::new()), 44 | } 45 | } 46 | 47 | pub fn check_package_manager(&self) -> Option { 48 | match self.0.inner.package_manager.is_none() { 49 | true => Some(RootPackageManagerFieldIssue::new()), 50 | false => None, 51 | } 52 | } 53 | 54 | pub fn check_dependencies(&self) -> Option { 55 | match self.0.inner.dependencies.is_some() { 56 | true => Some(RootPackageDependenciesIssue::new()), 57 | false => self.0.check_dependencies(), 58 | } 59 | } 60 | 61 | pub fn check_dev_dependencies(&self) -> Option { 62 | self.0.check_dev_dependencies() 63 | } 64 | 65 | pub fn check_peer_dependencies(&self) -> Option { 66 | self.0.check_peer_dependencies() 67 | } 68 | 69 | pub fn check_optional_dependencies(&self) -> Option { 70 | self.0.check_optional_dependencies() 71 | } 72 | 73 | pub fn get_dependencies(&self) -> Option> { 74 | self.0.get_dependencies() 75 | } 76 | 77 | pub fn get_dev_dependencies(&self) -> Option> { 78 | self.0.get_dev_dependencies() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/packages/semversion.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use semver::{Prerelease, Version, VersionReq}; 3 | use std::{cmp::Ordering, fmt::Display}; 4 | 5 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 6 | pub enum SemVersion { 7 | Exact(Version), 8 | Range(VersionReq), 9 | } 10 | 11 | impl Display for SemVersion { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Self::Exact(version) => f.write_str(&version.to_string()), 15 | Self::Range(version) => f.write_str(&version.to_string()), 16 | } 17 | } 18 | } 19 | 20 | impl SemVersion { 21 | pub fn parse(version: &str) -> Result { 22 | if let Ok(version) = Version::parse(version) { 23 | return Ok(Self::Exact(version)); 24 | } 25 | 26 | if let Ok(version) = VersionReq::parse(version) { 27 | return Ok(Self::Range(version)); 28 | } 29 | 30 | Err(anyhow!("Invalid version: {}", version)) 31 | } 32 | 33 | pub fn patch(&self) -> u64 { 34 | match self { 35 | Self::Exact(version) => version.patch, 36 | Self::Range(version) => version 37 | .comparators 38 | .first() 39 | .map_or(0, |comparator| comparator.patch.unwrap_or(0)), 40 | } 41 | } 42 | 43 | pub fn minor(&self) -> u64 { 44 | match self { 45 | Self::Exact(version) => version.minor, 46 | Self::Range(version) => version 47 | .comparators 48 | .first() 49 | .map_or(0, |comparator| comparator.minor.unwrap_or(0)), 50 | } 51 | } 52 | 53 | pub fn major(&self) -> u64 { 54 | match self { 55 | Self::Exact(version) => version.major, 56 | Self::Range(version) => version 57 | .comparators 58 | .first() 59 | .map_or(0, |comparator| comparator.major), 60 | } 61 | } 62 | 63 | pub fn prerelease(&self) -> Prerelease { 64 | match self { 65 | Self::Exact(version) => version.pre.clone(), 66 | Self::Range(version) => version 67 | .comparators 68 | .first() 69 | .map_or(Prerelease::EMPTY, |comparator| comparator.pre.clone()), 70 | } 71 | } 72 | 73 | pub fn cmp(&self, other: &Self) -> Ordering { 74 | let mut ordering = self.patch().cmp(&other.patch()); 75 | 76 | ordering = match self.minor().cmp(&other.minor()) { 77 | Ordering::Equal => ordering, 78 | new_ordering => new_ordering, 79 | }; 80 | 81 | ordering = match self.major().cmp(&other.major()) { 82 | Ordering::Equal => ordering, 83 | new_ordering => new_ordering, 84 | }; 85 | 86 | match self.prerelease().cmp(&other.prerelease()) { 87 | Ordering::Equal => ordering, 88 | new_ordering => new_ordering, 89 | } 90 | } 91 | 92 | pub fn is_valid(&self) -> bool { 93 | match self { 94 | Self::Exact(_) => true, 95 | Self::Range(version) => !version.comparators.is_empty(), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/plural.rs: -------------------------------------------------------------------------------- 1 | pub trait Pluralize { 2 | fn plural(&self, count: usize) -> String; 3 | } 4 | 5 | impl Pluralize for &'static str { 6 | fn plural(&self, count: usize) -> String { 7 | match count { 8 | 1 => format!("{} {}", count, self), 9 | _ => format!("{} {}s", count, self), 10 | } 11 | } 12 | } 13 | 14 | #[cfg(test)] 15 | mod test { 16 | use super::*; 17 | 18 | #[test] 19 | fn test_pluralize() { 20 | assert_eq!("1 package", "package".plural(1)); 21 | assert_eq!("2 packages", "package".plural(2)); 22 | assert_eq!("4 packages", "package".plural(4)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | plural::Pluralize, 3 | rules::{IssueLevel, IssuesList, ERROR, SUCCESS, WARNING}, 4 | }; 5 | use anyhow::Result; 6 | use colored::Colorize; 7 | use inquire::ui::{Color, RenderConfig, StyleSheet, Styled}; 8 | use std::io::Write; 9 | use std::time::Instant; 10 | 11 | pub fn print_success() { 12 | println!(); 13 | println!("{}", format!("{} No issues found", SUCCESS).green()); 14 | } 15 | 16 | pub fn print_error(title: &str, message: &str) { 17 | eprintln!(); 18 | eprintln!(" {} {}", IssueLevel::Error, title.bold()); 19 | eprintln!(" {}", message.bright_black()); 20 | } 21 | 22 | pub fn print_issues(issues: IssuesList) -> Result<()> { 23 | // Lock stdout manually instead of in every `println` 24 | // calls, since we might have a lot of them. 25 | let stdout = std::io::stdout(); 26 | let mut lock = stdout.lock(); 27 | 28 | for (package_type, issues) in issues { 29 | writeln!(lock)?; 30 | writeln!( 31 | lock, 32 | "{} found in {}:", 33 | "issue".plural(issues.len()), 34 | package_type.to_string().bold(), 35 | )?; 36 | 37 | for issue in issues { 38 | writeln!(lock)?; 39 | writeln!( 40 | lock, 41 | " {} {} {}", 42 | issue.level().to_string().bold(), 43 | issue.why().bold(), 44 | issue.name().bright_black(), 45 | )?; 46 | writeln!(lock, "{}", issue.message())?; 47 | } 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | pub fn print_footer( 54 | total_issues: usize, 55 | total_packages: usize, 56 | warnings: usize, 57 | errors: usize, 58 | fixed: usize, 59 | start: Instant, 60 | ) { 61 | println!(); 62 | println!( 63 | "{} found {} across {} in {:?}.", 64 | "issue".plural(total_issues), 65 | format!( 66 | "({} {}, {} {}, {} {})", 67 | errors, ERROR, warnings, WARNING, fixed, SUCCESS 68 | ) 69 | .bright_black(), 70 | "package".plural(total_packages), 71 | start.elapsed(), 72 | ); 73 | println!( 74 | "{}", 75 | " Note: use `-i` to ignore dependencies, `-r` to ignore rules, `-p` to ignore packages, and `-f` to autofix fixable issues." 76 | .bright_black() 77 | ); 78 | } 79 | 80 | pub fn get_render_config() -> RenderConfig { 81 | let mut render_config = RenderConfig::default_colored() 82 | .with_prompt_prefix(Styled::new("✓").with_fg(Color::DarkGrey)) 83 | .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)) 84 | .with_highlighted_option_prefix(Styled::new(" → ").with_fg(Color::LightCyan)) 85 | .with_canceled_prompt_indicator(Styled::new("✗").with_fg(Color::LightRed)); 86 | render_config.answered_prompt_prefix = Styled::new("✓").with_fg(Color::LightGreen); 87 | render_config 88 | } 89 | -------------------------------------------------------------------------------- /src/rules/empty_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use crate::json::{self}; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use std::{borrow::Cow, fmt::Display, fs, path::PathBuf}; 6 | 7 | #[derive(Debug)] 8 | pub enum DependencyKind { 9 | Dependencies, 10 | DevDependencies, 11 | PeerDependencies, 12 | OptionalDependencies, 13 | } 14 | 15 | impl Display for DependencyKind { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | match self { 18 | DependencyKind::Dependencies => write!(f, "dependencies"), 19 | DependencyKind::DevDependencies => write!(f, "devDependencies"), 20 | DependencyKind::PeerDependencies => write!(f, "peerDependencies"), 21 | DependencyKind::OptionalDependencies => write!(f, "optionalDependencies"), 22 | } 23 | } 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct EmptyDependenciesIssue { 28 | dependency_kind: DependencyKind, 29 | fixed: bool, 30 | } 31 | 32 | impl EmptyDependenciesIssue { 33 | pub fn new(dependency_kind: DependencyKind) -> Box { 34 | Box::new(Self { 35 | dependency_kind, 36 | fixed: false, 37 | }) 38 | } 39 | } 40 | 41 | impl Issue for EmptyDependenciesIssue { 42 | fn name(&self) -> &str { 43 | "empty-dependencies" 44 | } 45 | 46 | fn level(&self) -> IssueLevel { 47 | match self.fixed { 48 | true => IssueLevel::Fixed, 49 | false => IssueLevel::Error, 50 | } 51 | } 52 | 53 | fn message(&self) -> String { 54 | format!( 55 | r#" │ {{ 56 | {} "{}": {} {} 57 | │ }}"#, 58 | "-".red(), 59 | self.dependency_kind.to_string().white(), 60 | "{}".white(), 61 | "← field is empty.".red(), 62 | ) 63 | .bright_black() 64 | .to_string() 65 | } 66 | 67 | fn why(&self) -> Cow<'static, str> { 68 | Cow::Borrowed("package.json should not have empty dependencies fields.") 69 | } 70 | 71 | fn fix(&mut self, package_type: &PackageType) -> Result<()> { 72 | if let PackageType::Package(path) = package_type { 73 | let path = PathBuf::from(path).join("package.json"); 74 | let value = fs::read_to_string(&path)?; 75 | let (mut value, indent, lineending) = json::deserialize::(&value)?; 76 | let dependency = self.dependency_kind.to_string(); 77 | 78 | if let Some(dependency_field) = value.get(&dependency) { 79 | if dependency_field.is_object() && dependency_field.as_object().unwrap().is_empty() 80 | { 81 | value.as_object_mut().unwrap().remove(&dependency); 82 | 83 | let value = json::serialize(&value, indent, lineending)?; 84 | fs::write(path, value)?; 85 | 86 | self.fixed = true; 87 | } 88 | } 89 | } 90 | 91 | Ok(()) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod test { 97 | use super::*; 98 | 99 | #[test] 100 | fn test() { 101 | let issue = EmptyDependenciesIssue::new(DependencyKind::Dependencies); 102 | 103 | assert_eq!(issue.name(), "empty-dependencies"); 104 | assert_eq!(issue.level(), IssueLevel::Error); 105 | assert_eq!( 106 | issue.why(), 107 | "package.json should not have empty dependencies fields." 108 | ); 109 | } 110 | 111 | #[test] 112 | fn test_dependency_kind() { 113 | colored::control::set_override(false); 114 | 115 | let issue = EmptyDependenciesIssue::new(DependencyKind::Dependencies); 116 | insta::assert_snapshot!(issue.message()); 117 | 118 | let issue = EmptyDependenciesIssue::new(DependencyKind::DevDependencies); 119 | insta::assert_snapshot!(issue.message()); 120 | 121 | let issue = EmptyDependenciesIssue::new(DependencyKind::PeerDependencies); 122 | insta::assert_snapshot!(issue.message()); 123 | 124 | let issue = EmptyDependenciesIssue::new(DependencyKind::OptionalDependencies); 125 | insta::assert_snapshot!(issue.message()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use colored::Colorize; 3 | use indexmap::IndexMap; 4 | use std::{ 5 | borrow::Cow, 6 | fmt::{Debug, Display}, 7 | }; 8 | 9 | pub mod empty_dependencies; 10 | pub mod multiple_dependency_versions; 11 | pub mod non_existant_packages; 12 | pub mod packages_without_package_json; 13 | pub mod root_package_dependencies; 14 | pub mod root_package_manager_field; 15 | pub mod root_package_private_field; 16 | pub mod types_in_dependencies; 17 | pub mod unordered_dependencies; 18 | pub mod unsync_similar_dependencies; 19 | 20 | pub const ERROR: &str = "⨯"; 21 | pub const WARNING: &str = "⚠️"; 22 | pub const SUCCESS: &str = "✓"; 23 | 24 | #[derive(Debug, PartialEq)] 25 | pub enum IssueLevel { 26 | Error, 27 | Warning, 28 | Fixed, 29 | } 30 | 31 | impl IssueLevel { 32 | pub fn as_str(&self) -> &'static str { 33 | match self { 34 | IssueLevel::Error => "⨯ error", 35 | IssueLevel::Warning => "⚠️ warning", 36 | IssueLevel::Fixed => "✓ fixed", 37 | } 38 | } 39 | } 40 | 41 | impl Display for IssueLevel { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | let value = self.as_str(); 44 | 45 | match self { 46 | IssueLevel::Error => write!(f, "{}", value.red()), 47 | IssueLevel::Warning => write!(f, "{}", value.yellow()), 48 | IssueLevel::Fixed => write!(f, "{}", value.green()), 49 | } 50 | } 51 | } 52 | 53 | pub trait Issue { 54 | fn name(&self) -> &str; 55 | fn level(&self) -> IssueLevel; 56 | fn message(&self) -> String; 57 | fn why(&self) -> Cow<'static, str>; 58 | 59 | fn fix(&mut self, _package_type: &PackageType) -> Result<()> { 60 | Ok(()) 61 | } 62 | } 63 | 64 | pub type BoxIssue = Box; 65 | 66 | #[derive(Debug, Hash, PartialEq, Eq, Clone)] 67 | pub enum PackageType { 68 | None, 69 | Root, 70 | Package(String), 71 | } 72 | 73 | impl Display for PackageType { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 75 | match self { 76 | PackageType::None => write!(f, "./"), 77 | PackageType::Root => write!(f, "./package.json"), 78 | PackageType::Package(name) => write!(f, "{}/package.json", name), 79 | } 80 | } 81 | } 82 | 83 | pub struct IssuesList<'a> { 84 | ignored_issues: &'a [String], 85 | issues: IndexMap>, 86 | } 87 | 88 | impl<'a> IssuesList<'a> { 89 | pub fn new(ignored_issues: &'a [String]) -> Self { 90 | Self { 91 | ignored_issues, 92 | issues: IndexMap::new(), 93 | } 94 | } 95 | 96 | pub fn add_raw(&mut self, package_type: PackageType, issue: BoxIssue) { 97 | if self.ignored_issues.contains(&issue.name().to_string()) { 98 | return; 99 | } 100 | 101 | self.issues.entry(package_type).or_default().push(issue); 102 | } 103 | 104 | pub fn add(&mut self, package_type: PackageType, issue: Option) { 105 | if let Some(issue) = issue { 106 | self.add_raw(package_type, issue); 107 | } 108 | } 109 | 110 | pub fn total_len(&self) -> usize { 111 | self.issues.values().flatten().collect::>().len() 112 | } 113 | 114 | pub fn len_by_level(&self, level: IssueLevel) -> usize { 115 | self.issues 116 | .values() 117 | .flatten() 118 | .filter(|issue| issue.level() == level) 119 | .count() 120 | } 121 | 122 | pub fn fix(&mut self) -> Result<()> { 123 | for (package_type, issues) in self.issues.iter_mut() { 124 | for issue in issues { 125 | if let Err(error) = issue.fix(package_type) { 126 | return Err(anyhow!("Error while fixing {}: {}", package_type, error)); 127 | } 128 | } 129 | } 130 | 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl IntoIterator for IssuesList<'_> { 136 | type Item = (PackageType, Vec); 137 | type IntoIter = indexmap::map::IntoIter>; 138 | 139 | fn into_iter(self) -> Self::IntoIter { 140 | self.issues.into_iter() 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod test { 146 | use super::*; 147 | use crate::rules::{ 148 | root_package_dependencies::RootPackageDependenciesIssue, 149 | root_package_manager_field::RootPackageManagerFieldIssue, 150 | }; 151 | 152 | #[test] 153 | fn add_issues() { 154 | let ignored_issues = Vec::new(); 155 | let mut issues = IssuesList::new(&ignored_issues); 156 | 157 | issues.add(PackageType::Root, Some(RootPackageManagerFieldIssue::new())); 158 | assert_eq!(issues.total_len(), 1); 159 | 160 | issues.add_raw(PackageType::Root, RootPackageManagerFieldIssue::new()); 161 | assert_eq!(issues.total_len(), 2); 162 | 163 | issues.add(PackageType::Root, None); 164 | assert_eq!(issues.total_len(), 2); 165 | } 166 | 167 | #[test] 168 | fn add_ignored() { 169 | let ignored_issues = vec!["root-package-manager-field".to_string()]; 170 | let mut issues = IssuesList::new(&ignored_issues); 171 | 172 | issues.add_raw(PackageType::Root, RootPackageManagerFieldIssue::new()); 173 | assert_eq!(issues.total_len(), 0); 174 | 175 | issues.add_raw(PackageType::Root, RootPackageDependenciesIssue::new()); 176 | assert_eq!(issues.total_len(), 1); 177 | } 178 | 179 | #[test] 180 | fn len_by_level() { 181 | let ignored_issues = Vec::new(); 182 | let mut issues = IssuesList::new(&ignored_issues); 183 | 184 | issues.add_raw(PackageType::Root, RootPackageManagerFieldIssue::new()); 185 | issues.add_raw(PackageType::Root, RootPackageDependenciesIssue::new()); 186 | issues.add_raw(PackageType::Root, RootPackageDependenciesIssue::new()); 187 | issues.add_raw(PackageType::Root, RootPackageDependenciesIssue::new()); 188 | 189 | assert_eq!(issues.len_by_level(IssueLevel::Error), 1); 190 | assert_eq!(issues.len_by_level(IssueLevel::Warning), 3); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/rules/multiple_dependency_versions.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use crate::{json, packages::semversion::SemVersion, printer::get_render_config}; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use indexmap::IndexMap; 6 | use inquire::Select; 7 | use std::{borrow::Cow, fs, path::PathBuf}; 8 | 9 | #[derive(Debug)] 10 | pub struct MultipleDependencyVersionsIssue { 11 | name: String, 12 | versions: IndexMap, 13 | fixed: bool, 14 | } 15 | 16 | impl MultipleDependencyVersionsIssue { 17 | pub fn new(name: String, mut versions: IndexMap) -> Box { 18 | versions.sort_by(|_, a, _, b| b.cmp(a)); 19 | 20 | Box::new(Self { 21 | name, 22 | versions, 23 | fixed: false, 24 | }) 25 | } 26 | } 27 | 28 | fn format_version( 29 | version: &SemVersion, 30 | versions: &IndexMap, 31 | skip_version_color: bool, 32 | ) -> String { 33 | let (version, indicator) = if version == versions.first().unwrap().1 { 34 | (version.to_string().green(), "↑ highest".green()) 35 | } else if version == versions.last().unwrap().1 { 36 | (version.to_string().red(), "↓ lowest".red()) 37 | } else { 38 | (version.to_string().yellow(), "∼ between".yellow()) 39 | }; 40 | let version = match skip_version_color { 41 | true => version.clear(), 42 | false => version, 43 | }; 44 | 45 | format!("{} {}", version, indicator) 46 | } 47 | 48 | impl Issue for MultipleDependencyVersionsIssue { 49 | fn name(&self) -> &str { 50 | "multiple-dependency-versions" 51 | } 52 | 53 | fn level(&self) -> IssueLevel { 54 | match self.fixed { 55 | true => IssueLevel::Fixed, 56 | false => IssueLevel::Error, 57 | } 58 | } 59 | 60 | fn message(&self) -> String { 61 | let mut group = vec![]; 62 | 63 | self.versions 64 | .iter() 65 | .map(|(package, version)| { 66 | let mut common_path = package.split('/').collect::>(); 67 | let mut end = common_path.pop().unwrap(); 68 | 69 | if end == "." { 70 | end = "./"; 71 | } 72 | 73 | let formatted_version = format_version(version, &self.versions, false); 74 | let version_pad = " ".repeat(if end.len() >= 26 { 3 } else { 26 - end.len() }); 75 | 76 | if group.is_empty() || group != common_path { 77 | let root = common_path.join("/").bright_black(); 78 | group = common_path; 79 | 80 | if group.len() == 1 && group[0] == "." { 81 | let root = format!("{}{}", "./".bright_black(), end.bright_black()); 82 | 83 | return format!(" {} {}{}", root, version_pad, formatted_version); 84 | } 85 | 86 | return format!( 87 | " {} 88 | {}{}{}", 89 | root, 90 | end.bright_black(), 91 | version_pad, 92 | formatted_version 93 | ); 94 | } 95 | 96 | group = common_path; 97 | 98 | format!( 99 | " {}{}{}", 100 | end.bright_black(), 101 | version_pad, 102 | formatted_version 103 | ) 104 | }) 105 | .collect::>() 106 | .join("\n") 107 | } 108 | 109 | fn why(&self) -> Cow<'static, str> { 110 | Cow::Owned(format!( 111 | "Dependency {} has multiple versions defined in the workspace.", 112 | self.name 113 | )) 114 | } 115 | 116 | fn fix(&mut self, _package_type: &PackageType) -> Result<()> { 117 | let message = format!("Select the version of {} to use:", self.name.bold()); 118 | 119 | let mut sorted_versions = self.versions.values().collect::>(); 120 | sorted_versions.sort_by(|a, b| b.cmp(a)); 121 | 122 | let mut versions = sorted_versions 123 | .iter() 124 | .map(|version| format_version(version, &self.versions, true)) 125 | .collect::>(); 126 | versions.dedup(); 127 | 128 | let select = Select::new(&message, versions) 129 | .with_render_config(get_render_config()) 130 | .with_help_message("Enter to select, Esc to skip") 131 | .prompt_skippable()?; 132 | 133 | if let Some(select) = select { 134 | let version = select 135 | .split_once(' ') 136 | .expect("Please report this as a bug") 137 | .0 138 | .to_string(); 139 | 140 | for package in self.versions.keys() { 141 | let path = PathBuf::from(package).join("package.json"); 142 | let value = fs::read_to_string(&path)?; 143 | let (mut value, indent, lineending) = 144 | json::deserialize::(&value)?; 145 | 146 | if let Some(dependencies) = value.get_mut("dependencies") { 147 | let dependencies = dependencies.as_object_mut().unwrap(); 148 | 149 | if let Some(dependency) = dependencies.get_mut(&self.name) { 150 | *dependency = serde_json::Value::String(version.to_string()); 151 | } 152 | } 153 | 154 | if let Some(dev_dependencies) = value.get_mut("devDependencies") { 155 | let dev_dependencies = dev_dependencies.as_object_mut().unwrap(); 156 | 157 | if let Some(dev_dependency) = dev_dependencies.get_mut(&self.name) { 158 | *dev_dependency = serde_json::Value::String(version.to_string()); 159 | } 160 | } 161 | 162 | let value = json::serialize(&value, indent, lineending)?; 163 | fs::write(path, value)?; 164 | } 165 | 166 | self.fixed = true; 167 | } 168 | 169 | Ok(()) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod test { 175 | use super::*; 176 | 177 | #[test] 178 | fn test() { 179 | let issue = MultipleDependencyVersionsIssue::new( 180 | "test".to_string(), 181 | indexmap::indexmap! { 182 | "./packages/package-a".into() => SemVersion::parse("1.2.3").unwrap(), 183 | "./packages/package-b".into() => SemVersion::parse("1.2.4").unwrap(), 184 | "./package-c".into() => SemVersion::parse("1.2.5").unwrap(), 185 | }, 186 | ); 187 | 188 | assert_eq!(issue.name(), "multiple-dependency-versions"); 189 | assert_eq!(issue.level(), IssueLevel::Error); 190 | assert_eq!(issue.versions.len(), 3); 191 | assert_eq!( 192 | issue.why(), 193 | "Dependency test has multiple versions defined in the workspace.".to_string() 194 | ); 195 | } 196 | 197 | #[test] 198 | fn root() { 199 | let issue = MultipleDependencyVersionsIssue::new( 200 | "test".to_string(), 201 | indexmap::indexmap! { 202 | "./".into() => SemVersion::parse("5.6.3").unwrap(), 203 | "./packages/package-a".into() => SemVersion::parse("1.2.3").unwrap(), 204 | "./packages/package-b".into() => SemVersion::parse("3.1.6").unwrap(), 205 | }, 206 | ); 207 | 208 | colored::control::set_override(false); 209 | insta::assert_snapshot!(issue.message()); 210 | } 211 | 212 | #[test] 213 | fn order_single() { 214 | let issue = MultipleDependencyVersionsIssue::new( 215 | "test".to_string(), 216 | indexmap::indexmap! { 217 | "./package-a".into() => SemVersion::parse("1.2.3").unwrap(), 218 | }, 219 | ); 220 | 221 | colored::control::set_override(false); 222 | insta::assert_snapshot!(issue.message()); 223 | } 224 | 225 | #[test] 226 | fn order_multiple() { 227 | let issue = MultipleDependencyVersionsIssue::new( 228 | "test".to_string(), 229 | indexmap::indexmap! { 230 | "./apps/package-a".into() => SemVersion::parse("5.6.3").unwrap(), 231 | "./apps/package-b".into() => SemVersion::parse("1.2.3").unwrap(), 232 | "./packages/package-c".into() => SemVersion::parse("3.1.6").unwrap(), 233 | }, 234 | ); 235 | 236 | colored::control::set_override(false); 237 | insta::assert_snapshot!(issue.message()); 238 | } 239 | 240 | #[test] 241 | fn order_prerelease() { 242 | let issue = MultipleDependencyVersionsIssue::new( 243 | "test".to_string(), 244 | indexmap::indexmap! { 245 | "./apps/package-a".into() => SemVersion::parse("5.0.0-next.4").unwrap(), 246 | "./apps/package-b".into() => SemVersion::parse("5.0.0-next.3").unwrap(), 247 | "./packages/package-c".into() => SemVersion::parse("5.0.0-next.6").unwrap(), 248 | }, 249 | ); 250 | 251 | colored::control::set_override(false); 252 | insta::assert_snapshot!(issue.message()); 253 | } 254 | 255 | #[test] 256 | fn exact_and_range() { 257 | let issue = MultipleDependencyVersionsIssue::new( 258 | "test".to_string(), 259 | indexmap::indexmap! { 260 | "./apps/package-a".into() => SemVersion::parse("5.6.3").unwrap(), 261 | "./apps/package-b".into() => SemVersion::parse("^1.2.3").unwrap(), 262 | "./packages/package-c".into() => SemVersion::parse("~3.1.6").unwrap(), 263 | }, 264 | ); 265 | 266 | colored::control::set_override(false); 267 | insta::assert_snapshot!(issue.message()); 268 | } 269 | 270 | #[test] 271 | fn dedupe() { 272 | let issue = MultipleDependencyVersionsIssue::new( 273 | "test".to_string(), 274 | indexmap::indexmap! { 275 | "./package-a".into() => SemVersion::parse("1.2.3").unwrap(), 276 | "./packages/package-b".into() => SemVersion::parse("3.1.6").unwrap(), 277 | "./packages/package-c".into() => SemVersion::parse("3.1.6").unwrap(), 278 | }, 279 | ); 280 | 281 | colored::control::set_override(false); 282 | insta::assert_snapshot!(issue.message()); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/rules/non_existant_packages.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use crate::json; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use std::{borrow::Cow, fs, path::PathBuf}; 6 | 7 | #[derive(Debug)] 8 | pub struct NonExistantPackagesIssue { 9 | pnpm_workspace: bool, 10 | packages_list: Vec, 11 | paths: Vec, 12 | fixed: bool, 13 | } 14 | 15 | impl NonExistantPackagesIssue { 16 | pub fn new(pnpm_workspace: bool, packages_list: Vec, paths: Vec) -> Box { 17 | Box::new(Self { 18 | pnpm_workspace, 19 | packages_list, 20 | paths, 21 | fixed: false, 22 | }) 23 | } 24 | 25 | fn pnpm_message(&self) -> String { 26 | let workspaces = self 27 | .packages_list 28 | .iter() 29 | .map(|package| match self.paths.contains(package) { 30 | true => format!( 31 | " {} - '{}' {}", 32 | "-".red(), 33 | package.white(), 34 | "← but this one doesn't match any package".red(), 35 | ), 36 | false => format!(" │ - '{}'", package), 37 | }) 38 | .collect::>() 39 | .join("\n"); 40 | 41 | format!( 42 | r#" │ packages: {} 43 | {}"#, 44 | "← Workspace has paths defined...".blue(), 45 | workspaces, 46 | ) 47 | .bright_black() 48 | .to_string() 49 | } 50 | 51 | fn package_message(&self) -> String { 52 | let workspaces = self 53 | .packages_list 54 | .iter() 55 | .map(|package| match self.paths.contains(package) { 56 | true => format!( 57 | r#" {} "{}", {}"#, 58 | "-".red(), 59 | package.white(), 60 | "← but this one doesn't match any package".red(), 61 | ), 62 | false => format!(r#" │ "{}","#, package), 63 | }) 64 | .collect::>() 65 | .join("\n"); 66 | 67 | format!( 68 | r#" │ {{ 69 | │ "workspaces": [ {} 70 | {} 71 | │ ], 72 | │ }}"#, 73 | "← Workspace has paths defined...".blue(), 74 | workspaces, 75 | ) 76 | .bright_black() 77 | .to_string() 78 | } 79 | } 80 | 81 | impl Issue for NonExistantPackagesIssue { 82 | fn name(&self) -> &str { 83 | "non-existant-packages" 84 | } 85 | 86 | fn level(&self) -> IssueLevel { 87 | match self.fixed { 88 | true => IssueLevel::Fixed, 89 | false => IssueLevel::Warning, 90 | } 91 | } 92 | 93 | fn message(&self) -> String { 94 | match self.pnpm_workspace { 95 | true => self.pnpm_message(), 96 | false => self.package_message(), 97 | } 98 | } 99 | 100 | fn why(&self) -> Cow<'static, str> { 101 | Cow::Borrowed("All paths defined in the workspace should match at least one package.") 102 | } 103 | 104 | fn fix(&mut self, package_type: &PackageType) -> Result<()> { 105 | if let PackageType::None = package_type { 106 | match self.pnpm_workspace { 107 | true => { 108 | let path = PathBuf::from("pnpm-workspace.yaml"); 109 | let value = fs::read_to_string(&path)?; 110 | let mut value = serde_yaml::from_str::(&value)?; 111 | 112 | value 113 | .get_mut("packages") 114 | .unwrap() 115 | .as_sequence_mut() 116 | .unwrap() 117 | .retain(|package| { 118 | let package = package.as_str().unwrap().to_string(); 119 | 120 | !self.paths.contains(&package) 121 | }); 122 | 123 | let value = serde_yaml::to_string(&value)?; 124 | fs::write(path, value)?; 125 | 126 | self.fixed = true; 127 | } 128 | false => { 129 | let path = PathBuf::from("package.json"); 130 | let value = fs::read_to_string(&path)?; 131 | let (mut value, indent, lineending) = 132 | json::deserialize::(&value)?; 133 | 134 | value 135 | .get_mut("workspaces") 136 | .unwrap() 137 | .as_array_mut() 138 | .unwrap() 139 | .retain(|package| { 140 | let package = package.as_str().unwrap().to_string(); 141 | 142 | !self.paths.contains(&package) 143 | }); 144 | 145 | let value = json::serialize(&value, indent, lineending)?; 146 | fs::write(path, value)?; 147 | 148 | self.fixed = true; 149 | } 150 | } 151 | } 152 | 153 | Ok(()) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod test { 159 | use super::*; 160 | 161 | #[test] 162 | fn test() { 163 | let issue = NonExistantPackagesIssue::new( 164 | true, 165 | vec![ 166 | "apps/*".into(), 167 | "packages/*".into(), 168 | "empty/*".into(), 169 | "docs".into(), 170 | ], 171 | vec!["empty/*".into(), "docs".into()], 172 | ); 173 | 174 | assert_eq!(issue.name(), "non-existant-packages"); 175 | assert_eq!(issue.level(), IssueLevel::Warning); 176 | assert_eq!( 177 | issue.why(), 178 | "All paths defined in the workspace should match at least one package." 179 | ); 180 | } 181 | 182 | #[test] 183 | fn test_pnpm_workspace() { 184 | let issue = NonExistantPackagesIssue::new( 185 | true, 186 | vec![ 187 | "apps/*".into(), 188 | "packages/*".into(), 189 | "empty/*".into(), 190 | "docs".into(), 191 | ], 192 | vec!["empty/*".into(), "docs".into()], 193 | ); 194 | 195 | colored::control::set_override(false); 196 | insta::assert_snapshot!(issue.message()); 197 | } 198 | 199 | #[test] 200 | fn test_package_workspace() { 201 | let issue = NonExistantPackagesIssue::new( 202 | false, 203 | vec![ 204 | "apps/*".into(), 205 | "packages/*".into(), 206 | "empty/*".into(), 207 | "docs".into(), 208 | ], 209 | vec!["empty/*".into(), "docs".into()], 210 | ); 211 | 212 | colored::control::set_override(false); 213 | insta::assert_snapshot!(issue.message()); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/rules/packages_without_package_json.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use anyhow::Result; 3 | use std::{borrow::Cow, fs, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct PackagesWithoutPackageJsonIssue { 7 | package: String, 8 | fixed: bool, 9 | } 10 | 11 | impl PackagesWithoutPackageJsonIssue { 12 | pub fn new(package: String) -> Box { 13 | Box::new(Self { 14 | package, 15 | fixed: false, 16 | }) 17 | } 18 | } 19 | 20 | impl Issue for PackagesWithoutPackageJsonIssue { 21 | fn name(&self) -> &str { 22 | "packages-without-package-json" 23 | } 24 | 25 | fn level(&self) -> IssueLevel { 26 | match self.fixed { 27 | true => IssueLevel::Fixed, 28 | false => IssueLevel::Warning, 29 | } 30 | } 31 | 32 | fn message(&self) -> String { 33 | format!(" {}/package.json doesn't exist.", self.package) 34 | } 35 | 36 | fn why(&self) -> Cow<'static, str> { 37 | Cow::Borrowed("All packages matching the workspace should have a package.json file.") 38 | } 39 | 40 | fn fix(&mut self, _package_type: &PackageType) -> Result<()> { 41 | let path = PathBuf::from(&self.package).join("package.json"); 42 | let package_name = path 43 | .parent() 44 | .unwrap() 45 | .file_name() 46 | .unwrap() 47 | .to_str() 48 | .unwrap(); 49 | 50 | let value = serde_json::json!({ 51 | "name": package_name, 52 | "version": "0.0.0", 53 | "private": true, 54 | }); 55 | 56 | let value = serde_json::to_string_pretty(&value)?; 57 | fs::write(path, value)?; 58 | 59 | self.fixed = true; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod test { 67 | use super::*; 68 | 69 | #[test] 70 | fn test() { 71 | let issue = PackagesWithoutPackageJsonIssue::new("test".to_string()); 72 | 73 | assert_eq!(issue.name(), "packages-without-package-json"); 74 | assert_eq!(issue.level(), IssueLevel::Warning); 75 | 76 | colored::control::set_override(false); 77 | assert_eq!(issue.message(), " test/package.json doesn't exist."); 78 | assert_eq!( 79 | issue.why(), 80 | "All packages matching the workspace should have a package.json file." 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/rules/root_package_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel}; 2 | use colored::Colorize; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Debug)] 6 | pub struct RootPackageDependenciesIssue; 7 | 8 | impl RootPackageDependenciesIssue { 9 | pub fn new() -> Box { 10 | Box::new(Self) 11 | } 12 | } 13 | 14 | impl Issue for RootPackageDependenciesIssue { 15 | fn name(&self) -> &str { 16 | "root-package-dependencies" 17 | } 18 | 19 | fn level(&self) -> IssueLevel { 20 | IssueLevel::Warning 21 | } 22 | 23 | fn message(&self) -> String { 24 | format!( 25 | r#" │ {{ 26 | │ "{}": "{}", {} 27 | │ ... 28 | {} "{}": {{ {} 29 | {} ... 30 | {} }}, 31 | │ ... 32 | {} "{}": {{ {} 33 | {} ... 34 | {} }} 35 | │ }}"#, 36 | "private".white(), 37 | "true".white(), 38 | "← root package is private...".blue(), 39 | "-".red(), 40 | "dependencies".white(), 41 | "← but has dependencies...".red(), 42 | "-".red(), 43 | "-".red(), 44 | "+".green(), 45 | "devDependencies".white(), 46 | "← instead of devDependencies.".green(), 47 | "+".green(), 48 | "+".green(), 49 | ) 50 | .bright_black() 51 | .to_string() 52 | } 53 | 54 | fn why(&self) -> Cow<'static, str> { 55 | Cow::Borrowed("The root package.json is private and should only have devDependencies. Declare dependencies in each package.") 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | use super::*; 62 | 63 | #[test] 64 | fn test() { 65 | let issue = RootPackageDependenciesIssue::new(); 66 | 67 | assert_eq!(issue.name(), "root-package-dependencies"); 68 | assert_eq!(issue.level(), IssueLevel::Warning); 69 | 70 | colored::control::set_override(false); 71 | insta::assert_snapshot!(issue.message()); 72 | assert_eq!( 73 | issue.why(), 74 | "The root package.json is private and should only have devDependencies. Declare dependencies in each package." 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/rules/root_package_manager_field.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel}; 2 | use colored::Colorize; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Debug)] 6 | pub struct RootPackageManagerFieldIssue; 7 | 8 | impl RootPackageManagerFieldIssue { 9 | pub fn new() -> Box { 10 | Box::new(Self) 11 | } 12 | } 13 | 14 | impl Issue for RootPackageManagerFieldIssue { 15 | fn name(&self) -> &str { 16 | "root-package-manager-field" 17 | } 18 | 19 | fn level(&self) -> IssueLevel { 20 | IssueLevel::Error 21 | } 22 | 23 | fn message(&self) -> String { 24 | format!( 25 | r#" │ {{ 26 | {} "{}": "..." {} 27 | │ }}"#, 28 | "+".green(), 29 | "packageManager".white(), 30 | "← missing packageManager field.".green(), 31 | ) 32 | .bright_black() 33 | .to_string() 34 | } 35 | 36 | fn why(&self) -> Cow<'static, str> { 37 | Cow::Borrowed("The root package.json should specify the package manager and version to use. Useful for tools like corepack.") 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod test { 43 | use super::*; 44 | 45 | #[test] 46 | fn test() { 47 | let issue = RootPackageManagerFieldIssue::new(); 48 | 49 | assert_eq!(issue.name(), "root-package-manager-field"); 50 | assert_eq!(issue.level(), IssueLevel::Error); 51 | 52 | colored::control::set_override(false); 53 | insta::assert_snapshot!(issue.message()); 54 | assert_eq!( 55 | issue.why(), 56 | "The root package.json should specify the package manager and version to use. Useful for tools like corepack." 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/rules/root_package_private_field.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use crate::json; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use std::{borrow::Cow, fs, path::PathBuf}; 6 | 7 | #[derive(Debug)] 8 | pub struct RootPackagePrivateFieldIssue { 9 | fixed: bool, 10 | } 11 | 12 | impl RootPackagePrivateFieldIssue { 13 | pub fn new() -> Box { 14 | Box::new(Self { fixed: false }) 15 | } 16 | } 17 | 18 | impl Issue for RootPackagePrivateFieldIssue { 19 | fn name(&self) -> &str { 20 | "root-package-private-field" 21 | } 22 | 23 | fn level(&self) -> IssueLevel { 24 | match self.fixed { 25 | true => IssueLevel::Fixed, 26 | false => IssueLevel::Error, 27 | } 28 | } 29 | 30 | fn message(&self) -> String { 31 | format!( 32 | r#" │ {{ 33 | {} "{}": "{}" {} 34 | │ }}"#, 35 | "+".green(), 36 | "private".white(), 37 | "true".white(), 38 | "← missing private field.".green(), 39 | ) 40 | .bright_black() 41 | .to_string() 42 | } 43 | 44 | fn why(&self) -> Cow<'static, str> { 45 | Cow::Borrowed("The root package.json should be private to prevent accidentaly publishing it to a registry.") 46 | } 47 | 48 | fn fix(&mut self, package_type: &PackageType) -> Result<()> { 49 | if let PackageType::Root = package_type { 50 | let path = PathBuf::from("package.json"); 51 | let value = fs::read_to_string(&path)?; 52 | let (mut value, indent, lineending) = json::deserialize::(&value)?; 53 | 54 | value 55 | .as_object_mut() 56 | .unwrap() 57 | .insert("private".to_string(), serde_json::Value::Bool(true)); 58 | 59 | let value = json::serialize(&value, indent, lineending)?; 60 | fs::write(path, value)?; 61 | 62 | self.fixed = true; 63 | } 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use super::*; 72 | 73 | #[test] 74 | fn test() { 75 | let issue = RootPackagePrivateFieldIssue::new(); 76 | 77 | assert_eq!(issue.name(), "root-package-private-field"); 78 | assert_eq!(issue.level(), IssueLevel::Error); 79 | } 80 | 81 | #[test] 82 | fn private_field_not_set() { 83 | let issue = RootPackagePrivateFieldIssue::new(); 84 | 85 | colored::control::set_override(false); 86 | insta::assert_snapshot!(issue.message()); 87 | 88 | assert_eq!(issue.why(), "The root package.json should be private to prevent accidentaly publishing it to a registry."); 89 | } 90 | 91 | #[test] 92 | fn private_field_set_not_true() { 93 | let issue = RootPackagePrivateFieldIssue::new(); 94 | 95 | colored::control::set_override(false); 96 | insta::assert_snapshot!(issue.message()); 97 | assert_eq!(issue.why(), "The root package.json should be private to prevent accidentaly publishing it to a registry."); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__empty_dependencies__test__dependency_kind-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/empty_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | - "devDependencies": {} ← field is empty. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__empty_dependencies__test__dependency_kind-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/empty_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | - "peerDependencies": {} ← field is empty. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__empty_dependencies__test__dependency_kind-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/empty_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | - "optionalDependencies": {} ← field is empty. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__empty_dependencies__test__dependency_kind.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/empty_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | - "dependencies": {} ← field is empty. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__dedupe.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./packages 6 | package-b 3.1.6 ↑ highest 7 | package-c 3.1.6 ↑ highest 8 | ./package-a 1.2.3 ↓ lowest 9 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__exact_and_range.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./apps 6 | package-a 5.6.3 ↑ highest 7 | ./packages 8 | package-c ~3.1.6 ∼ between 9 | ./apps 10 | package-b ^1.2.3 ↓ lowest 11 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__order_multiple.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./apps 6 | package-a 5.6.3 ↑ highest 7 | ./packages 8 | package-c 3.1.6 ∼ between 9 | ./apps 10 | package-b 1.2.3 ↓ lowest 11 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__order_prerelease.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./packages 6 | package-c 5.0.0-next.6 ↑ highest 7 | ./apps 8 | package-a 5.0.0-next.4 ∼ between 9 | package-b 5.0.0-next.3 ↓ lowest 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__order_single.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./package-a 1.2.3 ↑ highest 6 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__multiple_dependency_versions__test__root.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/multiple_dependency_versions.rs 3 | expression: issue.message() 4 | --- 5 | ./ 5.6.3 ↑ highest 6 | ./packages 7 | package-b 3.1.6 ∼ between 8 | package-a 1.2.3 ↓ lowest 9 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__non_existant_packages__test__package_workspace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/non_existant_packages.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "workspaces": [ ← Workspace has paths defined... 7 | │ "apps/*", 8 | │ "packages/*", 9 | - "empty/*", ← but this one doesn't match any package 10 | - "docs", ← but this one doesn't match any package 11 | │ ], 12 | │ } 13 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__non_existant_packages__test__pnpm_workspace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/non_existant_packages.rs 3 | expression: issue.message() 4 | --- 5 | │ packages: ← Workspace has paths defined... 6 | │ - 'apps/*' 7 | │ - 'packages/*' 8 | - - 'empty/*' ← but this one doesn't match any package 9 | - - 'docs' ← but this one doesn't match any package 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__non_existant_packages__test__test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/non_existant_packages.rs 3 | expression: issue.message() 4 | --- 5 | │ packages: ← Workspace has paths defined... 6 | │ - 'apps/*' 7 | │ - 'packages/*' 8 | - - 'empty/*' ← but this one doesn't match any package 9 | - - 'docs' ← but this one doesn't match any package 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__root_package_dependencies__test__test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/root_package_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "private": "true", ← root package is private... 7 | │ ... 8 | - "dependencies": { ← but has dependencies... 9 | - ... 10 | - }, 11 | │ ... 12 | + "devDependencies": { ← instead of devDependencies. 13 | + ... 14 | + } 15 | │ } 16 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__root_package_manager_field__test__test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/root_package_manager_field.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | + "packageManager": "..." ← missing packageManager field. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__root_package_private_field__test__private_field_not_set.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/root_package_private_field.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | + "private": "true" ← missing private field. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__root_package_private_field__test__private_field_set_not_true.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/root_package_private_field.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | + "private": "true" ← missing private field. 7 | │ } 8 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__types_in_dependencies__test__test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/types_in_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "private": "true", ← package is private... 7 | │ ... 8 | - "dependencies": { ← but has @types/* in dependencies... 9 | - "@types/react": "...", 10 | - "@types/react-dom": "...", 11 | - }, 12 | │ ... 13 | + "devDependencies": { ← instead of devDependencies. 14 | + "@types/react": "...", 15 | + "@types/react-dom": "...", 16 | + } 17 | │ } 18 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__unordered_dependencies__test__dependency_kind-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/unordered_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "devDependencies": { 7 | ~ ... ← keys aren't sorted. 8 | │ } 9 | │ } 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__unordered_dependencies__test__dependency_kind-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/unordered_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "peerDependencies": { 7 | ~ ... ← keys aren't sorted. 8 | │ } 9 | │ } 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__unordered_dependencies__test__dependency_kind-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/unordered_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "optionalDependencies": { 7 | ~ ... ← keys aren't sorted. 8 | │ } 9 | │ } 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__unordered_dependencies__test__dependency_kind.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/unordered_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "dependencies": { 7 | ~ ... ← keys aren't sorted. 8 | │ } 9 | │ } 10 | -------------------------------------------------------------------------------- /src/rules/snapshots/sherif__rules__unsync_similar_dependencies__tests__basic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/rules/unsync_similar_dependencies.rs 3 | expression: issue.message() 4 | --- 5 | │ { 6 | │ "dependencies": { 7 | ~ "react": "1.0.0", 8 | ~ "react-dom": "2.0.0" 9 | │ } 10 | │ } 11 | -------------------------------------------------------------------------------- /src/rules/types_in_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::{Issue, IssueLevel, PackageType}; 2 | use crate::json; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use indexmap::IndexMap; 6 | use std::{borrow::Cow, fs, path::PathBuf}; 7 | 8 | #[derive(Debug)] 9 | pub struct TypesInDependenciesIssue { 10 | packages: Vec, 11 | fixed: bool, 12 | } 13 | 14 | impl TypesInDependenciesIssue { 15 | pub fn new(packages: Vec) -> Box { 16 | Box::new(Self { 17 | packages, 18 | fixed: false, 19 | }) 20 | } 21 | } 22 | 23 | impl Issue for TypesInDependenciesIssue { 24 | fn name(&self) -> &str { 25 | "types-in-dependencies" 26 | } 27 | 28 | fn level(&self) -> IssueLevel { 29 | match self.fixed { 30 | true => IssueLevel::Fixed, 31 | false => IssueLevel::Error, 32 | } 33 | } 34 | 35 | fn message(&self) -> String { 36 | let before = self 37 | .packages 38 | .iter() 39 | .map(|package| format!(r#" {} "{}": "...","#, "-".red(), package.white())) 40 | .collect::>() 41 | .join("\n"); 42 | 43 | let after = self 44 | .packages 45 | .iter() 46 | .map(|package| format!(r#" {} "{}": "...","#, "+".green(), package.white())) 47 | .collect::>() 48 | .join("\n"); 49 | 50 | format!( 51 | r#" │ {{ 52 | │ "{}": "{}", {} 53 | │ ... 54 | {} "{}": {{ {} 55 | {} 56 | {} }}, 57 | │ ... 58 | {} "{}": {{ {} 59 | {} 60 | {} }} 61 | │ }}"#, 62 | "private".white(), 63 | "true".white(), 64 | "← package is private...".blue(), 65 | "-".red(), 66 | "dependencies".white(), 67 | "← but has @types/* in dependencies...".red(), 68 | before, 69 | "-".red(), 70 | "+".green(), 71 | "devDependencies".white(), 72 | "← instead of devDependencies.".green(), 73 | after, 74 | "+".green(), 75 | ) 76 | .bright_black() 77 | .to_string() 78 | } 79 | 80 | fn why(&self) -> Cow<'static, str> { 81 | Cow::Borrowed("Private packages shouldn't have @types/* in dependencies.") 82 | } 83 | 84 | fn fix(&mut self, package_type: &PackageType) -> Result<()> { 85 | if let PackageType::Package(path) = package_type { 86 | let path = PathBuf::from(path).join("package.json"); 87 | let value = fs::read_to_string(&path)?; 88 | let (mut value, indent, lineending) = json::deserialize::(&value)?; 89 | 90 | let dependencies = value 91 | .get_mut("dependencies") 92 | .unwrap() 93 | .as_object_mut() 94 | .unwrap(); 95 | let mut dependencies_to_add = IndexMap::new(); 96 | 97 | for package in &self.packages { 98 | if let Some(version) = dependencies.remove(package) { 99 | dependencies_to_add.insert(package.clone(), version); 100 | } 101 | } 102 | 103 | // The package.json file might not have a devDependencies field. 104 | let dev_dependencies = match value.get_mut("devDependencies") { 105 | Some(dev_dependencies) => dev_dependencies, 106 | None => { 107 | value.as_object_mut().unwrap().insert( 108 | "devDependencies".into(), 109 | serde_json::Value::Object(serde_json::Map::new()), 110 | ); 111 | 112 | value.get_mut("devDependencies").unwrap() 113 | } 114 | }; 115 | 116 | let dev_dependencies = dev_dependencies.as_object_mut().unwrap(); 117 | 118 | for (package, version) in dependencies_to_add { 119 | dev_dependencies.insert(package, version); 120 | } 121 | 122 | let value = json::serialize(&value, indent, lineending)?; 123 | fs::write(path, value)?; 124 | 125 | self.fixed = true; 126 | } 127 | 128 | Ok(()) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod test { 134 | use super::*; 135 | 136 | #[test] 137 | fn test() { 138 | let issue = 139 | TypesInDependenciesIssue::new(vec!["@types/react".into(), "@types/react-dom".into()]); 140 | 141 | assert_eq!(issue.name(), "types-in-dependencies"); 142 | assert_eq!(issue.level(), IssueLevel::Error); 143 | 144 | colored::control::set_override(false); 145 | insta::assert_snapshot!(issue.message()); 146 | assert_eq!( 147 | issue.why(), 148 | "Private packages shouldn't have @types/* in dependencies." 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/rules/unordered_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::{empty_dependencies::DependencyKind, Issue, IssueLevel, PackageType}; 2 | use crate::json; 3 | use anyhow::Result; 4 | use colored::Colorize; 5 | use std::{borrow::Cow, fs, path::PathBuf}; 6 | 7 | #[derive(Debug)] 8 | pub struct UnorderedDependenciesIssue { 9 | dependency_kind: DependencyKind, 10 | fixed: bool, 11 | } 12 | 13 | impl UnorderedDependenciesIssue { 14 | pub fn new(dependency_kind: DependencyKind) -> Box { 15 | Box::new(Self { 16 | dependency_kind, 17 | fixed: false, 18 | }) 19 | } 20 | 21 | pub fn sort(&mut self, path: PathBuf) -> Result<()> { 22 | let value = fs::read_to_string(&path)?; 23 | let (mut value, indent, lineending) = json::deserialize::(&value)?; 24 | let dependency = self.dependency_kind.to_string(); 25 | 26 | if let Some(dependency_field) = value.get(&dependency) { 27 | if dependency_field.is_object() { 28 | let mut keys = dependency_field 29 | .as_object() 30 | .unwrap() 31 | .keys() 32 | .collect::>(); 33 | keys.sort(); 34 | 35 | let mut sorted = serde_json::Map::new(); 36 | for key in keys { 37 | sorted.insert(key.to_string(), dependency_field[key].clone()); 38 | } 39 | 40 | value 41 | .as_object_mut() 42 | .unwrap() 43 | .insert(dependency, serde_json::Value::Object(sorted)); 44 | 45 | let value = json::serialize(&value, indent, lineending)?; 46 | fs::write(path, value)?; 47 | 48 | self.fixed = true; 49 | } 50 | } 51 | 52 | Ok(()) 53 | } 54 | } 55 | 56 | impl Issue for UnorderedDependenciesIssue { 57 | fn name(&self) -> &str { 58 | "unordered-dependencies" 59 | } 60 | 61 | fn level(&self) -> IssueLevel { 62 | match self.fixed { 63 | true => IssueLevel::Fixed, 64 | false => IssueLevel::Error, 65 | } 66 | } 67 | 68 | fn message(&self) -> String { 69 | format!( 70 | r#" │ {{ 71 | │ "{}": {{ 72 | {} ... {} 73 | │ }} 74 | │ }}"#, 75 | self.dependency_kind.to_string().white(), 76 | "~".blue(), 77 | "← keys aren't sorted.".blue(), 78 | ) 79 | .bright_black() 80 | .to_string() 81 | } 82 | 83 | fn why(&self) -> Cow<'static, str> { 84 | Cow::Owned(format!( 85 | "{} should be ordered alphabetically.", 86 | self.dependency_kind 87 | )) 88 | } 89 | 90 | fn fix(&mut self, package_type: &PackageType) -> Result<()> { 91 | if let PackageType::Package(path) = package_type { 92 | let path = PathBuf::from(path).join("package.json"); 93 | self.sort(path)?; 94 | } else if let PackageType::Root = package_type { 95 | let path = PathBuf::from("package.json"); 96 | self.sort(path)?; 97 | } 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod test { 105 | use super::*; 106 | 107 | #[test] 108 | fn test() { 109 | let issue = UnorderedDependenciesIssue::new(DependencyKind::Dependencies); 110 | 111 | assert_eq!(issue.name(), "unordered-dependencies"); 112 | assert_eq!(issue.level(), IssueLevel::Error); 113 | assert_eq!( 114 | issue.why(), 115 | "dependencies should be ordered alphabetically." 116 | ); 117 | } 118 | 119 | #[test] 120 | fn test_dependency_kind() { 121 | colored::control::set_override(false); 122 | 123 | let issue = UnorderedDependenciesIssue::new(DependencyKind::Dependencies); 124 | insta::assert_snapshot!(issue.message()); 125 | 126 | let issue = UnorderedDependenciesIssue::new(DependencyKind::DevDependencies); 127 | insta::assert_snapshot!(issue.message()); 128 | 129 | let issue = UnorderedDependenciesIssue::new(DependencyKind::PeerDependencies); 130 | insta::assert_snapshot!(issue.message()); 131 | 132 | let issue = UnorderedDependenciesIssue::new(DependencyKind::OptionalDependencies); 133 | insta::assert_snapshot!(issue.message()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/rules/unsync_similar_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::Issue; 2 | use crate::packages::semversion::SemVersion; 3 | use colored::Colorize; 4 | use indexmap::IndexMap; 5 | use std::{borrow::Cow, fmt::Display, hash::Hash}; 6 | 7 | #[derive(Debug, Hash, PartialEq, Eq, Clone)] 8 | pub enum SimilarDependency { 9 | Trpc, 10 | React, 11 | NextJS, 12 | Storybook, 13 | Turborepo, 14 | TanstackQuery, 15 | Prisma, 16 | TypescriptEslint, 17 | EslintStylistic, 18 | Playwright, 19 | Lexical, 20 | } 21 | 22 | impl Display for SimilarDependency { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | match self { 25 | Self::Trpc => write!(f, "tRPC"), 26 | Self::React => write!(f, "React"), 27 | Self::NextJS => write!(f, "Next.js"), 28 | Self::Storybook => write!(f, "Storybook"), 29 | Self::Turborepo => write!(f, "Turborepo"), 30 | Self::TanstackQuery => write!(f, "Tanstack Query"), 31 | Self::Prisma => write!(f, "Prisma"), 32 | Self::TypescriptEslint => write!(f, "typescript-eslint"), 33 | Self::EslintStylistic => write!(f, "ESLint Stylistic"), 34 | Self::Playwright => write!(f, "Playwright"), 35 | Self::Lexical => write!(f, "Lexical"), 36 | } 37 | } 38 | } 39 | 40 | impl TryFrom<&str> for SimilarDependency { 41 | type Error = anyhow::Error; 42 | 43 | fn try_from(value: &str) -> Result { 44 | match value { 45 | "@trpc/client" | "@trpc/server" | "@trpc/next" | "@trpc/react-query" => Ok(Self::Trpc), 46 | "react" | "react-dom" => Ok(Self::React), 47 | "eslint-config-next" 48 | | "@next/eslint-plugin-next" 49 | | "@next/font" 50 | | "@next/bundle-analyzer" 51 | | "@next/mdx" 52 | | "next" 53 | | "@next/third-parties" => Ok(Self::NextJS), 54 | "eslint-config-turbo" 55 | | "eslint-plugin-turbo" 56 | | "@turbo/gen" 57 | | "turbo-ignore" 58 | | "turbo" => Ok(Self::Turborepo), 59 | "sb" 60 | | "storybook" 61 | | "@storybook/codemod" 62 | | "@storybook/cli" 63 | | "@storybook/channels" 64 | | "@storybook/addon-actions" 65 | | "@storybook/addon-links" 66 | | "@storybook/react" 67 | | "@storybook/react-native" 68 | | "@storybook/components" 69 | | "@storybook/addon-backgrounds" 70 | | "@storybook/addon-viewport" 71 | | "@storybook/angular" 72 | | "@storybook/addon-a11y" 73 | | "@storybook/addon-jest" 74 | | "@storybook/client-logger" 75 | | "@storybook/node-logger" 76 | | "@storybook/core" 77 | | "@storybook/addon-storysource" 78 | | "@storybook/html" 79 | | "@storybook/core-events" 80 | | "@storybook/svelte" 81 | | "@storybook/ember" 82 | | "@storybook/addon-ondevice-backgrounds" 83 | | "@storybook/addon-ondevice-notes" 84 | | "@storybook/preact" 85 | | "@storybook/theming" 86 | | "@storybook/router" 87 | | "@storybook/addon-docs" 88 | | "@storybook/addon-ondevice-actions" 89 | | "@storybook/source-loader" 90 | | "@storybook/preset-create-react-app" 91 | | "@storybook/web-components" 92 | | "@storybook/addon-essentials" 93 | | "@storybook/server" 94 | | "@storybook/addon-toolbars" 95 | | "@storybook/addon-controls" 96 | | "@storybook/core-common" 97 | | "@storybook/builder-webpack5" 98 | | "@storybook/core-server" 99 | | "@storybook/csf-tools" 100 | | "@storybook/addon-measure" 101 | | "@storybook/addon-outline" 102 | | "@storybook/addon-ondevice-controls" 103 | | "@storybook/instrumenter" 104 | | "@storybook/addon-interactions" 105 | | "@storybook/docs-tools" 106 | | "@storybook/builder-vite" 107 | | "@storybook/telemetry" 108 | | "@storybook/core-webpack" 109 | | "@storybook/preset-html-webpack" 110 | | "@storybook/preset-preact-webpack" 111 | | "@storybook/preset-svelte-webpack" 112 | | "@storybook/preset-react-webpack" 113 | | "@storybook/html-webpack5" 114 | | "@storybook/preact-webpack5" 115 | | "@storybook/svelte-webpack5" 116 | | "@storybook/web-components-webpack5" 117 | | "@storybook/preset-server-webpack" 118 | | "@storybook/react-webpack5" 119 | | "@storybook/server-webpack5" 120 | | "@storybook/addon-highlight" 121 | | "@storybook/blocks" 122 | | "@storybook/builder-manager" 123 | | "@storybook/react-vite" 124 | | "@storybook/svelte-vite" 125 | | "@storybook/web-components-vite" 126 | | "@storybook/nextjs" 127 | | "@storybook/types" 128 | | "@storybook/manager" 129 | | "@storybook/csf-plugin" 130 | | "@storybook/preview" 131 | | "@storybook/manager-api" 132 | | "@storybook/preview-api" 133 | | "@storybook/html-vite" 134 | | "@storybook/sveltekit" 135 | | "@storybook/preact-vite" 136 | | "@storybook/addon-mdx-gfm" 137 | | "@storybook/react-dom-shim" 138 | | "create-storybook" 139 | | "@storybook/addon-onboarding" 140 | | "@storybook/react-native-theming" 141 | | "@storybook/addon-themes" 142 | | "@storybook/test" 143 | | "@storybook/react-native-ui" 144 | | "@storybook/experimental-nextjs-vite" 145 | | "@storybook/experimental-addon-test" 146 | | "@storybook/react-native-web-vite" => Ok(Self::Storybook), 147 | "@tanstack/eslint-plugin-query" 148 | | "@tanstack/query-async-storage-persister" 149 | | "@tanstack/query-broadcast-client-experimental" 150 | | "@tanstack/query-core" 151 | | "@tanstack/query-devtools" 152 | | "@tanstack/query-persist-client-core" 153 | | "@tanstack/query-sync-storage-persister" 154 | | "@tanstack/react-query" 155 | | "@tanstack/react-query-devtools" 156 | | "@tanstack/react-query-persist-client" 157 | | "@tanstack/react-query-next-experimental" 158 | | "@tanstack/solid-query" 159 | | "@tanstack/solid-query-devtools" 160 | | "@tanstack/solid-query-persist-client" 161 | | "@tanstack/svelte-query" 162 | | "@tanstack/svelte-query-devtools" 163 | | "@tanstack/svelte-query-persist-client" 164 | | "@tanstack/vue-query" 165 | | "@tanstack/vue-query-devtools" 166 | | "@tanstack/angular-query-devtools-experimental" 167 | | "@tanstack/angular-query-experimental" => Ok(Self::TanstackQuery), 168 | "prisma" 169 | | "@prisma/client" 170 | | "@prisma/instrumentation" 171 | | "@prisma/adapter-pg" 172 | | "@prisma/adapter-neon" 173 | | "@prisma/adapter-planetscale" 174 | | "@prisma/adapter-d1" 175 | | "@prisma/adapter-libsql" 176 | | "@prisma/adapter-pg-worker" 177 | | "@prisma/pg-worker" => Ok(Self::Prisma), 178 | "typescript-eslint" 179 | | "@typescript-eslint/eslint-plugin" 180 | | "@typescript-eslint/parser" => Ok(Self::TypescriptEslint), 181 | "@stylistic/eslint-plugin-js" 182 | | "@stylistic/eslint-plugin-ts" 183 | | "@stylistic/eslint-plugin-migrate" 184 | | "@stylistic/eslint-plugin" 185 | | "@stylistic/eslint-plugin-jsx" 186 | | "@stylistic/eslint-plugin-plus" => Ok(Self::EslintStylistic), 187 | "playwright" | "@playwright/test" => Ok(Self::Playwright), 188 | "lexical" 189 | | "@lexical/clipboard" 190 | | "@lexical/code" 191 | | "@lexical/devtools-core" 192 | | "@lexical/dragon" 193 | | "@lexical/eslint-plugin" 194 | | "@lexical/file" 195 | | "@lexical/hashtag" 196 | | "@lexical/headless" 197 | | "@lexical/history" 198 | | "@lexical/html" 199 | | "@lexical/link" 200 | | "@lexical/list" 201 | | "@lexical/mark" 202 | | "@lexical/markdown" 203 | | "@lexical/offset" 204 | | "@lexical/overflow" 205 | | "@lexical/plain-text" 206 | | "@lexical/react" 207 | | "@lexical/rich-text" 208 | | "@lexical/selection" 209 | | "@lexical/table" 210 | | "@lexical/text" 211 | | "@lexical/utils" 212 | | "@lexical/yjs" => Ok(Self::Lexical), 213 | _ => Err(anyhow::anyhow!("Unknown similar dependency")), 214 | } 215 | } 216 | } 217 | 218 | #[derive(Debug)] 219 | pub struct UnsyncSimilarDependenciesIssue { 220 | r#type: SimilarDependency, 221 | versions: IndexMap, 222 | fixed: bool, 223 | } 224 | 225 | impl UnsyncSimilarDependenciesIssue { 226 | pub fn new(r#type: SimilarDependency, versions: IndexMap) -> Box { 227 | Box::new(Self { 228 | r#type, 229 | versions, 230 | fixed: false, 231 | }) 232 | } 233 | } 234 | 235 | impl Issue for UnsyncSimilarDependenciesIssue { 236 | fn name(&self) -> &str { 237 | "unsync-similar-dependencies" 238 | } 239 | 240 | fn level(&self) -> super::IssueLevel { 241 | match self.fixed { 242 | true => super::IssueLevel::Fixed, 243 | false => super::IssueLevel::Error, 244 | } 245 | } 246 | 247 | fn message(&self) -> String { 248 | let deps = self 249 | .versions 250 | .iter() 251 | .map(|(version, dependency)| { 252 | format!( 253 | r#" {} "{}": "{}""#, 254 | "~".yellow(), 255 | dependency.white(), 256 | version.to_string().yellow() 257 | ) 258 | }) 259 | .collect::>() 260 | .join(",\n"); 261 | 262 | format!( 263 | r#" │ {{ 264 | │ "{}": {{ 265 | {} 266 | │ }} 267 | │ }}"#, 268 | "dependencies".white(), 269 | deps, 270 | ) 271 | .bright_black() 272 | .to_string() 273 | } 274 | 275 | fn why(&self) -> Cow<'static, str> { 276 | Cow::Owned(format!( 277 | "Similar {} dependencies should use the same version.", 278 | self.r#type 279 | )) 280 | } 281 | 282 | fn fix(&mut self, _package_type: &super::PackageType) -> anyhow::Result<()> { 283 | Ok(()) 284 | } 285 | } 286 | 287 | #[cfg(test)] 288 | mod tests { 289 | use super::*; 290 | use crate::rules::IssueLevel; 291 | 292 | #[test] 293 | fn test() { 294 | let versions = vec![ 295 | (SemVersion::parse("1.0.0").unwrap(), "react".to_string()), 296 | (SemVersion::parse("2.0.0").unwrap(), "react-dom".to_string()), 297 | ] 298 | .into_iter() 299 | .collect(); 300 | 301 | let issue = UnsyncSimilarDependenciesIssue::new(SimilarDependency::React, versions); 302 | 303 | assert_eq!(issue.name(), "unsync-similar-dependencies"); 304 | assert_eq!(issue.level(), IssueLevel::Error); 305 | assert_eq!(issue.versions.len(), 2); 306 | assert_eq!( 307 | issue.why(), 308 | "Similar React dependencies should use the same version." 309 | ); 310 | } 311 | 312 | #[test] 313 | fn basic() { 314 | let versions = vec![ 315 | (SemVersion::parse("1.0.0").unwrap(), "react".to_string()), 316 | (SemVersion::parse("2.0.0").unwrap(), "react-dom".to_string()), 317 | ] 318 | .into_iter() 319 | .collect(); 320 | 321 | let issue = UnsyncSimilarDependenciesIssue::new(SimilarDependency::React, versions); 322 | 323 | colored::control::set_override(false); 324 | insta::assert_snapshot!(issue.message()); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 43 | // "resolveJsonModule": true, /* Enable importing .json files. */ 44 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 45 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 46 | 47 | /* JavaScript Support */ 48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | 75 | /* Interop Constraints */ 76 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 77 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 78 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 92 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 93 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 94 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 95 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 96 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 97 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 98 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 99 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 100 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 101 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 102 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 103 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 104 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 105 | 106 | /* Completeness */ 107 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 108 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 109 | } 110 | } 111 | --------------------------------------------------------------------------------