├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── compiler-facade.spec.ts ├── constants.ts ├── fixtures │ ├── alphabetically-sorted-dependencies │ │ └── package.json │ ├── better-alternative │ │ └── package.json │ ├── controlled-versions │ │ └── package.json │ ├── duplicate-dependencies │ │ └── package.json │ ├── no-missing-types │ │ ├── made-up-package │ │ │ └── package.json │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── typings │ │ │ └── made-up-package.d.ts │ └── valid-versions │ │ └── package.json ├── has-types.spec.ts ├── integration_tests.spec.ts ├── parse-for-eslint.spec.ts ├── rules │ ├── alphabetically-sorted-dependencies.spec.ts │ ├── better-alternative.spec.ts │ ├── controlled-versions.spec.ts │ ├── duplicate-dependencies.spec.ts │ ├── no-missing-types.spec.ts │ └── valid-versions.spec.ts ├── to-controlled-semver.spec.ts └── utils.spec.ts ├── docs └── rules │ ├── alphabetically-sorted-dependencies.md │ ├── better-alternative.md │ ├── controlled-versions.md │ ├── duplicate-dependencies.md │ ├── no-missing-types.md │ └── valid-versions.md ├── index.ts ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── compiler-facade.ts ├── has-types.ts ├── parse-for-eslint.ts ├── rules │ ├── alphabetically-sorted-dependencies.ts │ ├── better-alternative.ts │ ├── constants.ts │ ├── controlled-versions.ts │ ├── duplicate-dependencies.ts │ ├── index.ts │ ├── no-missing-types.ts │ └── valid-versions.ts ├── to-controlled-semver.ts ├── types.ts └── utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | __tests__/fixtures/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["self"], 3 | "parser": ".", 4 | "rules": { 5 | "self/no-missing-types": [ 6 | "error", 7 | { "excludePatterns": ["eslint-plugin-self"] } 8 | ], 9 | "self/alphabetically-sorted-dependencies": "error", 10 | "self/controlled-versions": ["error", { "granularity": "patch" }] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: "test with node version ${{ matrix.node-version }}" 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x] 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: "Checkout" 17 | uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | __tests__/**/package-lock.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | #### 1.0.14 4 | - controlled-versions: ignore git and file links [`#5`](https://github.com/idan-at/eslint-plugin-package-json-dependencies/pull/5) 5 | 6 | #### 1.0.15 7 | - Switch to [MIT license](https://github.com/idan-at/eslint-plugin-package-json-dependencies/pull/7) 8 | 9 | #### 1.0.16 10 | - Add prepublish validation 11 | 12 | #### 1.0.17 13 | - Upgrade dependencies 14 | 15 | #### 1.0.18 16 | - Update semver dependency in order to fix audit issue (#12) 17 | - feat: handle ranges (#13) 18 | - feat: allow setting granularity per dependency type (#14) 19 | - Upgrade dependencies 20 | 21 | #### 1.0.19 22 | - feat: valid-versions rule (#18) 23 | 24 | #### 1.0.20 25 | - fix: resolveDistTag throws when tag does not exist 26 | - feat: duplicate-dependencies rule 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present eslint-plugin-package-json-dependencies contributors 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 | # eslint-plugin-package-json-dependencies 2 | 3 | [![npm version](https://badge.fury.io/js/eslint-plugin-package-json-dependencies.svg)](https://badge.fury.io/js/eslint-plugin-package-json-dependencies) 4 | [![Actions Status: build](https://github.com/idan-at/eslint-plugin-package-json-dependencies/workflows/test/badge.svg)](https://github.com/idan-at/eslint-plugin-package-json-dependencies/actions?query=workflow%3A"test") 5 | 6 | This plugin contains rules for maintaining a valid, consistent `package.json` dependency setup. 7 | 8 | # Installation 9 | 10 | ```bash 11 | npm install --save-dev eslint eslint-plugin-package-json-dependencies 12 | ``` 13 | 14 | # Usage 15 | 16 | 1. Add the plugin and its parser to your eslint [config file](https://eslint.org/docs/user-guide/configuring/configuration-files) `overrides` section: 17 | 18 | ```js 19 | // eslintrc.json 20 | { 21 | "overrides": [ 22 | { 23 | "files": ["*.json"], 24 | "parser": "eslint-plugin-package-json-dependencies", 25 | "plugins": ["package-json-dependencies"] 26 | } 27 | ] 28 | } 29 | ``` 30 | 31 | 2. Apply the specific rules applicable to your repo, e.g.: 32 | 33 | ```js 34 | // eslintrc.json 35 | { 36 | "rules": { 37 | "package-json-dependencies/no-missing-types": "error" 38 | } 39 | } 40 | ``` 41 | 42 | # Available Rules 43 | 44 | - [no-missing-types](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/no-missing-types.md) 45 | - [alphabetically-sorted-dependencies](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/alphabetically-sorted-dependencies.md) 46 | - [controlled-versions](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/controlled-versions.md) 47 | - [better-alternative](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/better-alternative.md) 48 | - [valid-versions](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/valid-versions.md) 49 | - [duplicate-dependencies](https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/duplicate-dependencies.md) 50 | 51 | # Development 52 | 53 | - `npm install` 54 | - `npm test` 55 | - `npm run format` 56 | -------------------------------------------------------------------------------- /__tests__/compiler-facade.spec.ts: -------------------------------------------------------------------------------- 1 | import { resolveTypeRoots } from "../src/compiler-facade"; 2 | import { FIXTURES_ROOT_PATH, NO_MISSING_TYPES_FIXTURE_PATH } from "./constants"; 3 | import path from "path"; 4 | 5 | describe("compiler facade", () => { 6 | test("resolveTypeRoots", () => { 7 | expect(resolveTypeRoots(FIXTURES_ROOT_PATH)).toStrictEqual([]); 8 | expect(resolveTypeRoots(NO_MISSING_TYPES_FIXTURE_PATH)).toStrictEqual([ 9 | path.resolve(NO_MISSING_TYPES_FIXTURE_PATH, "node_modules", "@types"), 10 | path.resolve(NO_MISSING_TYPES_FIXTURE_PATH, "typings"), 11 | ]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const FIXTURES_ROOT_PATH = path.resolve("./__tests__/fixtures"); 4 | const NO_MISSING_TYPES_FIXTURE_PATH = path.join( 5 | FIXTURES_ROOT_PATH, 6 | "no-missing-types", 7 | ); 8 | const ALPHABETICALLY_SORTED_DEPENDENCIES_FIXTURES_PATH = path.join( 9 | FIXTURES_ROOT_PATH, 10 | "alphabetically-sorted-dependencies", 11 | ); 12 | const CONTROLLED_VERSIONS_FIXTURE_PATH = path.join( 13 | FIXTURES_ROOT_PATH, 14 | "controlled-versions", 15 | ); 16 | const BETTER_ALTERNATIVE_FIXTURES_PATH = path.join( 17 | FIXTURES_ROOT_PATH, 18 | "better-alternative", 19 | ); 20 | const VALID_VERSIONS_FIXTURES_PATH = path.join( 21 | FIXTURES_ROOT_PATH, 22 | "valid-versions", 23 | ); 24 | const DUPLICATE_DEPENDENCIES_FIXTURES_PATH = path.join( 25 | FIXTURES_ROOT_PATH, 26 | "duplicate-dependencies", 27 | ); 28 | 29 | export { 30 | FIXTURES_ROOT_PATH, 31 | NO_MISSING_TYPES_FIXTURE_PATH, 32 | ALPHABETICALLY_SORTED_DEPENDENCIES_FIXTURES_PATH, 33 | CONTROLLED_VERSIONS_FIXTURE_PATH, 34 | BETTER_ALTERNATIVE_FIXTURES_PATH, 35 | VALID_VERSIONS_FIXTURES_PATH, 36 | DUPLICATE_DEPENDENCIES_FIXTURES_PATH, 37 | }; 38 | -------------------------------------------------------------------------------- /__tests__/fixtures/alphabetically-sorted-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alphabetically-sorted-dependencies", 3 | "description": "alphabetically-sorted-dependencies fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "b": "~4.17.0", 7 | "a": "~0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/better-alternative/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-alternative", 3 | "description": "better-alternative fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "foo": "1.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/fixtures/controlled-versions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controlled-versions", 3 | "description": "controlled-versions fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "foo": "latest", 7 | "bar": "^4.17.0", 8 | "baz": "~4.17.0", 9 | "ignored": "latest" 10 | }, 11 | "devDependencies": { 12 | "baz": "~1.0.0" 13 | }, 14 | "peerDependencies": { 15 | "bay": "~1.2.3" 16 | }, 17 | "optionalDependencies": { 18 | "bak": "~1.2.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /__tests__/fixtures/duplicate-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duplicate-dependencies", 3 | "description": "duplicate-dependencies fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "foo": "1.0.0", 7 | "ignored": "1.0.0" 8 | }, 9 | "devDependencies": { 10 | "foo": "1.0.0", 11 | "ignored": "1.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/no-missing-types/made-up-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "made-up-package", 3 | "description": "a package without definitions for tests", 4 | "license": "ISC" 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/fixtures/no-missing-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-missing-types", 3 | "description": "no-missing-types fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "lodash": "~4.17.0", 7 | "made-up-package": "file:./made-up-package", 8 | "streamifier": "~0.1.0" 9 | }, 10 | "devDependencies": { 11 | "@types/lodash": "~4.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/no-missing-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This is a comment to make sure tsconfig.json files with comments are loaded successfully. 3 | "compilerOptions": { 4 | "typeRoots": ["./node_modules/@types", "./typings"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/no-missing-types/typings/made-up-package.d.ts: -------------------------------------------------------------------------------- 1 | declare module "made-up-package" { 2 | export function something(): void; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/valid-versions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valid-versions", 3 | "description": "valid-versions fixture", 4 | "license": "ISC", 5 | "dependencies": { 6 | "foo": "workspace: *" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/has-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { hasTypes, isPackedTypesFile } from "../src/has-types"; 2 | import { NO_MISSING_TYPES_FIXTURE_PATH as cwd } from "./constants"; 3 | 4 | describe("hasTypes", () => { 5 | test("static checks", () => { 6 | expect(hasTypes(cwd, "package", ["@types/package"])).toBe(true); 7 | expect(hasTypes(cwd, "package", ["@types/another-package"])).toBe(false); 8 | expect(hasTypes(cwd, "@scope/package", ["@types/scope__package"])).toBe( 9 | true, 10 | ); 11 | expect( 12 | hasTypes(cwd, "@scope/package", ["@types/scope__another-package"]), 13 | ).toBe(false); 14 | }); 15 | 16 | // Those tests rely on the fact that both typescript and lodash are used within this package. 17 | test("dynamic checks", () => { 18 | expect(hasTypes(cwd, "typescript", [])).toBe(true); 19 | expect(hasTypes(cwd, "lodash", [])).toBe(false); 20 | expect(hasTypes(cwd, "made-up-package", [])).toBe(true); 21 | }); 22 | }); 23 | 24 | test("isPackedTypesFile", () => { 25 | expect(isPackedTypesFile("/path/to/@types/package/index.d.ts")).toBe(false); 26 | expect(isPackedTypesFile("/path/to/package/index.js")).toBe(false); 27 | expect(isPackedTypesFile("/path/to/package/index.d.ts")).toBe(true); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/integration_tests.spec.ts: -------------------------------------------------------------------------------- 1 | import { ESLint, Linter } from "eslint"; 2 | import { execSync } from "child_process"; 3 | import { 4 | NO_MISSING_TYPES_FIXTURE_PATH, 5 | ALPHABETICALLY_SORTED_DEPENDENCIES_FIXTURES_PATH, 6 | CONTROLLED_VERSIONS_FIXTURE_PATH, 7 | BETTER_ALTERNATIVE_FIXTURES_PATH, 8 | VALID_VERSIONS_FIXTURES_PATH, 9 | DUPLICATE_DEPENDENCIES_FIXTURES_PATH, 10 | } from "./constants"; 11 | 12 | const createLiner = (cwd: string, rules: Partial): ESLint => 13 | new ESLint({ 14 | extensions: [".json"], 15 | cwd, 16 | overrideConfig: { 17 | overrides: [ 18 | { 19 | plugins: ["eslint-plugin-package-json-dependencies"], 20 | files: ["*.json"], 21 | parser: "eslint-plugin-package-json-dependencies", 22 | rules, 23 | }, 24 | ], 25 | }, 26 | ignore: false, 27 | useEslintrc: false, 28 | }); 29 | 30 | describe("integration tests", () => { 31 | beforeAll(() => 32 | execSync("npm install", { 33 | cwd: NO_MISSING_TYPES_FIXTURE_PATH, 34 | }), 35 | ); 36 | 37 | test("no-missing-types", async () => { 38 | const results = await createLiner(NO_MISSING_TYPES_FIXTURE_PATH, { 39 | "package-json-dependencies/no-missing-types": [ 40 | "error", 41 | { excludePatterns: [] }, 42 | ], 43 | }).lintFiles("package.json"); 44 | 45 | expect(results).toHaveLength(1); 46 | expect(results[0]).toHaveProperty("errorCount", 1); 47 | expect(results[0]).toHaveProperty("warningCount", 0); 48 | expect(results[0].messages).toHaveLength(1); 49 | expect(results[0].messages[0]).toHaveProperty( 50 | "ruleId", 51 | "package-json-dependencies/no-missing-types", 52 | ); 53 | expect(results[0].messages[0]).toHaveProperty("messageId", "missingTypes"); 54 | expect(results[0].messages[0]).toHaveProperty( 55 | "message", 56 | "Missing types for streamifier", 57 | ); 58 | }); 59 | 60 | test("alphabetically-sorted-dependencies", async () => { 61 | const results = await createLiner( 62 | ALPHABETICALLY_SORTED_DEPENDENCIES_FIXTURES_PATH, 63 | { 64 | "package-json-dependencies/alphabetically-sorted-dependencies": "error", 65 | }, 66 | ).lintFiles("package.json"); 67 | 68 | expect(results).toHaveLength(1); 69 | expect(results[0]).toHaveProperty("errorCount", 1); 70 | expect(results[0]).toHaveProperty("warningCount", 0); 71 | expect(results[0].messages).toHaveLength(1); 72 | expect(results[0].messages[0]).toHaveProperty( 73 | "ruleId", 74 | "package-json-dependencies/alphabetically-sorted-dependencies", 75 | ); 76 | expect(results[0].messages[0]).toHaveProperty( 77 | "messageId", 78 | "unsortedDependencies", 79 | ); 80 | expect(results[0].messages[0]).toHaveProperty( 81 | "message", 82 | "Dependencies under the 'dependencies' key are not alphabetically sorted", 83 | ); 84 | }); 85 | 86 | test("controlled-versions", async () => { 87 | const results = await createLiner(CONTROLLED_VERSIONS_FIXTURE_PATH, { 88 | "package-json-dependencies/controlled-versions": [ 89 | "error", 90 | { 91 | granularity: { dependencies: "patch", devDepdendencies: "fixed" }, 92 | excludePatterns: ["ignored"], 93 | }, 94 | ], 95 | }).lintFiles("package.json"); 96 | 97 | expect(results).toHaveLength(1); 98 | expect(results[0]).toHaveProperty("errorCount", 5); 99 | expect(results[0]).toHaveProperty("warningCount", 0); 100 | expect(results[0].messages).toHaveLength(5); 101 | for (const [i, dependency] of [ 102 | "foo", 103 | "bar", 104 | "baz", 105 | "bay", 106 | "bak", 107 | ].entries()) { 108 | expect(results[0].messages[i]).toHaveProperty( 109 | "ruleId", 110 | "package-json-dependencies/controlled-versions", 111 | ); 112 | expect(results[0].messages[i]).toHaveProperty( 113 | "messageId", 114 | "nonControlledDependency", 115 | ); 116 | expect(results[0].messages[i]).toHaveProperty( 117 | "message", 118 | `Non controlled version found for dependency '${dependency}'`, 119 | ); 120 | } 121 | }); 122 | 123 | test("better-alternative", async () => { 124 | const results = await createLiner(BETTER_ALTERNATIVE_FIXTURES_PATH, { 125 | "package-json-dependencies/better-alternative": [ 126 | "error", 127 | { alternatives: { foo: "bar" } }, 128 | ], 129 | }).lintFiles("package.json"); 130 | 131 | expect(results).toHaveLength(1); 132 | expect(results[0]).toHaveProperty("errorCount", 1); 133 | expect(results[0]).toHaveProperty("warningCount", 0); 134 | expect(results[0].messages).toHaveLength(1); 135 | expect(results[0].messages[0]).toHaveProperty( 136 | "ruleId", 137 | "package-json-dependencies/better-alternative", 138 | ); 139 | expect(results[0].messages[0]).toHaveProperty( 140 | "messageId", 141 | "betterAlternativeExists", 142 | ); 143 | expect(results[0].messages[0]).toHaveProperty( 144 | "message", 145 | "Replace 'foo' with 'bar'", 146 | ); 147 | }); 148 | 149 | test("valid-versions", async () => { 150 | const results = await createLiner(VALID_VERSIONS_FIXTURES_PATH, { 151 | "package-json-dependencies/valid-versions": ["error"], 152 | }).lintFiles("package.json"); 153 | 154 | expect(results).toHaveLength(1); 155 | expect(results[0]).toHaveProperty("errorCount", 1); 156 | expect(results[0]).toHaveProperty("warningCount", 0); 157 | expect(results[0].messages).toHaveLength(1); 158 | expect(results[0].messages[0]).toHaveProperty( 159 | "ruleId", 160 | "package-json-dependencies/valid-versions", 161 | ); 162 | expect(results[0].messages[0]).toHaveProperty( 163 | "messageId", 164 | "invalidVersionDetected", 165 | ); 166 | expect(results[0].messages[0]).toHaveProperty( 167 | "message", 168 | "Invalid version found for dependency 'foo' (space detected after worksapce protocol)", 169 | ); 170 | }); 171 | 172 | test("duplicate-dependencies", async () => { 173 | const results = await createLiner(DUPLICATE_DEPENDENCIES_FIXTURES_PATH, { 174 | "package-json-dependencies/duplicate-dependencies": [ 175 | "error", 176 | { exclude: ["ignored"] }, 177 | ], 178 | }).lintFiles("package.json"); 179 | 180 | expect(results).toHaveLength(1); 181 | expect(results[0]).toHaveProperty("errorCount", 1); 182 | expect(results[0]).toHaveProperty("warningCount", 0); 183 | expect(results[0].messages).toHaveLength(1); 184 | expect(results[0].messages[0]).toHaveProperty( 185 | "ruleId", 186 | "package-json-dependencies/duplicate-dependencies", 187 | ); 188 | expect(results[0].messages[0]).toHaveProperty( 189 | "messageId", 190 | "duplicateDependencyFound", 191 | ); 192 | expect(results[0].messages[0]).toHaveProperty( 193 | "message", 194 | "dependency 'foo' declared multiple times ([dependencies,devDependencies])", 195 | ); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /__tests__/parse-for-eslint.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseForESLint } from ".."; 2 | import dedent from "dedent"; 3 | 4 | describe("parseForESLint", () => { 5 | test("throws with the correct position for invalid JSON", () => { 6 | expect(() => parseForESLint("{},")).toThrow( 7 | /Unexpected token , in JSON at position 2/, 8 | ); 9 | }); 10 | 11 | test("returns a correct AST for valid JSON", () => { 12 | const { ast } = parseForESLint(dedent`{ 13 | "foo": "bar", 14 | "baz": 42 15 | }`); 16 | 17 | expect(ast.body).toHaveLength(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/rules/alphabetically-sorted-dependencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from "eslint"; 2 | import path from "path"; 3 | import { rule } from "../../src/rules/alphabetically-sorted-dependencies"; 4 | import dedent from "dedent"; 5 | 6 | const tester = new RuleTester({ parser: path.resolve(".") }); 7 | 8 | tester.run("alphabetically-sorted-dependencies", rule, { 9 | valid: [ 10 | // not a package.json file 11 | { 12 | code: `"not package.json"`, 13 | filename: "index.js", 14 | }, 15 | // sorted dependencies 16 | { 17 | code: `{ 18 | "dependencies": { 19 | "@scope/a": "~1.0.0", 20 | "@scope/b": "~1.0.0", 21 | "a": "~1.0.0", 22 | "b": "~1.0.0" 23 | } 24 | }`, 25 | filename: "package.json", 26 | }, 27 | // sorted dev dependencies 28 | { 29 | code: `{ 30 | "devDependencies": { 31 | "@scope/a": "~1.0.0", 32 | "@scope/b": "~1.0.0", 33 | "a": "~1.0.0", 34 | "b": "~1.0.0" 35 | } 36 | }`, 37 | filename: "package.json", 38 | }, 39 | // sorted peer dependencies 40 | { 41 | code: `{ 42 | "peerDependencies": { 43 | "@scope/a": "~1.0.0", 44 | "@scope/b": "~1.0.0", 45 | "a": "~1.0.0", 46 | "b": "~1.0.0" 47 | } 48 | }`, 49 | filename: "package.json", 50 | }, 51 | // sorted optional dependencies 52 | { 53 | code: `{ 54 | "optionalDependencies": { 55 | "@scope/a": "~1.0.0", 56 | "@scope/b": "~1.0.0", 57 | "a": "~1.0.0", 58 | "b": "~1.0.0" 59 | } 60 | }`, 61 | filename: "package.json", 62 | }, 63 | ], 64 | invalid: [ 65 | // unsorted dependencies 66 | { 67 | code: dedent`{ 68 | "name": "p1", 69 | "dependencies": { 70 | "a": "~1.0.0", 71 | "@scope/a": "~1.0.0" 72 | } 73 | }`, 74 | filename: "package.json", 75 | errors: [ 76 | { messageId: "unsortedDependencies", data: { key: "dependencies" } }, 77 | ], 78 | output: dedent`{ 79 | "name": "p1", 80 | "dependencies": { 81 | "@scope/a": "~1.0.0", 82 | "a": "~1.0.0" 83 | } 84 | }`, 85 | }, 86 | // unsorted dev dependencies 87 | { 88 | code: dedent`{ 89 | "name": "p1", 90 | "devDependencies": { 91 | "a": "~1.0.0", 92 | "@scope/a": "~1.0.0" 93 | } 94 | }`, 95 | filename: "package.json", 96 | errors: [ 97 | { messageId: "unsortedDependencies", data: { key: "devDependencies" } }, 98 | ], 99 | output: dedent`{ 100 | "name": "p1", 101 | "devDependencies": { 102 | "@scope/a": "~1.0.0", 103 | "a": "~1.0.0" 104 | } 105 | }`, 106 | }, 107 | // unsorted peer dependencies 108 | { 109 | code: dedent`{ 110 | "name": "p1", 111 | "peerDependencies": { 112 | "a": "~1.0.0", 113 | "@scope/a": "~1.0.0" 114 | } 115 | }`, 116 | filename: "package.json", 117 | errors: [ 118 | { 119 | messageId: "unsortedDependencies", 120 | data: { key: "peerDependencies" }, 121 | }, 122 | ], 123 | output: dedent`{ 124 | "name": "p1", 125 | "peerDependencies": { 126 | "@scope/a": "~1.0.0", 127 | "a": "~1.0.0" 128 | } 129 | }`, 130 | }, 131 | // unsorted optional dependencies 132 | { 133 | code: dedent`{ 134 | "name": "p1", 135 | "optionalDependencies": { 136 | "a": "~1.0.0", 137 | "@scope/a": "~1.0.0" 138 | } 139 | }`, 140 | filename: "package.json", 141 | errors: [ 142 | { 143 | messageId: "unsortedDependencies", 144 | data: { key: "optionalDependencies" }, 145 | }, 146 | ], 147 | output: dedent`{ 148 | "name": "p1", 149 | "optionalDependencies": { 150 | "@scope/a": "~1.0.0", 151 | "a": "~1.0.0" 152 | } 153 | }`, 154 | }, 155 | // unsorted dev dependencies but sorted dependencies 156 | { 157 | code: dedent`{ 158 | "name": "p1", 159 | "dependencies": { 160 | "a": "~1.0.0", 161 | "b": "~1.0.0" 162 | }, 163 | "devDependencies": { 164 | "a": "~1.0.0", 165 | "@scope/a": "~1.0.0" 166 | } 167 | }`, 168 | filename: "package.json", 169 | errors: [ 170 | { messageId: "unsortedDependencies", data: { key: "devDependencies" } }, 171 | ], 172 | output: dedent`{ 173 | "name": "p1", 174 | "dependencies": { 175 | "a": "~1.0.0", 176 | "b": "~1.0.0" 177 | }, 178 | "devDependencies": { 179 | "@scope/a": "~1.0.0", 180 | "a": "~1.0.0" 181 | } 182 | }`, 183 | }, 184 | ], 185 | }); 186 | -------------------------------------------------------------------------------- /__tests__/rules/better-alternative.spec.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from "eslint"; 2 | import path from "path"; 3 | import { rule } from "../../src/rules/better-alternative"; 4 | 5 | const tester = new RuleTester({ parser: path.resolve(".") }); 6 | 7 | tester.run("better-alternative", rule, { 8 | valid: [ 9 | // not a package.json file 10 | { 11 | code: `"not package.json"`, 12 | filename: "index.js", 13 | }, 14 | // no alternatives given 15 | { 16 | code: `{ 17 | "dependencies": { 18 | "a": "~1.0.0", 19 | "b": "~1.0.0" 20 | } 21 | }`, 22 | filename: "package.json", 23 | }, 24 | // empty alternatives object 25 | { 26 | code: `{ 27 | "dependencies": { 28 | "a": "~1.0.0", 29 | "b": "~1.0.0" 30 | } 31 | }`, 32 | filename: "package.json", 33 | options: [{ alternatives: {} }], 34 | }, 35 | // alternatives are not listed as dependencies 36 | { 37 | code: `{ 38 | "dependencies": { 39 | "a": "~1.0.0", 40 | "b": "~1.0.0" 41 | } 42 | }`, 43 | filename: "package.json", 44 | options: [{ alternatives: { foo: "bar" } }], 45 | }, 46 | ], 47 | invalid: [ 48 | // better alternative exists on dependencies 49 | { 50 | code: `{ 51 | "name": "p1", 52 | "dependencies": { 53 | "foo": "~1.0.0" 54 | } 55 | }`, 56 | filename: "package.json", 57 | options: [{ alternatives: { foo: "bar" } }], 58 | errors: [ 59 | { 60 | messageId: "betterAlternativeExists", 61 | data: { package: "foo", alternative: "bar" }, 62 | }, 63 | ], 64 | }, 65 | // better alternative exists on devDependencies 66 | { 67 | code: `{ 68 | "name": "p1", 69 | "devDependencies": { 70 | "foo": "~1.0.0" 71 | } 72 | }`, 73 | filename: "package.json", 74 | options: [{ alternatives: { foo: "bar" } }], 75 | errors: [ 76 | { 77 | messageId: "betterAlternativeExists", 78 | data: { package: "foo", alternative: "bar" }, 79 | }, 80 | ], 81 | }, 82 | // better alternative exists on peerDependencies 83 | { 84 | code: `{ 85 | "name": "p1", 86 | "peerDependencies": { 87 | "foo": "~1.0.0" 88 | } 89 | }`, 90 | filename: "package.json", 91 | options: [{ alternatives: { foo: "bar" } }], 92 | errors: [ 93 | { 94 | messageId: "betterAlternativeExists", 95 | data: { package: "foo", alternative: "bar" }, 96 | }, 97 | ], 98 | }, 99 | // better alternative exists on optionalDependencies 100 | { 101 | code: `{ 102 | "name": "p1", 103 | "optionalDependencies": { 104 | "foo": "~1.0.0" 105 | } 106 | }`, 107 | filename: "package.json", 108 | options: [{ alternatives: { foo: "bar" } }], 109 | errors: [ 110 | { 111 | messageId: "betterAlternativeExists", 112 | data: { package: "foo", alternative: "bar" }, 113 | }, 114 | ], 115 | }, 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /__tests__/rules/controlled-versions.spec.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { RuleTester } from "eslint"; 3 | import path from "path"; 4 | import { rule } from "../../src/rules/controlled-versions"; 5 | 6 | const tester = new RuleTester({ parser: path.resolve(".") }); 7 | 8 | // NOTE: Some packages here are real NPM packages that when `fix` is run, 9 | // Are checked against the NPM registry. 10 | tester.run("controlled-versions", rule, { 11 | valid: [ 12 | // not a package.json file 13 | { 14 | code: `"not package.json"`, 15 | filename: "index.js", 16 | }, 17 | // fixed without explicitly passing granularity 18 | { 19 | code: `{ 20 | "name": "p1", 21 | "dependencies": { 22 | "lodash": "4.17.21", 23 | "axios": "=1.2.3", 24 | "foo": "1.2.3 || 1.2.4", 25 | "bar": "1 - 2" 26 | } 27 | }`, 28 | filename: "package.json", 29 | }, 30 | // fixed with explicitly passing granularity 31 | { 32 | code: `{ 33 | "name": "p1", 34 | "dependencies": { 35 | "lodash": "4.17.21", 36 | "axios": "=1.2.3", 37 | "foo": "1.2.3 || 1.2.4", 38 | "bar": "1 - 2" 39 | } 40 | }`, 41 | filename: "package.json", 42 | options: [{ granularity: "fixed" }], 43 | }, 44 | // patch 45 | { 46 | code: `{ 47 | "name": "p1", 48 | "dependencies": { 49 | "axios": "4.17.21", 50 | "foo": "=4.17.21", 51 | "lodash": "~4.17.21", 52 | "baz": "1.2.3 || 1.2.4", 53 | "bay": "~1.2.3 || ~1.2.4", 54 | "bar": "1 - 2", 55 | "bak": "~1 - ~2" 56 | } 57 | }`, 58 | filename: "package.json", 59 | options: [{ granularity: "patch" }], 60 | }, 61 | // minor 62 | { 63 | code: `{ 64 | "name": "p1", 65 | "dependencies": { 66 | "axios": "4.17.21", 67 | "bar": "=4.17.21", 68 | "lodash": "~4.17.21", 69 | "foo": "^4.17.21", 70 | "baz": "1.2.3 || 1.2.4", 71 | "bay": "~1.2.3 || ~1.2.4", 72 | "bal": "^1.2.3 || ^1.2.4", 73 | "bar": "1 - 2", 74 | "bak": "~1 - ~2", 75 | "bap": "^1 - ^2" 76 | } 77 | }`, 78 | filename: "package.json", 79 | options: [{ granularity: "minor" }], 80 | }, 81 | // excluded 82 | { 83 | code: `{ 84 | "name": "p1", 85 | "dependencies": { 86 | "foo1": "latest", 87 | "foo2": "*" 88 | } 89 | }`, 90 | filename: "package.json", 91 | options: [{ granularity: "minor", excludePatterns: ["foo*"] }], 92 | }, 93 | // ignores git links 94 | { 95 | code: `{ 96 | "name": "p1", 97 | "dependencies": { 98 | "foo1": "git://github.com/foo/foo1.git", 99 | "foo2": "git+ssh://git@github.com:foo/foo2.git", 100 | "foo3": "git+http://user@github.com/foo/foo3", 101 | "foo4": "git+https://user@github.com/foo/foo4", 102 | "foo5": "git+file:///path/to/file" 103 | } 104 | }`, 105 | filename: "package.json", 106 | options: [{ granularity: "minor" }], 107 | }, 108 | // ignores file links 109 | { 110 | code: `{ 111 | "name": "p1", 112 | "dependencies": { 113 | "foo1": "file:../relative/path/to/file", 114 | "foo2": "file:/full/path/to/file" 115 | } 116 | }`, 117 | filename: "package.json", 118 | options: [{ granularity: "minor" }], 119 | }, 120 | ], 121 | invalid: [ 122 | // fixed without explicitly passing granularity 123 | { 124 | code: dedent`{ 125 | "name": "p1", 126 | "dependencies": { 127 | "lodash": "~4.17.21", 128 | "axios": "^4.17.21", 129 | "foo": "latest", 130 | "bar": "*", 131 | "baz": ">1.0.0", 132 | "bay": "<=1.0.0", 133 | "bak": "~1.2.3 || 1.2.4", 134 | "bal": "1.2.3 - ^1.2.4", 135 | "valid1": "1.2.3", 136 | "valid2": "=1.2.3", 137 | "valid3": "14 - 16" 138 | } 139 | }`, 140 | filename: "package.json", 141 | errors: [ 142 | { messageId: "nonControlledDependency", data: { package: "lodash" } }, 143 | { messageId: "nonControlledDependency", data: { package: "axios" } }, 144 | { messageId: "nonControlledDependency", data: { package: "foo" } }, 145 | { messageId: "nonControlledDependency", data: { package: "bar" } }, 146 | { messageId: "nonControlledDependency", data: { package: "baz" } }, 147 | { messageId: "nonControlledDependency", data: { package: "bay" } }, 148 | { messageId: "nonControlledDependency", data: { package: "bak" } }, 149 | { messageId: "nonControlledDependency", data: { package: "bal" } }, 150 | ], 151 | output: dedent`{ 152 | "name": "p1", 153 | "dependencies": { 154 | "lodash": "4.17.21", 155 | "axios": "4.17.21", 156 | "foo": "0.0.7", 157 | "bar": "0.1.2", 158 | "baz": "1.0.0", 159 | "bay": "1.0.0", 160 | "bak": "1.2.3 || 1.2.4", 161 | "bal": "1.2.3 - 1.2.4", 162 | "valid1": "1.2.3", 163 | "valid2": "=1.2.3", 164 | "valid3": "14 - 16" 165 | } 166 | }`, 167 | }, 168 | // fixed with explicitly passing granularity 169 | { 170 | code: dedent`{ 171 | "name": "p1", 172 | "devDependencies": { 173 | "lodash": "~4.17.21", 174 | "axios": "^4.17.21", 175 | "foo": "latest", 176 | "bar": "*", 177 | "baz": ">1.0.0", 178 | "bay": "<=1.0.0", 179 | "bak": "~1.2.3 || 1.2.4", 180 | "bal": "1.2.3 - ^1.2.4", 181 | "valid1": "1.2.3", 182 | "valid2": "=1.2.3", 183 | "valid3": "14 - 16" 184 | } 185 | }`, 186 | filename: "package.json", 187 | options: [{ granularity: "fixed" }], 188 | errors: [ 189 | { messageId: "nonControlledDependency", data: { package: "lodash" } }, 190 | { messageId: "nonControlledDependency", data: { package: "axios" } }, 191 | { messageId: "nonControlledDependency", data: { package: "foo" } }, 192 | { messageId: "nonControlledDependency", data: { package: "bar" } }, 193 | { messageId: "nonControlledDependency", data: { package: "baz" } }, 194 | { messageId: "nonControlledDependency", data: { package: "bay" } }, 195 | { messageId: "nonControlledDependency", data: { package: "bak" } }, 196 | { messageId: "nonControlledDependency", data: { package: "bal" } }, 197 | ], 198 | output: dedent`{ 199 | "name": "p1", 200 | "devDependencies": { 201 | "lodash": "4.17.21", 202 | "axios": "4.17.21", 203 | "foo": "0.0.7", 204 | "bar": "0.1.2", 205 | "baz": "1.0.0", 206 | "bay": "1.0.0", 207 | "bak": "1.2.3 || 1.2.4", 208 | "bal": "1.2.3 - 1.2.4", 209 | "valid1": "1.2.3", 210 | "valid2": "=1.2.3", 211 | "valid3": "14 - 16" 212 | } 213 | }`, 214 | }, 215 | // patch with explicitly passing granularity 216 | { 217 | code: dedent`{ 218 | "name": "p1", 219 | "peerDependencies": { 220 | "axios": "^4.17.21", 221 | "foo": "latest", 222 | "bar": "*", 223 | "baz": ">1.0.0", 224 | "bay": "<=1.0.0", 225 | "bak": "^1.2.3 || 1.2.4", 226 | "bal": "1.2.3 - ^1.2.4", 227 | "valid1": "1.2.3", 228 | "valid2": "=1.2.3", 229 | "valid3": "~1.3.4", 230 | "valid4": "~14 - ~16" 231 | } 232 | }`, 233 | filename: "package.json", 234 | options: [{ granularity: "patch" }], 235 | errors: [ 236 | { messageId: "nonControlledDependency", data: { package: "axios" } }, 237 | { messageId: "nonControlledDependency", data: { package: "foo" } }, 238 | { messageId: "nonControlledDependency", data: { package: "bar" } }, 239 | { messageId: "nonControlledDependency", data: { package: "baz" } }, 240 | { messageId: "nonControlledDependency", data: { package: "bay" } }, 241 | { messageId: "nonControlledDependency", data: { package: "bak" } }, 242 | { messageId: "nonControlledDependency", data: { package: "bal" } }, 243 | ], 244 | output: dedent`{ 245 | "name": "p1", 246 | "peerDependencies": { 247 | "axios": "~4.17.21", 248 | "foo": "~0.0.7", 249 | "bar": "~0.1.2", 250 | "baz": "~1.0.0", 251 | "bay": "~1.0.0", 252 | "bak": "~1.2.3 || 1.2.4", 253 | "bal": "1.2.3 - ~1.2.4", 254 | "valid1": "1.2.3", 255 | "valid2": "=1.2.3", 256 | "valid3": "~1.3.4", 257 | "valid4": "~14 - ~16" 258 | } 259 | }`, 260 | }, 261 | // minor with explicitly passing granularity 262 | { 263 | code: dedent`{ 264 | "name": "p1", 265 | "optionalDependencies": { 266 | "foo": "latest", 267 | "bar": "*", 268 | "baz": ">1.0.0", 269 | "bay": "<=1.0.0", 270 | "valid1": "1.2.3", 271 | "valid2": "=1.2.3", 272 | "valid3": "~1.3.4", 273 | "valid4": "^1.3.4", 274 | "valid5": "^14 - ^16" 275 | } 276 | }`, 277 | filename: "package.json", 278 | options: [{ granularity: "minor" }], 279 | errors: [ 280 | { messageId: "nonControlledDependency", data: { package: "foo" } }, 281 | { messageId: "nonControlledDependency", data: { package: "bar" } }, 282 | { messageId: "nonControlledDependency", data: { package: "baz" } }, 283 | { messageId: "nonControlledDependency", data: { package: "bay" } }, 284 | ], 285 | output: dedent`{ 286 | "name": "p1", 287 | "optionalDependencies": { 288 | "foo": "^0.0.7", 289 | "bar": "^0.1.2", 290 | "baz": "^1.0.0", 291 | "bay": "^1.0.0", 292 | "valid1": "1.2.3", 293 | "valid2": "=1.2.3", 294 | "valid3": "~1.3.4", 295 | "valid4": "^1.3.4", 296 | "valid5": "^14 - ^16" 297 | } 298 | }`, 299 | }, 300 | ], 301 | }); 302 | -------------------------------------------------------------------------------- /__tests__/rules/duplicate-dependencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from "eslint"; 2 | import path from "path"; 3 | import { rule } from "../../src/rules/duplicate-dependencies"; 4 | 5 | const tester = new RuleTester({ parser: path.resolve(".") }); 6 | 7 | tester.run("duplicate", rule, { 8 | valid: [ 9 | // not a package.json file 10 | { 11 | code: `"not package.json"`, 12 | filename: "index.js", 13 | }, 14 | // dependency only defined once 15 | { 16 | code: `{ 17 | "name": "p1", 18 | "devDependencies": { 19 | "package": "1.0.0" 20 | } 21 | }`, 22 | filename: "package.json", 23 | }, 24 | // dependency is excluded 25 | { 26 | code: `{ 27 | "name": "p1", 28 | "dependencies": { 29 | "package": "1.0.0" 30 | }, 31 | "devDependencies": { 32 | "package": "1.0.0" 33 | } 34 | }`, 35 | filename: "package.json", 36 | options: [{ exclude: ["package"] }], 37 | }, 38 | ], 39 | invalid: [ 40 | // dependency defined on multiple dependencies keys 41 | { 42 | code: `{ 43 | "name": "p1", 44 | "dependencies": { 45 | "package": "1.0.0" 46 | }, 47 | "devDependencies": { 48 | "package": "2.0.0" 49 | } 50 | }`, 51 | filename: "package.json", 52 | errors: [ 53 | { 54 | messageId: "duplicateDependencyFound", 55 | data: { 56 | package: "package", 57 | origins: "[dependencies,devDependencies]", 58 | }, 59 | }, 60 | ], 61 | }, 62 | // exclude without exact match 63 | { 64 | code: `{ 65 | "name": "p1", 66 | "optionalDependencies": { 67 | "package": "1.0.0" 68 | }, 69 | "peerDependencies": { 70 | "package": "2.0.0" 71 | } 72 | }`, 73 | filename: "package.json", 74 | options: [{ exclude: ["packag"] }], 75 | errors: [ 76 | { 77 | messageId: "duplicateDependencyFound", 78 | data: { 79 | package: "package", 80 | origins: "[peerDependencies,optionalDependencies]", 81 | }, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }); 87 | -------------------------------------------------------------------------------- /__tests__/rules/no-missing-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from "eslint"; 2 | import path from "path"; 3 | import { rule } from "../../src/rules/no-missing-types"; 4 | 5 | const tester = new RuleTester({ parser: path.resolve(".") }); 6 | 7 | tester.run("no-missing-types", rule, { 8 | valid: [ 9 | // not a package.json file 10 | { 11 | code: `"not package.json"`, 12 | filename: "index.js", 13 | }, 14 | // has @types without the dependency 15 | { 16 | code: `{ 17 | "name": "p1", 18 | "devDependencies": { 19 | "@types/package": "~1.0.0" 20 | } 21 | }`, 22 | filename: "package.json", 23 | }, 24 | // has scoped @types without the dependency 25 | { 26 | code: `{ 27 | "name": "p1", 28 | "devDependencies": { 29 | "@types/scope__package": "~1.0.0" 30 | } 31 | }`, 32 | filename: "package.json", 33 | }, 34 | // has @types for a dependency 35 | { 36 | code: `{ 37 | "name": "p1", 38 | "dependencies": { 39 | "package": "~1.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/package": "~1.0.0" 43 | } 44 | }`, 45 | filename: "package.json", 46 | }, 47 | // has @types for a scoped dependency 48 | { 49 | code: `{ 50 | "name": "p1", 51 | "dependencies": { 52 | "@scope/package": "~1.0.0" 53 | }, 54 | "devDependencies": { 55 | "@types/scope__package": "~1.0.0" 56 | } 57 | }`, 58 | filename: "package.json", 59 | }, 60 | // has @types for a dev-dependency 61 | { 62 | code: `{ 63 | "name": "p1", 64 | "devDependencies": { 65 | "package": "~1.0.0", 66 | "@types/package": "~1.0.0" 67 | } 68 | }`, 69 | filename: "package.json", 70 | }, 71 | // has @types for a scoped dev-dependency 72 | { 73 | code: `{ 74 | "name": "p1", 75 | "devDependencies": { 76 | "@scope/package": "~1.0.0", 77 | "@types/scope__package": "~1.0.0" 78 | } 79 | }`, 80 | filename: "package.json", 81 | }, 82 | // misses @types but is excluded 83 | { 84 | code: `{ 85 | "name": "p1", 86 | "devDependencies": { 87 | "package": "~1.0.0" 88 | } 89 | }`, 90 | filename: "package.json", 91 | options: [{ excludePatterns: ["pack*"] }], 92 | }, 93 | // misses scoped @types but is excluded 94 | { 95 | code: `{ 96 | "name": "p1", 97 | "devDependencies": { 98 | "@scope/package": "~1.0.0" 99 | } 100 | }`, 101 | filename: "package.json", 102 | options: [{ excludePatterns: ["@scope/package"] }], 103 | }, 104 | // has dynamic types (relies on typescript being a dependency of this package) 105 | { 106 | code: `{ 107 | "name": "p1", 108 | "devDependencies": { 109 | "typescript": "~1.0.0" 110 | } 111 | }`, 112 | filename: "package.json", 113 | }, 114 | ], 115 | invalid: [ 116 | // missing types for a dependency 117 | { 118 | code: `{ 119 | "name": "p1", 120 | "dependencies": { 121 | "package": "~1.0.0" 122 | } 123 | }`, 124 | filename: "package.json", 125 | errors: [{ messageId: "missingTypes", data: { package: "package" } }], 126 | }, 127 | // missing types for a scoped dependency 128 | { 129 | code: `{ 130 | "name": "p1", 131 | "dependencies": { 132 | "@scope/package": "~1.0.0" 133 | } 134 | }`, 135 | filename: "package.json", 136 | errors: [ 137 | { messageId: "missingTypes", data: { package: "@scope/package" } }, 138 | ], 139 | }, 140 | // missing types for a dev dependency 141 | { 142 | code: `{ 143 | "name": "p1", 144 | "devDependencies": { 145 | "package": "~1.0.0" 146 | } 147 | }`, 148 | filename: "package.json", 149 | errors: [{ messageId: "missingTypes", data: { package: "package" } }], 150 | }, 151 | // missing types for a scoped dev dependency 152 | { 153 | code: `{ 154 | "name": "p1", 155 | "devDependencies": { 156 | "@scope/package": "~1.0.0" 157 | } 158 | }`, 159 | filename: "package.json", 160 | errors: [ 161 | { messageId: "missingTypes", data: { package: "@scope/package" } }, 162 | ], 163 | }, 164 | ], 165 | }); 166 | -------------------------------------------------------------------------------- /__tests__/rules/valid-versions.spec.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from "eslint"; 2 | import path from "path"; 3 | import { rule } from "../../src/rules/valid-versions"; 4 | 5 | const tester = new RuleTester({ parser: path.resolve(".") }); 6 | 7 | tester.run("valid-versions", rule, { 8 | valid: [ 9 | // not a package.json file 10 | { 11 | code: `"not package.json"`, 12 | filename: "index.js", 13 | }, 14 | // valid version - fixed 15 | { 16 | code: `{ 17 | "name": "p1", 18 | "devDependencies": { 19 | "package": "1.0.0" 20 | } 21 | }`, 22 | filename: "package.json", 23 | }, 24 | // valid version - tilde 25 | { 26 | code: `{ 27 | "name": "p1", 28 | "devDependencies": { 29 | "package": "~1.0.0" 30 | } 31 | }`, 32 | filename: "package.json", 33 | }, 34 | // valid version - carrot 35 | { 36 | code: `{ 37 | "name": "p1", 38 | "devDependencies": { 39 | "package": "^1.0.0" 40 | } 41 | }`, 42 | filename: "package.json", 43 | }, 44 | // valid version - all 45 | { 46 | code: `{ 47 | "name": "p1", 48 | "devDependencies": { 49 | "package": "*" 50 | } 51 | }`, 52 | filename: "package.json", 53 | }, 54 | // valid version - dist-tag 55 | { 56 | code: `{ 57 | "name": "p1", 58 | "devDependencies": { 59 | "package": "latest" 60 | } 61 | }`, 62 | filename: "package.json", 63 | }, 64 | // valid version - workspace dependency 65 | { 66 | code: `{ 67 | "name": "p1", 68 | "devDependencies": { 69 | "package": "workspace:*" 70 | } 71 | }`, 72 | filename: "package.json", 73 | }, 74 | // ignored version - git 75 | { 76 | code: `{ 77 | "name": "p1", 78 | "devDependencies": { 79 | "package": "git://github.com/user/project.git#commit-ish" 80 | } 81 | }`, 82 | filename: "package.json", 83 | }, 84 | // ignored version - file 85 | { 86 | code: `{ 87 | "name": "p1", 88 | "devDependencies": { 89 | "package": "file:../package" 90 | } 91 | }`, 92 | filename: "package.json", 93 | }, 94 | ], 95 | invalid: [ 96 | // invalid workspace dependency (space) 97 | { 98 | code: `{ 99 | "name": "p1", 100 | "dependencies": { 101 | "package": "workspace: *" 102 | } 103 | }`, 104 | filename: "package.json", 105 | errors: [ 106 | { 107 | messageId: "invalidVersionDetected", 108 | data: { 109 | package: "package", 110 | reason: "space detected after worksapce protocol", 111 | }, 112 | }, 113 | ], 114 | }, 115 | // invalid workspace dependency (format) 116 | { 117 | code: `{ 118 | "name": "p1", 119 | "dependencies": { 120 | "package": "workspace:~~" 121 | } 122 | }`, 123 | filename: "package.json", 124 | errors: [ 125 | { 126 | messageId: "invalidVersionDetected", 127 | data: { 128 | package: "package", 129 | reason: "invalid version format", 130 | }, 131 | }, 132 | ], 133 | }, 134 | // dist tag does not exist 135 | { 136 | code: `{ 137 | "name": "p1", 138 | "dependencies": { 139 | "lodash": "latest2" 140 | } 141 | }`, 142 | filename: "package.json", 143 | errors: [ 144 | { 145 | messageId: "invalidVersionDetected", 146 | data: { 147 | package: "lodash", 148 | reason: "dist tag does not exist", 149 | }, 150 | }, 151 | ], 152 | }, 153 | // invalid dependency format 154 | { 155 | code: `{ 156 | "name": "p1", 157 | "dependencies": { 158 | "package": "~~" 159 | } 160 | }`, 161 | filename: "package.json", 162 | errors: [ 163 | { 164 | messageId: "invalidVersionDetected", 165 | data: { 166 | package: "package", 167 | reason: "invalid version format", 168 | }, 169 | }, 170 | ], 171 | }, 172 | ], 173 | }); 174 | -------------------------------------------------------------------------------- /__tests__/to-controlled-semver.spec.ts: -------------------------------------------------------------------------------- 1 | import { toControlledSemver } from "../src/to-controlled-semver"; 2 | 3 | describe("toControlledSemver", () => { 4 | // Testing this REAL package since it wasn't published a long time ago. 5 | // This might break, but I'm just avoiding writing a fake NPM server. 6 | const packageName = "streamifier"; 7 | 8 | test("fixed granularity", () => { 9 | expect(toControlledSemver(packageName, "~1.0.0", "fixed")).toStrictEqual( 10 | "1.0.0", 11 | ); 12 | expect(toControlledSemver(packageName, "^1.0.0", "fixed")).toStrictEqual( 13 | "1.0.0", 14 | ); 15 | expect(toControlledSemver(packageName, ">1.0.0", "fixed")).toStrictEqual( 16 | "1.0.0", 17 | ); 18 | expect(toControlledSemver(packageName, ">=1.0.0", "fixed")).toStrictEqual( 19 | "1.0.0", 20 | ); 21 | expect(toControlledSemver(packageName, "<1.0.0", "fixed")).toStrictEqual( 22 | "1.0.0", 23 | ); 24 | expect(toControlledSemver(packageName, "<=1.0.0", "fixed")).toStrictEqual( 25 | "1.0.0", 26 | ); 27 | expect(toControlledSemver(packageName, "*", "fixed")).toStrictEqual( 28 | "0.1.1", 29 | ); 30 | expect(toControlledSemver(packageName, "latest", "fixed")).toStrictEqual( 31 | "0.1.1", 32 | ); 33 | }); 34 | 35 | test("patch granularity", () => { 36 | expect(toControlledSemver(packageName, "^1.0.0", "patch")).toStrictEqual( 37 | "~1.0.0", 38 | ); 39 | expect(toControlledSemver(packageName, ">1.0.0", "patch")).toStrictEqual( 40 | "~1.0.0", 41 | ); 42 | expect(toControlledSemver(packageName, ">=1.0.0", "patch")).toStrictEqual( 43 | "~1.0.0", 44 | ); 45 | expect(toControlledSemver(packageName, "<1.0.0", "patch")).toStrictEqual( 46 | "~1.0.0", 47 | ); 48 | expect(toControlledSemver(packageName, "<=1.0.0", "patch")).toStrictEqual( 49 | "~1.0.0", 50 | ); 51 | expect(toControlledSemver(packageName, "*", "patch")).toStrictEqual( 52 | "~0.1.1", 53 | ); 54 | expect(toControlledSemver(packageName, "latest", "patch")).toStrictEqual( 55 | "~0.1.1", 56 | ); 57 | }); 58 | 59 | test("minor granularity", () => { 60 | expect(toControlledSemver(packageName, ">1.0.0", "minor")).toStrictEqual( 61 | "^1.0.0", 62 | ); 63 | expect(toControlledSemver(packageName, ">=1.0.0", "minor")).toStrictEqual( 64 | "^1.0.0", 65 | ); 66 | expect(toControlledSemver(packageName, "<1.0.0", "minor")).toStrictEqual( 67 | "^1.0.0", 68 | ); 69 | expect(toControlledSemver(packageName, "<=1.0.0", "minor")).toStrictEqual( 70 | "^1.0.0", 71 | ); 72 | expect(toControlledSemver(packageName, "*", "minor")).toStrictEqual( 73 | "^0.1.1", 74 | ); 75 | expect(toControlledSemver(packageName, "latest", "minor")).toStrictEqual( 76 | "^0.1.1", 77 | ); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPackageJsonFile, 3 | getDependenciesSafe, 4 | isGitDependency, 5 | isFileDependency, 6 | isWorkspaceDependency, 7 | isDistTagDependency, 8 | resolveDistTag, 9 | } from "../src/utils"; 10 | 11 | describe("utils", () => { 12 | test("isPackageJsonFile", () => { 13 | expect(isPackageJsonFile("package.json")).toBe(true); 14 | expect(isPackageJsonFile("./some/relative/path/package.json")).toBe(true); 15 | expect(isPackageJsonFile("/some/absolute/path/package.json")).toBe(true); 16 | }); 17 | 18 | test("getDependenciesSafe", () => { 19 | const packageJson = { 20 | dependencies: { foo: "bar" }, 21 | }; 22 | 23 | expect(getDependenciesSafe(packageJson, "dependencies")).toStrictEqual([ 24 | "foo", 25 | ]); 26 | expect(getDependenciesSafe(packageJson, "devDependencies")).toStrictEqual( 27 | [], 28 | ); 29 | }); 30 | 31 | test("isGitDependency", () => { 32 | expect( 33 | isGitDependency("git://github.com/user/project.git#commit-ish"), 34 | ).toBeTruthy(); 35 | expect(isGitDependency("*")).toBeFalsy(); 36 | }); 37 | 38 | test("isFileDependency", () => { 39 | expect(isFileDependency("file:../package")).toBeTruthy(); 40 | expect(isFileDependency("*")).toBeFalsy(); 41 | }); 42 | 43 | test("isWorkspaceDependency", () => { 44 | expect(isWorkspaceDependency("workspace:*")).toBeTruthy(); 45 | expect(isWorkspaceDependency("*")).toBeFalsy(); 46 | }); 47 | 48 | test("isDistTagDependency", () => { 49 | expect(isDistTagDependency("latest")).toBeTruthy(); 50 | expect(isDistTagDependency("*")).toBeFalsy(); 51 | }); 52 | 53 | test("resolveDistTag", () => { 54 | expect(resolveDistTag("lodash", "latest")).toBeTruthy(); 55 | expect(() => 56 | resolveDistTag("no-way__this__package_exists3", "latest"), 57 | ).toThrow(/does not exist/); 58 | expect(() => resolveDistTag("lodash", "latest2")).toThrow( 59 | /not found for package/, 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/rules/alphabetically-sorted-dependencies.md: -------------------------------------------------------------------------------- 1 | # alphabetically-sorted-dependencies 2 | 3 | Makes sure every dependencies object (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) is alphabetically sorted. 4 | 5 | **NOTE**: This rule will alphabetically order your dependencies, if the `--fix` flag is passed to ESLint. 6 | 7 | \***\*Bad\*\***: 8 | 9 | ```json 10 | { 11 | "dependencies": { 12 | "lodash": "~4.17.0", 13 | "axios": "~0.21.0" 14 | } 15 | } 16 | ``` 17 | 18 | \***\*Good\*\***: 19 | 20 | ```json 21 | { 22 | "dependencies": { 23 | "axios": "~0.21.0", 24 | "lodash": "~4.17.0" 25 | } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/rules/better-alternative.md: -------------------------------------------------------------------------------- 1 | # better-alternative 2 | 3 | Provides a way to ensure that the used dependencies are the preferred ones. 4 | 5 | Assuming the rule is used with the following options: 6 | 7 | ```json 8 | { 9 | "alternatives": { 10 | "node-fetch": "axios" 11 | } 12 | } 13 | ``` 14 | 15 | \***\*Bad\*\***: 16 | 17 | ```json 18 | { 19 | "dependencies": { 20 | "node-fetch": "~2.6.0" 21 | } 22 | } 23 | ``` 24 | 25 | \***\*Good\*\***: 26 | 27 | ```json 28 | { 29 | "dependencies": { 30 | "axios": "~0.21.0" 31 | } 32 | } 33 | ``` 34 | 35 | ## Options 36 | 37 | - `alternatives: Record`: An object containing the to-be replaced packages as keys, and their alternative packages as values. 38 | -------------------------------------------------------------------------------- /docs/rules/controlled-versions.md: -------------------------------------------------------------------------------- 1 | # controlled-versions 2 | 3 | Makes sure any used package is set to a controlled version. A controlled version is a semver that follows the passed granularity restrictions. 4 | 5 | Supported granularity: 6 | 7 | - `fixed`: All packages versions should be fixed (e.g `1.0.0`). 8 | - `patch`: All packages versions should start with `~` (e.g `~1.0.0`), or follow the restrictions for `fixed`. 9 | - `minor`: All packages versions should start with `^` (e.g `^1.0.0`) or follow the restrictions for `patch`. 10 | 11 | Examples for assuming `patch` is set: 12 | 13 | \***\*Bad\*\***: 14 | 15 | ```json 16 | { 17 | "dependencies": { 18 | "lodash": "^4.17.0" 19 | } 20 | } 21 | ``` 22 | 23 | \***\*Good\*\***: 24 | 25 | ```json 26 | { 27 | "dependencies": { 28 | "lodash": "~4.17.0" 29 | } 30 | } 31 | ``` 32 | 33 | \***\*Also Good\*\***: 34 | 35 | ```json 36 | { 37 | "dependencies": { 38 | "lodash": "4.17.0" 39 | } 40 | } 41 | ``` 42 | 43 | NOTE: This rule will update your dependencies' versions, if the --fix flag is passed to ESLint. 44 | 45 | ## Options 46 | 47 | - `granularity`: Either a string: `fixed` / `patch` / `minor`, as mentioned above, or an object where the key is the dependencies key (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`) and the value is one of the values above. If not provided, defaults to `fixed`. 48 | - `excludePatterns: string[]`: Makes this rule ignore packages that match the given patterns and do not fail. Might be useful for specific packages that are used with dist-tags. 49 | -------------------------------------------------------------------------------- /docs/rules/duplicate-dependencies.md: -------------------------------------------------------------------------------- 1 | # duplicate-dependencies 2 | 3 | Asserts dependencies are only defined once. 4 | 5 | \***\*Bad\*\***: 6 | 7 | ```json 8 | { 9 | "dependencies": { 10 | "lodash": "^4.17.0" 11 | }, 12 | "devDependencies": { 13 | "lodash": "^4.17.0" 14 | } 15 | } 16 | ``` 17 | 18 | \***\*Good\*\***: 19 | 20 | ```json 21 | { 22 | "dependencies": { 23 | "lodash": "~4.17.0" 24 | } 25 | } 26 | ``` 27 | 28 | \***\*Also Good\*\***: (depends on the use case) 29 | 30 | ```json 31 | { 32 | "devDependencies": { 33 | "lodash": "4.17.0" 34 | } 35 | } 36 | ``` 37 | 38 | ## Options 39 | 40 | - `exclude: string[]`: A list of dependencies this rule will ignore. 41 | -------------------------------------------------------------------------------- /docs/rules/no-missing-types.md: -------------------------------------------------------------------------------- 1 | # no-missing-types 2 | 3 | Ensures that any pacakges used has Typescript definitions (either included in the package itself, or installed as a separate `@types`-scoped dependency) 4 | 5 | **NOTE**: Packages containing their own types do not require any supporting `@types`-scoped packages to be installed (e.g., `axios`). 6 | 7 | \***\*Bad\*\***: 8 | 9 | `lodash` is used without installing `@types/lodash`: 10 | 11 | ```json 12 | { 13 | "dependencies": { 14 | "lodash": "~4.17.0" 15 | } 16 | } 17 | ``` 18 | 19 | \***\*Good\*\***: 20 | 21 | `@types/lodash` is included as dev-depdendency, providing types for the `lodash` package: 22 | 23 | ```json 24 | { 25 | "dependencies": { 26 | "lodash": "~4.17.0" 27 | }, 28 | "devDependencies": { 29 | "@types/lodash": "~4.14.0" 30 | } 31 | } 32 | ``` 33 | 34 | ## Options 35 | 36 | - `excludePatterns: string[]`: Tells this rule to ignore packages matching the given patterns. Might be useful for a custom `jest`-reporter or an `ESLint` plugin that doesn't have any types exposed. 37 | -------------------------------------------------------------------------------- /docs/rules/valid-versions.md: -------------------------------------------------------------------------------- 1 | # valid-versions 2 | 3 | Ensures that the used dependencies versions are valid. 4 | 5 | \***\*Bad\*\***: (invalid format) 6 | 7 | ```json 8 | { 9 | "dependencies": { 10 | "foo": "~~1.0.0" 11 | } 12 | } 13 | ``` 14 | 15 | \***\*Good\*\***: 16 | 17 | ```json 18 | { 19 | "dependencies": { 20 | "foo": "~1.0.0" 21 | } 22 | } 23 | ``` 24 | 25 | \***\*Bad\*\***: (notice the space before the asterisk) 26 | 27 | ```json 28 | { 29 | "dependencies": { 30 | "foo": "workspace: *" 31 | } 32 | } 33 | ``` 34 | 35 | \***\*Good\*\***: 36 | 37 | ```json 38 | { 39 | "dependencies": { 40 | "foo": "workspace:*" 41 | } 42 | } 43 | ``` 44 | 45 | \***\*Bad\*\***: (dist-tag does not exist) 46 | 47 | ```json 48 | { 49 | "dependencies": { 50 | "foo": "badDistTag" 51 | } 52 | } 53 | ``` 54 | 55 | \***\*Good\*\***: 56 | 57 | ```json 58 | { 59 | "dependencies": { 60 | "foo": "latest" 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { rules } from "./src/rules"; 2 | import { parseForESLint } from "./src/parse-for-eslint"; 3 | 4 | export { rules, parseForESLint }; 5 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | testMatch: ["**/__tests__/**/*.spec.ts"], 3 | verbose: true, 4 | preset: "ts-jest", 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-package-json-dependencies", 3 | "version": "1.0.20", 4 | "description": "A plugin for package json dependencies", 5 | "main": "dist", 6 | "scripts": { 7 | "build": "tsc", 8 | "pretest": "npm run build", 9 | "test": "jest", 10 | "posttest": "npm run lint", 11 | "lint": "eslint . --ext json", 12 | "format": "npx prettier --write ." 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:idan-at/eslint-plugin-package-json-dependencies.git" 20 | }, 21 | "keywords": [ 22 | "eslint", 23 | "package-json", 24 | "types", 25 | "typescript", 26 | "dependencies", 27 | "json" 28 | ], 29 | "author": "Idan Attias", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@types/dedent": "~0.7.0", 33 | "@types/eslint": "~8.4.10", 34 | "@types/esprima": "~4.0.0", 35 | "@types/estree": "~1.0.3", 36 | "@types/jest": "~29.2.0", 37 | "@types/lodash": "~4.14.0", 38 | "@types/micromatch": "~4.0.0", 39 | "@types/node": "~18.11.0", 40 | "@types/semver": "~7.5.2", 41 | "dedent": "~0.7.0", 42 | "eslint": "~8.28.0", 43 | "eslint-plugin-self": "~1.2.0", 44 | "jest": "~29.3.0", 45 | "ts-jest": "~29.0.0", 46 | "ts-node": "~10.9.0", 47 | "typescript": "~4.9.0" 48 | }, 49 | "dependencies": { 50 | "comment-json": "~4.2.0", 51 | "esprima": "~4.0.0", 52 | "lodash": "~4.17.0", 53 | "micromatch": "~4.0.0", 54 | "semver": "~7.5.2" 55 | } 56 | } -------------------------------------------------------------------------------- /src/compiler-facade.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { parse } from "comment-json"; 5 | 6 | interface TsConfig { 7 | compilerOptions: ts.CompilerOptions; 8 | } 9 | 10 | const fileExists = (fileName: string): boolean => ts.sys.fileExists(fileName); 11 | const readFile = (fileName: string): string | undefined => 12 | ts.sys.readFile(fileName); 13 | const parseTsConfig = (content: string): TsConfig => { 14 | try { 15 | return parse(content) as unknown as TsConfig; 16 | } catch (e) { 17 | console.error("Failed to parse tsconfig"); 18 | 19 | return { 20 | compilerOptions: {}, 21 | }; 22 | } 23 | }; 24 | 25 | const resolveTypeRoots = (cwd: string): string[] => { 26 | const tsconfigPath = path.join(cwd, "tsconfig.json"); 27 | 28 | if (!fs.existsSync(tsconfigPath)) { 29 | return []; 30 | } 31 | 32 | const content = fs.readFileSync(tsconfigPath, "utf8"); 33 | const tsconfig = parseTsConfig(content); 34 | const compilerOptions = tsconfig.compilerOptions; 35 | 36 | if (compilerOptions.typeRoots) { 37 | return Array.from( 38 | compilerOptions.typeRoots.map((typeRoot) => path.resolve(cwd, typeRoot)), 39 | ); 40 | } 41 | 42 | return []; 43 | }; 44 | 45 | const resolveModuleName = (cwd: string, moduleName: string): string | null => { 46 | const typeRoots = resolveTypeRoots(cwd); 47 | 48 | // File doesn't have to exist, resolveModuleName needs a file inside CWD. 49 | const fileInsideCwd = path.join(cwd, "index.ts"); 50 | 51 | const { resolvedModule } = ts.resolveModuleName( 52 | moduleName, 53 | fileInsideCwd, 54 | {}, 55 | { fileExists, readFile }, 56 | ); 57 | 58 | if (!resolvedModule) { 59 | for (const typeRoot of typeRoots) { 60 | const modulePath = path.join(typeRoot, `${moduleName}.d.ts`); 61 | 62 | if (fs.existsSync(modulePath)) { 63 | return modulePath; 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | 70 | return resolvedModule.resolvedFileName; 71 | }; 72 | 73 | export { resolveModuleName, resolveTypeRoots }; 74 | -------------------------------------------------------------------------------- /src/has-types.ts: -------------------------------------------------------------------------------- 1 | import { resolveModuleName } from "./compiler-facade"; 2 | 3 | const hasTypesDependency = ( 4 | dependency: string, 5 | typesDependencies: string[], 6 | ): boolean => { 7 | if (dependency.startsWith("@")) { 8 | return typesDependencies.includes( 9 | `@types/${dependency.replace("@", "").replace("/", "__")}`, 10 | ); 11 | } else { 12 | return typesDependencies.includes(`@types/${dependency}`); 13 | } 14 | }; 15 | 16 | // If it contains @types it means the package does not come packed with its own types. 17 | const isPackedTypesFile = (filename: string): boolean => 18 | !filename.includes("/@types/") && filename.endsWith(".d.ts"); 19 | 20 | const hasPackedTypes = (cwd: string, dependency: string): boolean => { 21 | const resolvedFileName = resolveModuleName(cwd, dependency); 22 | 23 | return !!resolvedFileName && isPackedTypesFile(resolvedFileName); 24 | }; 25 | 26 | const hasTypes = ( 27 | cwd: string, 28 | dependency: string, 29 | typesDependencies: string[], 30 | ): boolean => 31 | hasTypesDependency(dependency, typesDependencies) || 32 | hasPackedTypes(cwd, dependency); 33 | 34 | export { hasTypes, isPackedTypesFile }; 35 | -------------------------------------------------------------------------------- /src/parse-for-eslint.ts: -------------------------------------------------------------------------------- 1 | import { parseScript } from "esprima"; 2 | import { Linter, AST } from "eslint"; 3 | 4 | // We parse the JSON as JS, but since non of the rules use the AST, we don't fix the locations. 5 | function parseForESLint(text: string, options?: any): Linter.ESLintParseResult { 6 | const code = `(${text});`; 7 | 8 | try { 9 | const ast = parseScript(code, options) as AST.Program; 10 | 11 | return { ast }; 12 | } catch (error) { 13 | try { 14 | JSON.parse(text); 15 | } catch (parseError) { 16 | error.message = parseError.message; 17 | } 18 | 19 | throw error; 20 | } 21 | } 22 | 23 | export { parseForESLint }; 24 | -------------------------------------------------------------------------------- /src/rules/alphabetically-sorted-dependencies.ts: -------------------------------------------------------------------------------- 1 | import { isPackageJsonFile, getDependenciesSafe } from "../utils"; 2 | import { Rule } from "eslint"; 3 | import _ from "lodash"; 4 | import { DEPENDENCIES_KEYS } from "./constants"; 5 | 6 | const isSorted = (list: string[]): boolean => 7 | list.slice(1).every((item, i) => list[i] <= item); 8 | 9 | const sortObjectKeysAlphabetically = ( 10 | obj: Record, 11 | ): Record => _(obj).toPairs().sortBy(0).fromPairs().value(); 12 | 13 | const rule: Rule.RuleModule = { 14 | meta: { 15 | type: "problem", 16 | messages: { 17 | unsortedDependencies: 18 | "Dependencies under the '{{ key }}' key are not alphabetically sorted", 19 | }, 20 | docs: { 21 | description: "sort dependencies alphabetically", 22 | category: "Possible Errors", 23 | recommended: true, 24 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/alphabetically-sorted-dependencies.md", 25 | }, 26 | schema: [], 27 | fixable: "code", 28 | }, 29 | create: function (context: Rule.RuleContext): Rule.RuleListener { 30 | return { 31 | "Program:exit": (node: Rule.Node) => { 32 | const filePath = context.getFilename(); 33 | 34 | if (!isPackageJsonFile(filePath)) { 35 | return; 36 | } 37 | 38 | const { text } = context.getSourceCode(); 39 | 40 | const packageJson = JSON.parse(text); 41 | 42 | DEPENDENCIES_KEYS.forEach((key) => { 43 | const dependenciesList = getDependenciesSafe(packageJson, key); 44 | 45 | if (!isSorted(dependenciesList)) { 46 | context.report({ 47 | node, 48 | messageId: "unsortedDependencies", 49 | data: { 50 | key, 51 | }, 52 | fix: (fixer: Rule.RuleFixer) => { 53 | const keyIndex = text.indexOf(`"${key}":`); 54 | const rangeStart = text.indexOf("{", keyIndex); 55 | const rangeEnd = text.indexOf("}", keyIndex) + 1; 56 | 57 | const fixedSourceWithoutIndentation = JSON.stringify( 58 | sortObjectKeysAlphabetically(packageJson[key]), 59 | null, 60 | 2, 61 | ); 62 | const fixedSource = fixedSourceWithoutIndentation 63 | .split("\n") 64 | .map((line, idx) => (idx === 0 ? line : ` ${line}`)) 65 | .join("\n"); 66 | 67 | return fixer.replaceTextRange( 68 | [rangeStart, rangeEnd], 69 | fixedSource, 70 | ); 71 | }, 72 | }); 73 | } 74 | }); 75 | }, 76 | }; 77 | }, 78 | }; 79 | 80 | export { rule }; 81 | -------------------------------------------------------------------------------- /src/rules/better-alternative.ts: -------------------------------------------------------------------------------- 1 | import { isPackageJsonFile, getDependenciesSafe } from "../utils"; 2 | import { Rule } from "eslint"; 3 | import _ from "lodash"; 4 | import { DEPENDENCIES_KEYS } from "./constants"; 5 | 6 | interface RuleOptions { 7 | alternatives: Record; 8 | } 9 | 10 | const rule: Rule.RuleModule = { 11 | meta: { 12 | type: "problem", 13 | messages: { 14 | betterAlternativeExists: 15 | "Replace '{{ package }}' with '{{ alternative }}'", 16 | }, 17 | docs: { 18 | description: "prefer certain packages over others", 19 | category: "Possible Errors", 20 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/better-alternative.md", 21 | }, 22 | schema: [ 23 | { 24 | type: "object", 25 | properties: { 26 | alternatives: { 27 | type: "object", 28 | patternProperties: { 29 | "^.+$": { type: "string" }, 30 | }, 31 | }, 32 | }, 33 | additionalProperties: false, 34 | }, 35 | ], 36 | }, 37 | create: function (context: Rule.RuleContext): Rule.RuleListener { 38 | return { 39 | "Program:exit": (node: Rule.Node) => { 40 | const filePath = context.getFilename(); 41 | 42 | if (!isPackageJsonFile(filePath)) { 43 | return; 44 | } 45 | 46 | const { text } = context.getSourceCode(); 47 | 48 | const packageJson = JSON.parse(text); 49 | 50 | const { alternatives = {} } = (context.options[0] || {}) as RuleOptions; 51 | const packagesToReplace = Object.keys(alternatives); 52 | 53 | DEPENDENCIES_KEYS.forEach((key) => { 54 | const dependenciesList = getDependenciesSafe(packageJson, key); 55 | 56 | dependenciesList.forEach((dependency) => { 57 | if (packagesToReplace.includes(dependency)) { 58 | context.report({ 59 | node, 60 | messageId: "betterAlternativeExists", 61 | data: { 62 | package: dependency, 63 | alternative: alternatives[dependency], 64 | }, 65 | }); 66 | } 67 | }); 68 | }); 69 | }, 70 | }; 71 | }, 72 | }; 73 | 74 | export { rule }; 75 | -------------------------------------------------------------------------------- /src/rules/constants.ts: -------------------------------------------------------------------------------- 1 | const DEPENDENCIES_KEYS = [ 2 | "dependencies", 3 | "devDependencies", 4 | "peerDependencies", 5 | "optionalDependencies", 6 | ]; 7 | 8 | export { DEPENDENCIES_KEYS }; 9 | -------------------------------------------------------------------------------- /src/rules/controlled-versions.ts: -------------------------------------------------------------------------------- 1 | import { isPackageJsonFile, isFileDependency, isGitDependency } from "../utils"; 2 | import { Rule } from "eslint"; 3 | import _ from "lodash"; 4 | import { parse as parseSemver, clean as cleanSemver } from "semver"; 5 | import { DEPENDENCIES_KEYS } from "./constants"; 6 | import { 7 | Dependencies, 8 | DependencyGranularity, 9 | GranularityOption, 10 | } from "../types"; 11 | import micromatch from "micromatch"; 12 | import { toControlledSemver } from "../to-controlled-semver"; 13 | 14 | const getGranularily = ( 15 | key: (typeof DEPENDENCIES_KEYS)[number], 16 | granularity?: GranularityOption, 17 | ): DependencyGranularity => { 18 | if (!granularity) { 19 | return "fixed"; 20 | } 21 | 22 | if (typeof granularity === "string") { 23 | return granularity; 24 | } 25 | 26 | return granularity[key] || "fixed"; 27 | }; 28 | 29 | const isFixedVersion = (version: string): boolean => { 30 | const cleanedSemver = cleanSemver(version); 31 | 32 | if (cleanedSemver === null) { 33 | return version.match(/^\d+$/) !== null; 34 | } 35 | 36 | return parseSemver(cleanedSemver) !== null; 37 | }; 38 | 39 | const isPatchOrLess = (version: string): boolean => 40 | isFixedVersion(version) || version.startsWith("~"); 41 | const isMinorOrLess = (version: string): boolean => 42 | isPatchOrLess(version) || version.startsWith("^"); 43 | 44 | const fix = ( 45 | text: string, 46 | key: string, 47 | dependency: string, 48 | versions: string[], 49 | isValid: (version: string) => boolean, 50 | granularity: DependencyGranularity, 51 | fixer: Rule.RuleFixer, 52 | ): Rule.Fix => { 53 | const keyIndex = text.indexOf(`"${key}":`); 54 | const packageIndex = text.indexOf(`"${dependency}":`, keyIndex); 55 | const rangeStart = text.indexOf(`"${versions[0]}`, packageIndex) + 1; 56 | const lastVersionStart = 57 | text.indexOf(`${versions[versions.length - 1]}`, packageIndex) + 1; 58 | const rangeEnd = text.indexOf('"', lastVersionStart); 59 | 60 | let versionsString = text.substring(rangeStart, rangeEnd); 61 | versions.forEach((version) => { 62 | if (!isValid(version)) { 63 | const fixedVersion = toControlledSemver(dependency, version, granularity); 64 | versionsString = versionsString.replace(version, fixedVersion); 65 | } 66 | }); 67 | 68 | return fixer.replaceTextRange([rangeStart, rangeEnd], versionsString); 69 | }; 70 | 71 | interface RuleOptions { 72 | granularity?: GranularityOption; 73 | excludePatterns: string[]; 74 | } 75 | 76 | const rule: Rule.RuleModule = { 77 | meta: { 78 | type: "problem", 79 | messages: { 80 | nonControlledDependency: 81 | "Non controlled version found for dependency '{{ package }}'", 82 | }, 83 | docs: { 84 | description: "detect uncontrolled dependencies versions", 85 | category: "Possible Errors", 86 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/controlled-versions.md", 87 | }, 88 | fixable: "code", 89 | schema: [ 90 | { 91 | type: "object", 92 | properties: { 93 | granularity: { 94 | anyOf: [ 95 | { 96 | type: "string", 97 | enum: ["fixed", "patch", "minor"], 98 | }, 99 | { 100 | type: "object", 101 | properties: { 102 | dependencies: { 103 | type: "string", 104 | enum: ["fixed", "patch", "minor"], 105 | }, 106 | devDependencies: { 107 | type: "string", 108 | enum: ["fixed", "patch", "minor"], 109 | }, 110 | peerDependencies: { 111 | type: "string", 112 | enum: ["fixed", "patch", "minor"], 113 | }, 114 | optionalDependencies: { 115 | type: "string", 116 | enum: ["fixed", "patch", "minor"], 117 | }, 118 | }, 119 | }, 120 | ], 121 | }, 122 | excludePatterns: { 123 | type: "array", 124 | items: { 125 | type: "string", 126 | }, 127 | }, 128 | }, 129 | additionalProperties: false, 130 | }, 131 | ], 132 | }, 133 | create: function (context: Rule.RuleContext): Rule.RuleListener { 134 | return { 135 | "Program:exit": (node: Rule.Node) => { 136 | const filePath = context.getFilename(); 137 | 138 | if (!isPackageJsonFile(filePath)) { 139 | return; 140 | } 141 | 142 | const { text } = context.getSourceCode(); 143 | 144 | const { granularity: maybeGranularity, excludePatterns = [] } = (context 145 | .options[0] || {}) as RuleOptions; 146 | 147 | const packageJson = JSON.parse(text); 148 | 149 | DEPENDENCIES_KEYS.forEach((key) => { 150 | const dependencies: Dependencies = packageJson[key] || {}; 151 | 152 | _(dependencies) 153 | .pickBy( 154 | (_, dependency) => 155 | micromatch([dependency], excludePatterns).length === 0, 156 | ) 157 | .omitBy( 158 | (version) => 159 | isGitDependency(version!) || isFileDependency(version!), 160 | ) 161 | .forEach((version, dependency) => { 162 | if (!version) { 163 | return; 164 | } 165 | 166 | const versions = version.split(/\|\|| - /).map((s) => s.trim()); 167 | const granularity = getGranularily(key, maybeGranularity); 168 | 169 | switch (granularity) { 170 | case "fixed": 171 | if (versions.some((v) => !isFixedVersion(v))) { 172 | context.report({ 173 | node, 174 | messageId: "nonControlledDependency", 175 | data: { 176 | package: dependency, 177 | }, 178 | fix: (fixer: Rule.RuleFixer) => 179 | fix( 180 | text, 181 | key, 182 | dependency, 183 | versions, 184 | isFixedVersion, 185 | granularity, 186 | fixer, 187 | ), 188 | }); 189 | } 190 | 191 | break; 192 | case "patch": 193 | if (versions.some((v) => !isPatchOrLess(v))) { 194 | context.report({ 195 | node, 196 | messageId: "nonControlledDependency", 197 | data: { 198 | package: dependency, 199 | }, 200 | fix: (fixer: Rule.RuleFixer) => 201 | fix( 202 | text, 203 | key, 204 | dependency, 205 | versions, 206 | isPatchOrLess, 207 | granularity, 208 | fixer, 209 | ), 210 | }); 211 | } 212 | 213 | break; 214 | case "minor": 215 | if (versions.some((v) => !isMinorOrLess(v))) { 216 | context.report({ 217 | node, 218 | messageId: "nonControlledDependency", 219 | data: { 220 | package: dependency, 221 | }, 222 | fix: (fixer: Rule.RuleFixer) => 223 | fix( 224 | text, 225 | key, 226 | dependency, 227 | versions, 228 | isMinorOrLess, 229 | granularity, 230 | fixer, 231 | ), 232 | }); 233 | } 234 | 235 | break; 236 | default: 237 | // unsupported granularity, ignoring. 238 | } 239 | }); 240 | }); 241 | }, 242 | }; 243 | }, 244 | }; 245 | 246 | export { rule }; 247 | -------------------------------------------------------------------------------- /src/rules/duplicate-dependencies.ts: -------------------------------------------------------------------------------- 1 | import { isPackageJsonFile, getDependenciesSafe } from "../utils"; 2 | import { Rule } from "eslint"; 3 | import _ from "lodash"; 4 | import { DEPENDENCIES_KEYS } from "./constants"; 5 | 6 | interface RuleOptions { 7 | exclude: string[]; 8 | } 9 | 10 | const rule: Rule.RuleModule = { 11 | meta: { 12 | type: "problem", 13 | messages: { 14 | duplicateDependencyFound: 15 | "dependency '{{ package }}' declared multiple times ({{ origins }})", 16 | }, 17 | docs: { 18 | description: "detect dependencies declared on multiple levels", 19 | category: "Possible Errors", 20 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/duplicate-dependencies.md", 21 | }, 22 | schema: [ 23 | { 24 | type: "object", 25 | properties: { 26 | exclude: { 27 | type: "array", 28 | items: { 29 | type: "string", 30 | }, 31 | }, 32 | }, 33 | additionalProperties: false, 34 | }, 35 | ], 36 | }, 37 | create: function (context: Rule.RuleContext): Rule.RuleListener { 38 | return { 39 | "Program:exit": (node: Rule.Node) => { 40 | const filePath = context.getFilename(); 41 | 42 | if (!isPackageJsonFile(filePath)) { 43 | return; 44 | } 45 | 46 | const { text } = context.getSourceCode(); 47 | 48 | const { exclude = [] } = (context.options[0] || {}) as RuleOptions; 49 | 50 | const packageJson = JSON.parse(text); 51 | const dependencies = {}; 52 | 53 | DEPENDENCIES_KEYS.forEach((key) => { 54 | const dependenciesList = getDependenciesSafe(packageJson, key); 55 | dependenciesList.forEach((dependency) => { 56 | if (!(dependency in dependencies)) { 57 | dependencies[dependency] = []; 58 | } 59 | 60 | dependencies[dependency].push(key); 61 | }); 62 | }); 63 | 64 | Object.keys(dependencies) 65 | .filter((dependency) => !exclude.includes(dependency)) 66 | .forEach((dependency) => { 67 | if (dependencies[dependency].length > 1) { 68 | context.report({ 69 | node, 70 | messageId: "duplicateDependencyFound", 71 | data: { 72 | package: dependency, 73 | origins: `[${dependencies[dependency].join(",")}]`, 74 | }, 75 | }); 76 | } 77 | }); 78 | }, 79 | }; 80 | }, 81 | }; 82 | 83 | export { rule }; 84 | -------------------------------------------------------------------------------- /src/rules/index.ts: -------------------------------------------------------------------------------- 1 | import { rule as noMissingTypes } from "./no-missing-types"; 2 | import { rule as alphabeticallySortedDependencies } from "./alphabetically-sorted-dependencies"; 3 | import { rule as controlledVersions } from "./controlled-versions"; 4 | import { rule as betterAlternative } from "./better-alternative"; 5 | import { rule as validVersions } from "./valid-versions"; 6 | import { rule as duplicateDependencies } from "./duplicate-dependencies"; 7 | 8 | const rules = { 9 | "no-missing-types": noMissingTypes, 10 | "alphabetically-sorted-dependencies": alphabeticallySortedDependencies, 11 | "controlled-versions": controlledVersions, 12 | "better-alternative": betterAlternative, 13 | "valid-versions": validVersions, 14 | "duplicate-dependencies": duplicateDependencies, 15 | }; 16 | 17 | export { rules }; 18 | -------------------------------------------------------------------------------- /src/rules/no-missing-types.ts: -------------------------------------------------------------------------------- 1 | import { isPackageJsonFile, getDependenciesSafe } from "../utils"; 2 | import { hasTypes } from "../has-types"; 3 | import { Rule } from "eslint"; 4 | import { groupBy } from "lodash"; 5 | import micromatch from "micromatch"; 6 | 7 | interface RuleOptions { 8 | excludePatterns: string[]; 9 | } 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | type: "problem", 14 | messages: { 15 | missingTypes: "Missing types for {{ package }}", 16 | }, 17 | docs: { 18 | description: "detect missing @types dependencies", 19 | category: "Possible Errors", 20 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/no-missing-types.md", 21 | }, 22 | schema: [ 23 | { 24 | type: "object", 25 | properties: { 26 | excludePatterns: { 27 | type: "array", 28 | items: { 29 | type: "string", 30 | }, 31 | }, 32 | }, 33 | additionalProperties: false, 34 | }, 35 | ], 36 | }, 37 | create: function (context: Rule.RuleContext): Rule.RuleListener { 38 | return { 39 | "Program:exit": (node: Rule.Node) => { 40 | const filePath = context.getFilename(); 41 | 42 | if (!isPackageJsonFile(filePath)) { 43 | return; 44 | } 45 | 46 | const cwd = context.getCwd(); 47 | const { text } = context.getSourceCode(); 48 | 49 | const { excludePatterns = [] } = (context.options[0] || 50 | {}) as RuleOptions; 51 | 52 | const packageJson = JSON.parse(text); 53 | 54 | const dependencies = [ 55 | ...getDependenciesSafe(packageJson, "devDependencies"), 56 | ...getDependenciesSafe(packageJson, "dependencies"), 57 | ]; 58 | 59 | const { 60 | true: typesDependencies = [], 61 | false: allCodeDependencies = [], 62 | } = groupBy(dependencies, (dependency) => 63 | dependency.startsWith("@types/"), 64 | ); 65 | 66 | const codeDependencies = allCodeDependencies.filter( 67 | (dependency) => 68 | micromatch([dependency], excludePatterns).length === 0, 69 | ); 70 | 71 | for (const dependency of codeDependencies) { 72 | if (!hasTypes(cwd, dependency, typesDependencies)) { 73 | context.report({ 74 | node, 75 | messageId: "missingTypes", 76 | data: { 77 | package: dependency, 78 | }, 79 | }); 80 | } 81 | } 82 | }, 83 | }; 84 | }, 85 | }; 86 | 87 | export { rule }; 88 | -------------------------------------------------------------------------------- /src/rules/valid-versions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPackageJsonFile, 3 | isFileDependency, 4 | isGitDependency, 5 | isWorkspaceDependency, 6 | isDistTagDependency, 7 | resolveDistTag, 8 | } from "../utils"; 9 | import { Rule } from "eslint"; 10 | import _ from "lodash"; 11 | import { valid as validSemver, validRange as validSemverRange } from "semver"; 12 | import { DEPENDENCIES_KEYS } from "./constants"; 13 | import { Dependencies } from "../types"; 14 | 15 | const rule: Rule.RuleModule = { 16 | meta: { 17 | type: "problem", 18 | messages: { 19 | invalidVersionDetected: 20 | "Invalid version found for dependency '{{ package }}' ({{ reason }})", 21 | }, 22 | docs: { 23 | description: "detect invalid dependencies versions", 24 | category: "Possible Errors", 25 | url: "https://github.com/idan-at/eslint-plugin-package-json-dependencies/blob/master/docs/rules/valid-versions.md", 26 | }, 27 | schema: [ 28 | { 29 | type: "object", 30 | properties: {}, 31 | additionalProperties: false, 32 | }, 33 | ], 34 | }, 35 | create: function (context: Rule.RuleContext): Rule.RuleListener { 36 | return { 37 | "Program:exit": (node: Rule.Node) => { 38 | const filePath = context.getFilename(); 39 | 40 | if (!isPackageJsonFile(filePath)) { 41 | return; 42 | } 43 | 44 | const { text } = context.getSourceCode(); 45 | 46 | const packageJson = JSON.parse(text); 47 | 48 | const reportUnlessValidVersion = ( 49 | dependency: string, 50 | version: string, 51 | ) => { 52 | if (isDistTagDependency(version)) { 53 | try { 54 | resolveDistTag(dependency, version); 55 | } catch { 56 | context.report({ 57 | node, 58 | messageId: "invalidVersionDetected", 59 | data: { 60 | package: dependency, 61 | reason: "dist tag does not exist", 62 | }, 63 | }); 64 | } 65 | } else if (!validSemver(version) && !validSemverRange(version)) { 66 | context.report({ 67 | node, 68 | messageId: "invalidVersionDetected", 69 | data: { 70 | package: dependency, 71 | reason: "invalid version format", 72 | }, 73 | }); 74 | } 75 | }; 76 | 77 | DEPENDENCIES_KEYS.forEach((key) => { 78 | const dependencies: Dependencies = packageJson[key] || {}; 79 | 80 | _(dependencies) 81 | .omitBy( 82 | (version) => 83 | isGitDependency(version!) || isFileDependency(version!), 84 | ) 85 | .forEach((version, dependency) => { 86 | if (!version) { 87 | return; 88 | } 89 | 90 | if (isWorkspaceDependency(version)) { 91 | const extractedVersion = version.replace("workspace:", ""); 92 | if (extractedVersion.startsWith(" ")) { 93 | context.report({ 94 | node, 95 | messageId: "invalidVersionDetected", 96 | data: { 97 | package: dependency, 98 | reason: "space detected after worksapce protocol", 99 | }, 100 | }); 101 | } else { 102 | reportUnlessValidVersion(dependency, extractedVersion); 103 | } 104 | } else { 105 | reportUnlessValidVersion(dependency, version); 106 | } 107 | }); 108 | }); 109 | }, 110 | }; 111 | }, 112 | }; 113 | 114 | export { rule }; 115 | -------------------------------------------------------------------------------- /src/to-controlled-semver.ts: -------------------------------------------------------------------------------- 1 | import { DependencyGranularity } from "./types"; 2 | import { execSync } from "child_process"; 3 | import { resolveDistTag } from "./utils"; 4 | 5 | function getLatestVersion(packageName: string): string { 6 | try { 7 | const stdout = execSync( 8 | `npm view ${packageName} version --json`, 9 | ).toString(); 10 | 11 | return JSON.parse(stdout.trim()); 12 | } catch (e) { 13 | throw new Error(`package '${packageName}' does not exist`); 14 | } 15 | } 16 | 17 | const removeRange = (semver: string): string => semver.replace(/[~^>=<]+/, ""); 18 | 19 | const resolveVersion = ( 20 | packageName: string, 21 | semver: string, 22 | ): string | undefined => { 23 | const cleanSemver = removeRange(semver); 24 | 25 | if (semver === "*") { 26 | return getLatestVersion(packageName); 27 | } else if (cleanSemver === semver) { 28 | // This means semver is a dist-tag 29 | return resolveDistTag(packageName, semver); 30 | } 31 | 32 | return cleanSemver; 33 | }; 34 | 35 | const toControlledSemver = ( 36 | packageName: string, 37 | semver: string, 38 | granularity: DependencyGranularity, 39 | ): string => { 40 | const resolvedSemver = resolveVersion(packageName, semver) || ""; 41 | switch (granularity) { 42 | case "fixed": 43 | return resolvedSemver; 44 | case "patch": 45 | return `~${resolvedSemver}`; 46 | case "minor": 47 | return `^${resolvedSemver}`; 48 | default: 49 | // Unexpected granularity 50 | return semver; 51 | } 52 | }; 53 | 54 | export { toControlledSemver }; 55 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "lodash"; 2 | 3 | interface Dependencies extends Dictionary { 4 | [index: string]: string; 5 | } 6 | 7 | type DependencyGranularity = "fixed" | "patch" | "minor"; 8 | 9 | type GranularityOption = 10 | | DependencyGranularity 11 | | { 12 | dependencies?: DependencyGranularity; 13 | devDependencies?: DependencyGranularity; 14 | peerDependencies?: DependencyGranularity; 15 | optionalDependencies?: DependencyGranularity; 16 | }; 17 | 18 | export { Dependencies, DependencyGranularity, GranularityOption }; 19 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Dependencies } from "./types"; 3 | import { execSync } from "child_process"; 4 | 5 | const DIST_TAGS_REGEX = new RegExp("^[a-zA-Z0-9-_]+$"); 6 | 7 | const isPackageJsonFile = (filePath: string): boolean => 8 | path.basename(filePath) === "package.json"; 9 | 10 | const getDependenciesSafe = ( 11 | object: Record, 12 | key: string, 13 | ): string[] => Object.keys(object[key] || {}) || []; 14 | 15 | const isGitDependency = (version: string): boolean => version.startsWith("git"); 16 | const isDistTagDependency = (version: string): boolean => 17 | DIST_TAGS_REGEX.test(version); 18 | const isFileDependency = (version: string): boolean => 19 | version.startsWith("file"); 20 | const isWorkspaceDependency = (version: string): boolean => 21 | version.startsWith("workspace"); 22 | 23 | function resolveDistTag(packageName: string, distTag: string): string { 24 | try { 25 | const stdout = execSync( 26 | `npm view ${packageName} dist-tags --json`, 27 | ).toString(); 28 | 29 | const tag = JSON.parse(stdout.trim())[distTag]; 30 | if (tag == undefined) { 31 | throw new Error( 32 | `tag '${distTag}' not found for package '${packageName}'`, 33 | ); 34 | } 35 | 36 | return tag; 37 | } catch (e) { 38 | if (e.message.includes("not found for package")) { 39 | throw e; 40 | } 41 | 42 | throw new Error(`package '${packageName}' does not exist`); 43 | } 44 | } 45 | 46 | export { 47 | isPackageJsonFile, 48 | getDependenciesSafe, 49 | isGitDependency, 50 | isDistTagDependency, 51 | isFileDependency, 52 | isWorkspaceDependency, 53 | resolveDistTag, 54 | }; 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": ["es2020"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "strictFunctionTypes": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "composite": true, 12 | "strictNullChecks": true, 13 | "target": "es2020", 14 | "types": ["node", "jest"], 15 | "outDir": "dist" 16 | }, 17 | "include": ["index.ts", "src/**/*"] 18 | } 19 | --------------------------------------------------------------------------------