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