├── .gitattributes ├── .gitignore ├── tests ├── fixtures │ ├── volta │ │ ├── not-json.js │ │ ├── no-volta.json │ │ └── with-volta.json │ └── workspaces │ │ └── foo │ │ ├── baz │ │ └── .gitkeep │ │ └── bar │ │ └── package.json ├── helpers │ ├── mocha.js │ ├── chai.js │ └── preprocess.js └── lib │ ├── rules │ ├── eol-last.js │ ├── require-license.js │ ├── ensure-repository-directory.js │ ├── require-engines.js │ ├── ensure-volta-extends.js │ ├── no-branch-in-dependencies.js │ ├── ensure-workspaces.js │ ├── require-unique-dependency-names.js │ ├── sort-package-json.js │ ├── validate-schema.js │ └── restrict-ranges.js │ └── processors │ └── json.js ├── .npmrc ├── .mocharc.js ├── .editorconfig ├── lib ├── rules │ ├── eol-last.js │ ├── require-license.js │ ├── require-engines.js │ ├── sort-package-json.js │ ├── ensure-repository-directory.js │ ├── no-branch-in-dependencies.js │ ├── ensure-workspaces.js │ ├── require-unique-dependency-names.js │ ├── ensure-volta-extends.js │ ├── validate-schema.js │ └── restrict-ranges.js ├── index.js └── processors │ └── json.js ├── docs └── rules │ ├── eol-last.md │ ├── sort-package-json.md │ ├── ensure-volta-extends.md │ ├── require-license.md │ ├── require-engines.md │ ├── ensure-repository-directory.md │ ├── require-unique-dependency-names.md │ ├── no-branch-in-dependencies.md │ ├── ensure-workspaces.md │ ├── validate-schema.md │ └── restrict-ranges.md ├── renovate.json ├── eslint.config.js ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── .vscode └── launch.json ├── config └── ember-cli-update.json ├── LICENSE ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /tests/fixtures/volta/not-json.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/volta/no-volta.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/workspaces/foo/baz/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/workspaces/foo/bar/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/volta/with-volta.json: -------------------------------------------------------------------------------- 1 | { 2 | "volta": {} 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | 3 | omit=peer 4 | 5 | ignore-scripts=true 6 | -------------------------------------------------------------------------------- /tests/helpers/mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha-helpers')(module); 4 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | spec: ['tests'], 5 | recursive: true, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/helpers/chai.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | 5 | module.exports = chai; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /lib/rules/eol-last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { builtinRules } = require('eslint/use-at-your-own-risk'); 4 | 5 | module.exports = builtinRules.get('eol-last'); 6 | -------------------------------------------------------------------------------- /docs/rules/eol-last.md: -------------------------------------------------------------------------------- 1 | # require or disallow newline at the end of package.json (eol-last) 2 | 3 | Same as https://eslint.org/docs/latest/rules/eol-last, but works on JSON files. 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard" 4 | ], 5 | "packageRules": [ 6 | { 7 | "matchPackageNames": [ 8 | "standard-node-template", 9 | "@kellyselden/node-template" 10 | ], 11 | "separateMinorPatch": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | defineConfig, 5 | } = require('eslint/config'); 6 | 7 | const config = require('@kellyselden/eslint-config'); 8 | 9 | module.exports = defineConfig([ 10 | config, 11 | { 12 | ...config.find(c => c.name === 'mocha/recommended'), 13 | files: [ 14 | 'tests/**/*.js', 15 | ], 16 | }, 17 | ]); 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: v[0-9]+.[0-9]+.[0-9]+ 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v5 13 | 14 | - uses: actions/setup-node@v5 15 | with: 16 | node-version: 20 17 | 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /tests/helpers/preprocess.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const processor = require('../../lib/processors/json'); 4 | 5 | // RuleTester doesn't allow preprocessors 6 | function _preprocess(item) { 7 | item.code = processor.preprocess(item.code, item.filename)[0]; 8 | if (item.output) { 9 | item.output = processor.preprocess(item.output, item.filename)[0]; 10 | } 11 | return item; 12 | } 13 | 14 | function preprocess(tests) { 15 | for (let type of Object.keys(tests)) { 16 | tests[type] = tests[type].map(_preprocess); 17 | } 18 | return tests; 19 | } 20 | 21 | module.exports = preprocess; 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--timeout", 14 | "999999", 15 | "--colors", 16 | "tests", 17 | "--recursive" 18 | ], 19 | "internalConsoleOptions": "openOnSessionStart" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "standard-node-template", 6 | "version": "7.2.0", 7 | "blueprints": [ 8 | { 9 | "name": "standard-node-template", 10 | "isBaseBlueprint": true 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@kellyselden/node-template", 16 | "version": "6.11.1", 17 | "blueprints": [ 18 | { 19 | "name": "@kellyselden/node-template", 20 | "options": [ 21 | "--repo-slug=kellyselden/eslint-plugin-json-files", 22 | "--github-actions", 23 | "--renovate" 24 | ] 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /docs/rules/sort-package-json.md: -------------------------------------------------------------------------------- 1 | # enforce package.json sorting (sort-package-json) 2 | 3 | Use [sort-package-json](https://www.npmjs.com/package/sort-package-json) to keep your keys in a predictable order. 4 | 5 | ## Rule Details 6 | 7 | This rule aims to enforce sorting. 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```json 12 | { 13 | "name": "foo", 14 | "version": "1.0.0" 15 | } 16 | ``` 17 | 18 | Examples of **correct** code for this rule: 19 | 20 | ```json 21 | { 22 | "version": "1.0.0", 23 | "name": "foo" 24 | } 25 | ``` 26 | 27 | ### Options 28 | 29 | Same as . 30 | 31 | ## When Not To Use It 32 | 33 | If you don't like the defaults of `sort-package-json`, you may not want this rule. 34 | 35 | ## Further Reading 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/rules/ensure-volta-extends.md: -------------------------------------------------------------------------------- 1 | # Ensure volta/extends in package.json (ensure-repository-directory) 2 | 3 | Check that the volta/extends file exists and has a volta config. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to ensure the volta/extends file exists and has a volta config. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "volta": { 15 | "extends": "wrong-package/bad-copy-paste" 16 | } 17 | } 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```json 23 | { 24 | "volta": { 25 | "extends": "correct-path" 26 | } 27 | } 28 | ``` 29 | 30 | ```json 31 | { 32 | "volta": {} 33 | } 34 | ``` 35 | 36 | ### Options 37 | 38 | 39 | 40 | ## When Not To Use It 41 | 42 | Extending is optional. If you aren't using it, you may not want this rule. 43 | 44 | ## Further Reading 45 | 46 | https://volta.sh 47 | -------------------------------------------------------------------------------- /docs/rules/require-license.md: -------------------------------------------------------------------------------- 1 | # require a license in package.json (require-license) 2 | 3 | Enforce the license of a project 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to prevent publishing an unlicensed package. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "description": "missing license" 15 | } 16 | ``` 17 | 18 | ```json 19 | { 20 | "license": "UNLICENSED" 21 | } 22 | ``` 23 | 24 | Examples of **correct** code for this rule: 25 | 26 | ```json 27 | { 28 | "license": "MIT" 29 | } 30 | ``` 31 | 32 | ### Options 33 | 34 | * `"always"` (default) requires a string other than "UNLICENSED" 35 | * `"allow-unlicensed"` allows the string "UNLICENSED" 36 | 37 | ## When Not To Use It 38 | 39 | If this is a private package or app that isn't published, you may not want this rule. 40 | 41 | ## Further Reading 42 | 43 | https://docs.npmjs.com/files/package.json#license 44 | -------------------------------------------------------------------------------- /docs/rules/require-engines.md: -------------------------------------------------------------------------------- 1 | # require the engines field in package.json (require-engines) 2 | 3 | Enforce the engine of a project 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to enforce the engine of a package. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "description": "missing engines" 15 | } 16 | ``` 17 | 18 | ```json 19 | { 20 | "engines": {} 21 | } 22 | ``` 23 | 24 | Examples of **correct** code for this rule: 25 | 26 | ```json 27 | { 28 | "engines": { 29 | "node": ">=8" 30 | } 31 | } 32 | ``` 33 | 34 | ### Options 35 | 36 | * `"node-only"` (default) requires a string other than "UNLICENSED" 37 | * `"require-npm"` allows the string "UNLICENSED" 38 | 39 | ## When Not To Use It 40 | 41 | If this is local project, and you're always using the latest node version, you may not want this rule. 42 | 43 | ## Further Reading 44 | 45 | https://docs.npmjs.com/files/package.json#engines 46 | -------------------------------------------------------------------------------- /docs/rules/ensure-repository-directory.md: -------------------------------------------------------------------------------- 1 | # Ensure repository/directory in package.json (ensure-repository-directory) 2 | 3 | Check that the repository/directory is the same as where the package.json lives. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to ensure the repository/directory of a package. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "repository": { 15 | "directory": "wrong-package/bad-copy-paste" 16 | } 17 | } 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```json 23 | { 24 | "repository": { 25 | "directory": "correct-path" 26 | } 27 | } 28 | ``` 29 | 30 | ```json 31 | { 32 | "repository": "no-directory" 33 | } 34 | ``` 35 | 36 | ### Options 37 | 38 | 39 | 40 | ## When Not To Use It 41 | 42 | Using the directory is optional. If you aren't using it, you may not want this rule. 43 | 44 | ## Further Reading 45 | 46 | https://docs.npmjs.com/files/package.json#repository 47 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const requireIndex = require('requireindex'); 4 | const path = require('path'); 5 | const { name, version } = require('../package.json'); 6 | 7 | module.exports.rules = requireIndex(path.join(__dirname, 'rules')); 8 | 9 | // optional for our current use case but required if we ever want to use 10 | // -cache and --print-config command line options. 11 | // https://eslint.org/docs/latest/extend/plugin-migration-flat-config#adding-plugin-meta-information 12 | module.exports.meta = { 13 | name, 14 | version, 15 | }, 16 | 17 | module.exports.processors = { 18 | // .json is from previous versions, so we are leaving it in 19 | // for backward compatibility with < v9 eslint versions 20 | '.json': require('./processors/json'), 21 | // dot prefix is no longer allowed in eslint v9 22 | // https://eslint.org/docs/latest/extend/plugin-migration-flat-config#migrating-processors-for-flat-config 23 | 'json': require('./processors/json'), 24 | }; 25 | -------------------------------------------------------------------------------- /docs/rules/require-unique-dependency-names.md: -------------------------------------------------------------------------------- 1 | # prevent duplicate packages in dependencies and devDependencies (require-unique-dependency-names) 2 | 3 | NPM and Yarn may only warn about this, but it could be better to avoid altogether. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to ensure the uniqueness of a package. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "dependencies": { 15 | "foo": "0.0.0" 16 | }, 17 | "devDependencies": { 18 | "foo": "0.0.0" 19 | } 20 | } 21 | ``` 22 | 23 | Examples of **correct** code for this rule: 24 | 25 | ```json 26 | { 27 | "dependencies": { 28 | "foo": "0.0.0" 29 | } 30 | } 31 | ``` 32 | 33 | ```json 34 | { 35 | "dependencies": { 36 | "foo": "0.0.0" 37 | }, 38 | "devDependencies": { 39 | "bar": "0.0.0" 40 | } 41 | } 42 | ``` 43 | 44 | ### Options 45 | 46 | 47 | 48 | ## When Not To Use It 49 | 50 | Since it doesn't hurt to have this problem, you could ignore it. 51 | 52 | -------------------------------------------------------------------------------- /tests/lib/rules/eol-last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/eol-last'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('eol-last', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: `{} 11 | `, 12 | filename: 'package.json', 13 | }, 14 | { 15 | code: '{}', 16 | filename: 'package.json', 17 | options: ['never'], 18 | }, 19 | ], 20 | invalid: [ 21 | { 22 | code: '{}', 23 | filename: 'package.json', 24 | errors: [{ 25 | message: rule.meta.messages.missing, 26 | type: Object.keys(rule.create())[0], 27 | }], 28 | output: `{} 29 | `, 30 | }, 31 | { 32 | code: `{} 33 | `, 34 | filename: 'package.json', 35 | options: ['never'], 36 | errors: [{ 37 | message: rule.meta.messages.unexpected, 38 | type: Object.keys(rule.create())[0], 39 | }], 40 | output: '{}', 41 | }, 42 | ], 43 | })); 44 | -------------------------------------------------------------------------------- /docs/rules/no-branch-in-dependencies.md: -------------------------------------------------------------------------------- 1 | # prevent branches in package.json dependencies (no-branch-in-dependencies) 2 | 3 | Sometimes you may accidentally commit a temporary branch of a dependency instead of a version. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to prevent branches instead of versions. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "dependencies": { 15 | "lodash": "lodash/lodash" 16 | } 17 | } 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```json 23 | { 24 | "dependencies": { 25 | "lodash": "^4.17.11" 26 | } 27 | } 28 | ``` 29 | 30 | ### Options 31 | 32 | * `"keys": ["dependencies", "devDependencies", "optionalDependencies"]` alter the dependency keys checked 33 | * `"ignore": []` add certain dependencies to the ignore list 34 | 35 | ## When Not To Use It 36 | 37 | If you use long-lived branches as part of your normal workflow, you may not want this rule. 38 | 39 | ## Further Reading 40 | 41 | https://docs.npmjs.com/files/package.json#dependencies 42 | -------------------------------------------------------------------------------- /tests/lib/rules/require-license.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/require-license'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('require-license', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "license": "MIT" }', 11 | filename: 'package.json', 12 | }, 13 | { 14 | code: '{ "license": "UNLICENSED" }', 15 | filename: 'package.json', 16 | options: ['allow-unlicensed'], 17 | }, 18 | { 19 | code: '{}', 20 | filename: 'not-package.json', 21 | }, 22 | ], 23 | invalid: [ 24 | { 25 | code: '{}', 26 | filename: 'package.json', 27 | errors: [{ 28 | message: 'Missing license.', 29 | type: 'ObjectExpression', 30 | }], 31 | }, 32 | { 33 | code: '{ "license": "UNLICENSED" }', 34 | filename: 'package.json', 35 | errors: [{ 36 | message: 'Missing license.', 37 | type: 'Literal', 38 | }], 39 | }, 40 | ], 41 | })); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kelly Selden 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 | -------------------------------------------------------------------------------- /docs/rules/ensure-workspaces.md: -------------------------------------------------------------------------------- 1 | # Ensure workspace globs in package.json resolve to directories (ensure-workspaces) 2 | 3 | Check that the monorepo workspace globs find dirs with package.json files. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to ensure the workspace globs find dirs with package.json files. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | { 14 | "workspace": [ 15 | "packages/*/missing-dir" 16 | ] 17 | } 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```json 23 | { 24 | "workspace": [ 25 | "packages/*/dir-with-package-json" 26 | ] 27 | } 28 | ``` 29 | 30 | ```json 31 | { 32 | "workspace": [ 33 | "packages/dir-with-package-json" 34 | ] 35 | } 36 | ``` 37 | 38 | ```json 39 | { 40 | "workspace": { 41 | "packages": [ 42 | "packages/*/dir-with-package-json" 43 | ] 44 | } 45 | } 46 | ``` 47 | 48 | ### Options 49 | 50 | 51 | 52 | ## When Not To Use It 53 | 54 | If workspace globs are placeholders for future packages. 55 | 56 | ## Further Reading 57 | 58 | https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces 59 | -------------------------------------------------------------------------------- /lib/rules/require-license.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | meta: { 7 | docs: { 8 | description: 'require a license in package.json', 9 | }, 10 | schema: [ 11 | { 12 | 'enum': ['always', 'allow-unlicensed'], 13 | }, 14 | ], 15 | }, 16 | 17 | create(context) { 18 | let filename = context.getFilename(); 19 | if (path.basename(filename) !== 'package.json') { 20 | return {}; 21 | } 22 | 23 | let allowUnlicensed = context.options[0] === 'allow-unlicensed'; 24 | 25 | return { 26 | AssignmentExpression(node) { 27 | let json = node.right; 28 | let property = json.properties.find(p => p.key.value === 'license'); 29 | if (!property) { 30 | context.report({ 31 | node: json, 32 | message: 'Missing license.', 33 | }); 34 | return; 35 | } 36 | let license = property.value; 37 | if (license.value === 'UNLICENSED' && !allowUnlicensed) { 38 | context.report({ 39 | node: license, 40 | message: 'Missing license.', 41 | }); 42 | } 43 | }, 44 | }; 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-json-files", 3 | "version": "5.1.2", 4 | "description": "ESLint JSON processor and rules", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/kellyselden/eslint-plugin-json-files.git" 13 | }, 14 | "license": "MIT", 15 | "author": "Kelly Selden", 16 | "main": "lib/index.js", 17 | "files": [ 18 | "lib" 19 | ], 20 | "scripts": { 21 | "lint": "eslint", 22 | "test": "mocha" 23 | }, 24 | "dependencies": { 25 | "ajv": "^8.2.0", 26 | "better-ajv-errors": "^2.0.0", 27 | "fast-glob": "^3.3.2", 28 | "requireindex": "^1.2.0", 29 | "semver": "^7.0.0", 30 | "sort-package-json": "^1.22.1" 31 | }, 32 | "devDependencies": { 33 | "@kellyselden/eslint-config": "^1.0.0", 34 | "@kellyselden/node-template": "6.11.1", 35 | "chai": "^4.5.0", 36 | "eslint": "^9.34.0", 37 | "mocha": "^11.7.1", 38 | "mocha-helpers": "^10.2.1", 39 | "renovate-config-standard": "2.4.2", 40 | "standard-node-template": "7.2.0", 41 | "strip-ansi": "^6.0.1" 42 | }, 43 | "peerDependencies": { 44 | "eslint": ">=5" 45 | }, 46 | "engines": { 47 | "node": ">=20.9" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # allow manual running 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v5 17 | 18 | - uses: actions/setup-node@v5 19 | with: 20 | node-version: 20 21 | 22 | - run: npm ci 23 | 24 | - run: npm run lint 25 | 26 | test: 27 | needs: lint 28 | 29 | strategy: 30 | matrix: 31 | os: 32 | - ubuntu-latest 33 | - windows-latest 34 | node: 35 | - 20 36 | 37 | runs-on: ${{ matrix.os }} 38 | 39 | steps: 40 | - uses: actions/checkout@v5 41 | 42 | - uses: actions/setup-node@v5 43 | with: 44 | node-version: ${{ matrix.node }} 45 | 46 | - run: npm ci 47 | 48 | - run: npm test 49 | 50 | ember-cli-update: 51 | needs: test 52 | if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository 53 | 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - uses: actions/checkout@v5 58 | with: 59 | ref: ${{ github.head_ref }} 60 | token: ${{ secrets.GitHubToken }} 61 | 62 | - uses: actions/setup-node@v5 63 | with: 64 | node-version: 20 65 | 66 | - uses: kellyselden/ember-cli-update-action@v7 67 | with: 68 | autofix_command: npm run lint -- --fix 69 | ignore_to: true 70 | -------------------------------------------------------------------------------- /lib/rules/require-engines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | meta: { 7 | docs: { 8 | description: 'require a license in package.json', 9 | }, 10 | schema: [ 11 | { 12 | 'enum': ['node-only', 'require-npm'], 13 | }, 14 | ], 15 | }, 16 | 17 | create(context) { 18 | let filename = context.getFilename(); 19 | if (path.basename(filename) !== 'package.json') { 20 | return {}; 21 | } 22 | 23 | let requireNpm = context.options[0] === 'require-npm'; 24 | 25 | return { 26 | AssignmentExpression(node) { 27 | let json = node.right; 28 | let property = json.properties.find(p => p.key.value === 'engines'); 29 | if (!property) { 30 | context.report({ 31 | node: json, 32 | message: 'Missing engines.', 33 | }); 34 | return; 35 | } 36 | let engines = property.value; 37 | if (!engines.properties || !engines.properties.some(p => p.key.value === 'node')) { 38 | context.report({ 39 | node: engines, 40 | message: 'Missing node engine.', 41 | }); 42 | return; 43 | } 44 | if (requireNpm && !engines.properties.some(p => p.key.value === 'npm')) { 45 | context.report({ 46 | node: engines, 47 | message: 'Missing npm engine.', 48 | }); 49 | } 50 | }, 51 | }; 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /docs/rules/validate-schema.md: -------------------------------------------------------------------------------- 1 | # require a valid JSON Schema in package.json (validate-schema) 2 | 3 | Require a valid JSON Schema. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to prevent your JSON getting in a bad state. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```json 13 | /* eslint validate-schema: [{ 14 | schema: JSON.stringify({ 15 | "$schema": "https://json-schema.org/draft/2020-12/schema", 16 | "type": "object", 17 | "not": { "required": ["description"] } 18 | }) 19 | }] */ 20 | 21 | { 22 | "description": "hello" 23 | } 24 | ``` 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | ```json 29 | /* eslint validate-schema: [{ 30 | schema: JSON.stringify({ 31 | "$schema": "https://json-schema.org/draft/2020-12/schema", 32 | "type": "object", 33 | "properties": { 34 | "foo": { "type": "string" } 35 | } 36 | }) 37 | }] */ 38 | 39 | { 40 | "description": "hello" 41 | } 42 | ``` 43 | 44 | ### Options 45 | 46 | An options object of: 47 | 48 | * `"schema"` a string of your JSON Schema 49 | * `"prettyErrors"` on by default. Set this to false if you want more machine-readable errors. 50 | * `"avjFixerOptions"` if you want to autofix some issues. Use this for supported fixers https://ajv.js.org/options.html#options-to-modify-validated-data. 51 | 52 | ## When Not To Use It 53 | 54 | If you don't want to enforce or prevent any properties, you may not want this rule. 55 | 56 | ## Further Reading 57 | 58 | https://json-schema.org 59 | -------------------------------------------------------------------------------- /lib/processors/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | 5 | const prefix = 'module.exports = '; 6 | 7 | module.exports = { 8 | _prefix: prefix, 9 | preprocess(text/* , fileName */) { 10 | // if (path.basename(fileName) !== 'package.json') { 11 | // return []; 12 | // } 13 | return [`${prefix}${text}`]; 14 | }, 15 | postprocess(messages/* , fileName */) { 16 | return messages.reduce((total, next) => { 17 | // disable js rules running on json files 18 | // this becomes too noisey, and splitting js and json 19 | // into separate overrides so neither inherit the other 20 | // is lame 21 | // revisit once https://github.com/eslint/rfcs/pull/9 lands 22 | // return total.concat(next); 23 | 24 | return total.concat(next.filter(error => { 25 | return error.ruleId && error.ruleId.startsWith('json-files/'); 26 | })); 27 | }, []).map(message => { 28 | // message.fix.range refers to indices in the processed source text, so update them to refer to the correct 29 | // indices in the raw source text as documented here: 30 | // https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins 31 | return message.fix ? { 32 | ...message, 33 | fix: { 34 | ...message.fix, 35 | range: message.fix.range.map(rangePart => Math.max(0, rangePart - prefix.length)), 36 | }, 37 | } : message; 38 | }); 39 | }, 40 | supportsAutofix: true, 41 | }; 42 | -------------------------------------------------------------------------------- /lib/rules/sort-package-json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const sortPackageJson = require('sort-package-json'); 5 | 6 | module.exports = { 7 | meta: { 8 | docs: { 9 | description: 'Enforce package.json sorting', 10 | }, 11 | fixable: 'code', 12 | schema: [{ 13 | type: 'object', 14 | properties: { 15 | sortOrder: { 16 | type: 'array', 17 | minItems: 0, 18 | items: { 19 | type: 'string', 20 | }, 21 | }, 22 | }, 23 | additionalProperties: false, 24 | }], 25 | }, 26 | 27 | create(context) { 28 | let filename = context.getFilename(); 29 | if (path.basename(filename) !== 'package.json') { 30 | return {}; 31 | } 32 | 33 | let sourceCode = context.getSourceCode(); 34 | let options = context.options ? context.options[0] : undefined; 35 | 36 | return { 37 | AssignmentExpression(node) { 38 | let packageJsonNode = node.right; 39 | let packageJsonText = sourceCode.text.substring(...packageJsonNode.range); 40 | let sortedText = sortPackageJson(packageJsonText, options); 41 | 42 | if (packageJsonText !== sortedText) { 43 | context.report({ 44 | node: packageJsonNode, 45 | message: 'package.json is not sorted correctly.', 46 | fix(fixer) { 47 | return fixer.replaceText(packageJsonNode, sortedText); 48 | }, 49 | }); 50 | } 51 | }, 52 | }; 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /tests/lib/rules/ensure-repository-directory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/ensure-repository-directory'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('ensure-repository-directory', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "repository": { "directory": "foo/bar" } }', 11 | filename: 'foo/bar/package.json', 12 | }, 13 | { 14 | code: '{ "repository": { "directory": "bar/baz" } }', 15 | filename: 'foo/bar/baz/package.json', 16 | }, 17 | { 18 | code: '{ "repository": "" }', 19 | filename: 'package.json', 20 | }, 21 | ], 22 | invalid: [ 23 | { 24 | code: '{ "repository": { "directory": "wrong/dir" } }', 25 | filename: 'foo/bar/package.json', 26 | errors: [{ 27 | message: 'repository/directory does not match actual location.', 28 | type: 'Literal', 29 | }], 30 | }, 31 | { 32 | code: '{ "repository": { "directory": "foo/bar/baz" } }', 33 | filename: 'bar/baz/package.json', 34 | errors: [{ 35 | message: 'repository/directory does not match actual location.', 36 | type: 'Literal', 37 | }], 38 | }, 39 | { 40 | code: '{ "repository": { "directory": "/foo/bar" } }', 41 | filename: 'wrong-root/foo/bar/package.json', 42 | errors: [{ 43 | message: 'repository/directory does not match actual location.', 44 | type: 'Literal', 45 | }], 46 | }, 47 | ], 48 | })); 49 | -------------------------------------------------------------------------------- /tests/lib/rules/require-engines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/require-engines'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('require-engines', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "engines": { "node": ">=8" } }', 11 | filename: 'package.json', 12 | }, 13 | { 14 | code: '{ "engines": { "node": ">=8", "npm": ">=5" } }', 15 | filename: 'package.json', 16 | options: ['require-npm'], 17 | }, 18 | { 19 | code: '{}', 20 | filename: 'not-package.json', 21 | }, 22 | ], 23 | invalid: [ 24 | { 25 | code: '{}', 26 | filename: 'package.json', 27 | errors: [{ 28 | message: 'Missing engines.', 29 | type: 'ObjectExpression', 30 | }], 31 | }, 32 | { 33 | code: '{ "engines": {} }', 34 | filename: 'package.json', 35 | errors: [{ 36 | message: 'Missing node engine.', 37 | type: 'ObjectExpression', 38 | }], 39 | }, 40 | { 41 | code: '{ "engines": { "node": ">=8" } }', 42 | filename: 'package.json', 43 | options: ['require-npm'], 44 | errors: [{ 45 | message: 'Missing npm engine.', 46 | type: 'ObjectExpression', 47 | }], 48 | }, 49 | { 50 | // doesn't error on unexpected engines 51 | code: '{ "engines": "" }', 52 | filename: 'package.json', 53 | errors: [{ 54 | message: 'Missing node engine.', 55 | type: 'Literal', 56 | }], 57 | }, 58 | ], 59 | })); 60 | -------------------------------------------------------------------------------- /docs/rules/restrict-ranges.md: -------------------------------------------------------------------------------- 1 | # restrict the dependency ranges in package.json (restrict-ranges) 2 | 3 | Enforce an allowed version ranges using hints or regular expressions. 4 | 5 | 6 | ## Rule Details 7 | 8 | This rule aims to prevent too liberal of version ranges or branches. 9 | 10 | Examples of **incorrect** code for this rule: 11 | 12 | ```js 13 | /* eslint restrict-ranges: [{ versionHint: 'pin' }] */ 14 | 15 | { 16 | "dependencies": { 17 | "foo": "^1.2.3" 18 | } 19 | } 20 | ``` 21 | 22 | ```js 23 | /* eslint restrict-ranges: [{ versionRegex: '^[^#]+$' }] */ 24 | 25 | { 26 | "dependencies": { 27 | "foo": "foo/bar#baz" 28 | } 29 | } 30 | ``` 31 | 32 | Examples of **correct** code for this rule: 33 | 34 | ```js 35 | /* eslint restrict-ranges: [{ versionHint: 'pin' }] */ 36 | 37 | { 38 | "dependencies": { 39 | "foo": "1.2.3" 40 | } 41 | } 42 | ``` 43 | 44 | ### Options 45 | 46 | This rule has either an object option: 47 | 48 | * `"dependencyTypes"` limit the dependency groups 49 | * `"packages"` limit the range checking to select packages 50 | * `"packageRegex"` limit the range checking to select packages 51 | * `"versionHint"` limit the range using semver hints 52 | * `"pin"` no version hints 53 | * `"tilde"` include `~` or pinned 54 | * `"caret"` include `^`, `~`, or pinned 55 | * `"versionRegex"` limit the range using a regular expression 56 | * `"pinUnstable"` no version hints for 0.x.x or prerelease 57 | 58 | Or an array of the object option above 59 | 60 | ## When Not To Use It 61 | 62 | If you are the only one working on a project, you may not want this rule. 63 | 64 | ## Further Reading 65 | 66 | https://semver.org/ 67 | -------------------------------------------------------------------------------- /lib/rules/ensure-repository-directory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | meta: { 7 | docs: { 8 | description: 'ensure repository/directory in package.json', 9 | }, 10 | schema: [ 11 | ], 12 | }, 13 | 14 | create(context) { 15 | let filename = context.getFilename(); 16 | if (path.basename(filename) !== 'package.json') { 17 | return {}; 18 | } 19 | 20 | return { 21 | AssignmentExpression(node) { 22 | let json = node.right; 23 | let property = json.properties.find(p => p.key.value === 'repository'); 24 | if (!property) { 25 | return; 26 | } 27 | let repository = property.value; 28 | if (repository.type !== 'ObjectExpression') { 29 | return; 30 | } 31 | property = repository.properties.find(p => p.key.value === 'directory'); 32 | if (!property) { 33 | return; 34 | } 35 | let directory = property.value; 36 | let error = () => context.report({ 37 | node: directory, 38 | message: 'repository/directory does not match actual location.', 39 | }); 40 | let fake = directory.value; 41 | let real = path.dirname(filename); 42 | do { 43 | if (path.basename(fake) !== path.basename(real)) { 44 | error(); 45 | break; 46 | } 47 | fake = path.dirname(fake); 48 | real = path.dirname(real); 49 | } while (fake !== '.' && real !== '.'); 50 | if (fake !== '.' && real === '.') { 51 | error(); 52 | } 53 | }, 54 | }; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /lib/rules/no-branch-in-dependencies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const semver = require('semver'); 5 | 6 | const defaultKeys = [ 7 | 'dependencies', 8 | 'devDependencies', 9 | 'optionalDependencies', 10 | ]; 11 | 12 | module.exports = { 13 | meta: { 14 | docs: { 15 | description: 'prevent branches in package.json dependencies', 16 | }, 17 | schema: [ 18 | { 19 | 'type': 'object', 20 | 'properties': { 21 | 'keys': { 22 | 'type': 'array', 23 | 'items': { 24 | 'type': 'string', 25 | }, 26 | }, 27 | 'ignore': { 28 | 'type': 'array', 29 | 'items': { 30 | 'type': 'string', 31 | }, 32 | }, 33 | }, 34 | 'additionalProperties': false, 35 | }, 36 | ], 37 | }, 38 | 39 | create(context) { 40 | let filename = context.getFilename(); 41 | if (path.basename(filename) !== 'package.json') { 42 | return {}; 43 | } 44 | 45 | let options = context.options[0] || {}; 46 | let keys = options.keys || defaultKeys; 47 | let ignore = options.ignore || []; 48 | 49 | return { 50 | AssignmentExpression(node) { 51 | let json = node.right; 52 | for (let property of json.properties.filter(p => keys.includes(p.key.value))) { 53 | let deps = property.value; 54 | for (let p of deps.properties.filter(p => !ignore.includes(p.key.value))) { 55 | if (semver.validRange(p.value.value)) { 56 | continue; 57 | } 58 | context.report({ 59 | node: p.value, 60 | message: 'Don\'t use branches.', 61 | }); 62 | } 63 | } 64 | }, 65 | }; 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /tests/lib/rules/ensure-volta-extends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/ensure-volta-extends'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | const path = require('path'); 7 | 8 | new RuleTester().run('ensure-volta-extends', rule, preprocess({ 9 | valid: [ 10 | { 11 | code: '{}', 12 | filename: path.join(__dirname, 'package.json'), 13 | }, 14 | { 15 | code: '{ "volta": "" }', 16 | filename: path.join(__dirname, 'package.json'), 17 | }, 18 | { 19 | code: '{ "volta": {} }', 20 | filename: path.join(__dirname, 'package.json'), 21 | }, 22 | { 23 | code: '{ "volta": { "extends": {} } }', 24 | filename: path.join(__dirname, 'package.json'), 25 | }, 26 | { 27 | code: '{ "volta": { "extends": "../../fixtures/volta/with-volta.json" } }', 28 | filename: path.join(__dirname, 'package.json'), 29 | }, 30 | ], 31 | invalid: [ 32 | { 33 | code: '{ "volta": { "extends": "wrong/dir" } }', 34 | filename: path.join(__dirname, 'package.json'), 35 | errors: [{ 36 | message: 'volta/extends \'wrong/dir\' does not exist.', 37 | type: 'Literal', 38 | }], 39 | }, 40 | { 41 | code: '{ "volta": { "extends": "../../fixtures/volta/not-json.js" } }', 42 | filename: path.join(__dirname, 'package.json'), 43 | errors: [{ 44 | message: 'volta/extends \'../../fixtures/volta/not-json.js\' is not JSON.', 45 | type: 'Literal', 46 | }], 47 | }, 48 | { 49 | code: '{ "volta": { "extends": "../../fixtures/volta/no-volta.json" } }', 50 | filename: path.join(__dirname, 'package.json'), 51 | errors: [{ 52 | message: 'volta/extends \'../../fixtures/volta/no-volta.json\' does not have a volta config.', 53 | type: 'Literal', 54 | }], 55 | }, 56 | ], 57 | })); 58 | -------------------------------------------------------------------------------- /lib/rules/ensure-workspaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fg = require('fast-glob'); 5 | 6 | module.exports = { 7 | meta: { 8 | docs: { 9 | description: 'ensure workspace globs in package.json resolve to directories', 10 | }, 11 | schema: [ 12 | ], 13 | }, 14 | 15 | create(context) { 16 | let filename = context.getFilename(); 17 | if (path.basename(filename) !== 'package.json') { 18 | return {}; 19 | } 20 | 21 | return { 22 | AssignmentExpression(node) { 23 | let json = node.right; 24 | let property = json.properties.find(p => p.key.value === 'workspaces'); 25 | if (!property) { 26 | return; 27 | } 28 | 29 | let workspaces = property.value; 30 | if (workspaces.type === 'ObjectExpression') { 31 | let property = workspaces.properties.find(p => p.key.value === 'packages'); 32 | if (!property) { 33 | return; 34 | } 35 | 36 | workspaces = property.value; 37 | } 38 | 39 | if (workspaces.type !== 'ArrayExpression') { 40 | return; 41 | } 42 | 43 | for (let node of workspaces.elements) { 44 | if (node.type !== 'Literal') { 45 | continue; 46 | } 47 | 48 | if (typeof node.value !== 'string') { 49 | continue; 50 | } 51 | 52 | // fast-glob isn't handling windows paths 53 | let _path = path.posix; 54 | 55 | let glob = _path.join(node.value, 'package.json'); 56 | 57 | let entries = fg.sync(glob, { 58 | cwd: path.dirname(filename), 59 | }); 60 | 61 | if (!entries.length) { 62 | context.report({ 63 | node, 64 | message: 'workspace path/glob does not match any workspaces with a package.json.', 65 | }); 66 | } 67 | } 68 | }, 69 | }; 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /tests/lib/rules/no-branch-in-dependencies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/no-branch-in-dependencies'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('no-branch-in-dependencies', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "dependencies": { "lodash": "1.2.3" } }', 11 | filename: 'package.json', 12 | }, 13 | { 14 | code: '{ "devDependencies": { "lodash": "1.2.3" } }', 15 | filename: 'package.json', 16 | }, 17 | { 18 | code: '{ "optionalDependencies": { "lodash": "1.2.3" } }', 19 | filename: 'package.json', 20 | }, 21 | { 22 | code: '{ "foo": { "lodash": "lodash/lodash" } }', 23 | filename: 'package.json', 24 | }, 25 | { 26 | code: '{ "dependencies": { "lodash": "lodash/lodash" } }', 27 | filename: 'package.json', 28 | options: [{ keys: ['foo'] }], 29 | }, 30 | { 31 | code: '{ "dependencies": { "lodash": "lodash/lodash" } }', 32 | filename: 'package.json', 33 | options: [{ ignore: ['lodash'] }], 34 | }, 35 | { 36 | code: '{ "dependencies": { "lodash": "lodash/lodash" } }', 37 | filename: 'not-package.json', 38 | }, 39 | ], 40 | invalid: [ 41 | { 42 | code: '{ "dependencies": { "lodash": "lodash/lodash" } }', 43 | filename: 'package.json', 44 | errors: [{ 45 | message: 'Don\'t use branches.', 46 | type: 'Literal', 47 | }], 48 | }, 49 | { 50 | code: '{ "devDependencies": { "lodash": "lodash/lodash" } }', 51 | filename: 'package.json', 52 | errors: [{ 53 | message: 'Don\'t use branches.', 54 | type: 'Literal', 55 | }], 56 | }, 57 | { 58 | code: '{ "optionalDependencies": { "lodash": "lodash/lodash" } }', 59 | filename: 'package.json', 60 | errors: [{ 61 | message: 'Don\'t use branches.', 62 | type: 'Literal', 63 | }], 64 | }, 65 | ], 66 | })); 67 | -------------------------------------------------------------------------------- /tests/lib/rules/ensure-workspaces.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/ensure-workspaces'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('ensure-workspaces', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "workspaces": ["tests/fixtures/workspaces/*/bar"] }', 11 | }, 12 | { 13 | code: '{}', 14 | filename: 'package.json', 15 | }, 16 | { 17 | code: '{ "workspaces": "literal" }', 18 | filename: 'package.json', 19 | }, 20 | { 21 | code: '{ "workspaces": [{}] }', 22 | filename: 'package.json', 23 | }, 24 | { 25 | code: '{ "workspaces": [0] }', 26 | filename: 'package.json', 27 | }, 28 | { 29 | code: '{ "workspaces": ["tests/fixtures/workspaces/*/bar"] }', 30 | filename: 'package.json', 31 | }, 32 | { 33 | code: '{ "workspaces": {} }', 34 | filename: 'package.json', 35 | }, 36 | { 37 | code: '{ "workspaces": { "packages": "literal" } }', 38 | filename: 'package.json', 39 | }, 40 | { 41 | code: '{ "workspaces": { "packages": [{}] } }', 42 | filename: 'package.json', 43 | }, 44 | { 45 | code: '{ "workspaces": { "packages": [0] } }', 46 | filename: 'package.json', 47 | }, 48 | { 49 | code: '{ "workspaces": { "packages": ["tests/fixtures/workspaces/*/bar"] } }', 50 | filename: 'package.json', 51 | }, 52 | ], 53 | invalid: [ 54 | { 55 | code: '{ "workspaces": ["tests/fixtures/workspaces/*/baz"] }', 56 | filename: 'package.json', 57 | errors: [{ 58 | message: 'workspace path/glob does not match any workspaces with a package.json.', 59 | type: 'Literal', 60 | }], 61 | }, 62 | { 63 | code: '{ "workspaces": { "packages": ["tests/fixtures/workspaces/*/baz"] } }', 64 | filename: 'package.json', 65 | errors: [{ 66 | message: 'workspace path/glob does not match any workspaces with a package.json.', 67 | type: 'Literal', 68 | }], 69 | }, 70 | ], 71 | })); 72 | -------------------------------------------------------------------------------- /lib/rules/require-unique-dependency-names.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | meta: { 7 | docs: { 8 | description: 'prevent duplicate packages in dependencies and devDependencies', 9 | }, 10 | schema: [ 11 | ], 12 | fixable: true, 13 | }, 14 | 15 | create(context) { 16 | let filename = context.getFilename(); 17 | if (path.basename(filename) !== 'package.json') { 18 | return {}; 19 | } 20 | 21 | return { 22 | AssignmentExpression(node) { 23 | let json = node.right; 24 | 25 | let dependencies = json.properties.find(p => p.key.value === 'dependencies'); 26 | if (!dependencies) { 27 | return; 28 | } 29 | 30 | let devDependencies = json.properties.find(p => p.key.value === 'devDependencies'); 31 | if (!devDependencies) { 32 | return; 33 | } 34 | 35 | for (let i = 0; i < devDependencies.value.properties.length; i++) { 36 | let devDep = devDependencies.value.properties[i]; 37 | 38 | let dep = dependencies.value.properties.find(dep => dep.key.value === devDep.key.value); 39 | 40 | if (dep) { 41 | context.report({ 42 | node: devDep.key, 43 | message: `Package ${devDep.key.raw} already shows up in ${dependencies.key.raw}.`, 44 | fix(fixer) { 45 | if (devDep.value.value === dep.value.value) { 46 | let nextDevDep = devDependencies.value.properties[i + 1]; 47 | if (nextDevDep) { 48 | return fixer.removeRange([devDep.range[0], nextDevDep.range[0]]); 49 | } 50 | 51 | let prevDevDep = devDependencies.value.properties[i - 1]; 52 | if (prevDevDep) { 53 | return fixer.removeRange([prevDevDep.range[1], devDep.range[1]]); 54 | } 55 | 56 | return fixer.removeRange([ 57 | devDependencies.value.range[0] + 1, 58 | devDependencies.value.range[1] - 1, 59 | ]); 60 | } 61 | }, 62 | }); 63 | } 64 | } 65 | }, 66 | }; 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /lib/rules/ensure-volta-extends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | module.exports = { 7 | meta: { 8 | docs: { 9 | description: 'ensure volta/extends in package.json', 10 | }, 11 | schema: [ 12 | ], 13 | }, 14 | 15 | create(context) { 16 | let filename = context.getFilename(); 17 | if (path.basename(filename) !== 'package.json') { 18 | return {}; 19 | } 20 | 21 | return { 22 | AssignmentExpression(node) { 23 | let json = node.right; 24 | let property = json.properties.find(p => p.key.value === 'volta'); 25 | if (!property) { 26 | return; 27 | } 28 | let repository = property.value; 29 | if (repository.type !== 'ObjectExpression') { 30 | return; 31 | } 32 | property = repository.properties.find(p => p.key.value === 'extends'); 33 | if (!property) { 34 | return; 35 | } 36 | let _extends = property.value; 37 | if (_extends.type !== 'Literal') { 38 | return; 39 | } 40 | let value = _extends.value; 41 | let dirname = path.dirname(filename); 42 | let filePath = path.resolve(dirname, value); 43 | 44 | let text; 45 | 46 | try { 47 | text = fs.readFileSync(filePath); 48 | } catch (err) { 49 | if (err.code === 'ENOENT') { 50 | context.report({ 51 | node: _extends, 52 | message: `volta/extends '${value}' does not exist.`, 53 | }); 54 | 55 | return; 56 | } 57 | 58 | throw err; 59 | } 60 | 61 | json; 62 | 63 | try { 64 | json = JSON.parse(text); 65 | } catch (err) { 66 | if (err.message === 'Unexpected end of JSON input') { 67 | context.report({ 68 | node: _extends, 69 | message: `volta/extends '${value}' is not JSON.`, 70 | }); 71 | 72 | return; 73 | } 74 | 75 | throw err; 76 | } 77 | 78 | if (!('volta' in json)) { 79 | context.report({ 80 | node: _extends, 81 | message: `volta/extends '${value}' does not have a volta config.`, 82 | }); 83 | } 84 | }, 85 | }; 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /tests/lib/rules/require-unique-dependency-names.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/require-unique-dependency-names'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('require-unique-dependency-names', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": { "bar": "0.0.0" } }', 11 | filename: 'package.json', 12 | }, 13 | // test no dependencies 14 | { 15 | code: '{ "devDependencies": { "foo": "0.0.0" } }', 16 | filename: 'package.json', 17 | }, 18 | // test no devDependencies 19 | { 20 | code: '{ "dependencies": { "foo": "0.0.0" } }', 21 | filename: 'package.json', 22 | }, 23 | ], 24 | invalid: [ 25 | { 26 | code: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": { "foo": "0.0.0" } }', 27 | filename: 'package.json', 28 | errors: [{ 29 | message: 'Package "foo" already shows up in "dependencies".', 30 | type: 'Literal', 31 | }], 32 | output: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": {} }', 33 | }, 34 | { 35 | code: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": { "foo": "0.0.0", "bar": "0.0.0" } }', 36 | filename: 'package.json', 37 | errors: [{ 38 | message: 'Package "foo" already shows up in "dependencies".', 39 | type: 'Literal', 40 | }], 41 | output: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": { "bar": "0.0.0" } }', 42 | }, 43 | { 44 | code: '{ "dependencies": { "bar": "0.0.0" }, "devDependencies": { "foo": "0.0.0", "bar": "0.0.0" } }', 45 | filename: 'package.json', 46 | errors: [{ 47 | message: 'Package "bar" already shows up in "dependencies".', 48 | type: 'Literal', 49 | }], 50 | output: '{ "dependencies": { "bar": "0.0.0" }, "devDependencies": { "foo": "0.0.0" } }', 51 | }, 52 | // it doesn't autofix if different versions 53 | { 54 | code: '{ "dependencies": { "foo": "0.0.0" }, "devDependencies": { "foo": "1.0.0" } }', 55 | filename: 'package.json', 56 | errors: [{ 57 | message: 'Package "foo" already shows up in "dependencies".', 58 | type: 'Literal', 59 | }], 60 | }, 61 | ], 62 | })); 63 | -------------------------------------------------------------------------------- /tests/lib/rules/sort-package-json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/sort-package-json'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('sort-package-json', rule, preprocess({ 8 | valid: [ 9 | { 10 | code: '{"name":"foo","version":"1.0.0"}', 11 | filename: 'package.json', 12 | }, 13 | ], 14 | invalid: [ 15 | { 16 | code: '{"version":"1.0.0","name":"foo"}', 17 | filename: 'package.json', 18 | errors: [{ 19 | message: 'package.json is not sorted correctly.', 20 | type: 'ObjectExpression', 21 | }], 22 | output: '{"name":"foo","version":"1.0.0"}', 23 | }, 24 | // preserves trailing whitespace 25 | { 26 | code: `{ 27 | "version": "1.0.0", 28 | "name": "foo" 29 | } 30 | `, 31 | filename: 'package.json', 32 | errors: [{ 33 | message: 'package.json is not sorted correctly.', 34 | type: 'ObjectExpression', 35 | }], 36 | output: `{ 37 | "name": "foo", 38 | "version": "1.0.0" 39 | } 40 | `, 41 | }, 42 | // preserves existing indentation 43 | { 44 | code: `{ 45 | "version": "1.0.0", 46 | "name": "foo" 47 | }`, 48 | filename: 'package.json', 49 | errors: [{ 50 | message: 'package.json is not sorted correctly.', 51 | type: 'ObjectExpression', 52 | }], 53 | output: `{ 54 | "name": "foo", 55 | "version": "1.0.0" 56 | }`, 57 | }, 58 | // accepts and uses options 59 | { 60 | code: '{"version":"1.0.0","name":"foo","license":"UNLICENSED"}', 61 | filename: 'package.json', 62 | options: [{ sortOrder: ['license', 'name'] }], 63 | errors: [{ 64 | message: 'package.json is not sorted correctly.', 65 | type: 'ObjectExpression', 66 | }], 67 | output: '{"license":"UNLICENSED","name":"foo","version":"1.0.0"}', 68 | }, 69 | // accepts and uses options, uses different sort order 70 | { 71 | code: '{"version":"1.0.0","name":"foo","license":"UNLICENSED"}', 72 | filename: 'package.json', 73 | options: [{ sortOrder: ['name', 'license'] }], 74 | errors: [{ 75 | message: 'package.json is not sorted correctly.', 76 | type: 'ObjectExpression', 77 | }], 78 | output: '{"name":"foo","license":"UNLICENSED","version":"1.0.0"}', 79 | }, 80 | ], 81 | })); 82 | -------------------------------------------------------------------------------- /lib/rules/validate-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Ajv = require('ajv'); 4 | const { default: betterAjvErrors } = require('better-ajv-errors'); 5 | 6 | let ajv; 7 | 8 | module.exports = { 9 | meta: { 10 | docs: { 11 | description: 'require a valid JSON Schema', 12 | }, 13 | fixable: 'code', 14 | schema: [ 15 | { 16 | 'type': 'object', 17 | 'properties': { 18 | 'schema': { 19 | 'type': 'string', 20 | }, 21 | 'prettyErrors': { 22 | 'type': 'boolean', 23 | 'default': true, 24 | }, 25 | // https://ajv.js.org/options.html#options-to-modify-validated-data 26 | 'avjFixerOptions': { 27 | 'type': 'object', 28 | }, 29 | }, 30 | 'additionalProperties': false, 31 | }, 32 | ], 33 | }, 34 | 35 | create(context) { 36 | let options = context.options[0]; 37 | 38 | let sourceCode = context.getSourceCode(); 39 | 40 | if (!ajv) { 41 | ajv = new Ajv(); 42 | } 43 | 44 | let schema = JSON.parse(options.schema); 45 | 46 | return { 47 | AssignmentExpression(node) { 48 | let packageJsonNode = node.right; 49 | let packageJsonText = sourceCode.text.substring(...packageJsonNode.range); 50 | 51 | // Calling `validate()` mutates the `validate` function/object, 52 | // so we have to make a new instance every time. 53 | let validate = ajv.compile(schema); 54 | 55 | let packageJson = JSON.parse(packageJsonText); 56 | let isValid = validate(packageJson); 57 | 58 | if (!isValid) { 59 | let message; 60 | 61 | if (options.prettyErrors) { 62 | message = betterAjvErrors(schema, packageJson, validate.errors, { 63 | json: packageJsonText, 64 | }); 65 | } else { 66 | message = validate.errors 67 | .map(error => `${error.schemaPath} ${error.message}`) 68 | .join(', '); 69 | } 70 | 71 | context.report({ 72 | node: packageJsonNode, 73 | message, 74 | fix(fixer) { 75 | let ajvFix = new Ajv(options.avjFixerOptions); 76 | let validate = ajvFix.compile(schema); 77 | validate(packageJson); 78 | 79 | return fixer.replaceText(packageJsonNode, JSON.stringify(packageJson, null, 2)); 80 | }, 81 | }); 82 | } 83 | }, 84 | }; 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /tests/lib/processors/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { describe } = require('../../helpers/mocha'); 4 | const { expect } = require('../../helpers/chai'); 5 | const { ESLint } = require('eslint'); 6 | const jsonFiles = require('../../../lib'); 7 | 8 | describe(function() { 9 | async function _test({ rules, fix, text }) { 10 | let cli = new ESLint({ 11 | overrideConfigFile: true, 12 | overrideConfig: { 13 | files: ['package.json'], 14 | rules, 15 | plugins: { 16 | 'json-files': jsonFiles, 17 | }, 18 | processor: jsonFiles.processors.json, 19 | }, 20 | fix, 21 | }); 22 | 23 | let results = await cli.lintText(text, { filePath: 'package.json' }); 24 | 25 | expect(results.length).to.equal(1); 26 | 27 | return results; 28 | } 29 | 30 | it('should run plugin on .json files', async function() { 31 | let text = `{ 32 | "license": "UNLICENSED" 33 | } 34 | `; 35 | 36 | let results = await _test({ 37 | rules: { 38 | 'json-files/require-license': [2], 39 | }, 40 | text, 41 | }); 42 | 43 | expect(results[0].messages.length).to.equal(1); 44 | expect(results[0].messages[0].message).to.equal('Missing license.'); 45 | expect(results[0].messages[0].line).to.equal(2); 46 | }); 47 | 48 | it('should fix issues in .json files', async function() { 49 | let text = `{ 50 | "version": "1.0.0", 51 | "name": "foo" 52 | } 53 | `; 54 | 55 | let results = await _test({ 56 | rules: { 57 | 'json-files/sort-package-json': [2], 58 | }, 59 | fix: true, 60 | text, 61 | }); 62 | 63 | expect(results[0].output).to.equal(`{ 64 | "name": "foo", 65 | "version": "1.0.0" 66 | } 67 | `); 68 | }); 69 | 70 | it('should ignore js rules on .json files', async function() { 71 | let text = `{ 72 | "foo": "", 73 | "bar": "" 74 | } 75 | `; 76 | 77 | let results = await _test({ 78 | rules: { 79 | 'sort-keys': [2], 80 | }, 81 | text, 82 | }); 83 | 84 | expect(results[0].messages.length).to.equal(0); 85 | }); 86 | 87 | it.skip('should run js rules on .json files', async function() { 88 | let text = `{ 89 | "foo": "", 90 | "bar": "" 91 | } 92 | `; 93 | 94 | let results = await _test({ 95 | rules: { 96 | 'sort-keys': [2], 97 | }, 98 | text, 99 | }); 100 | 101 | expect(results[0].messages.length).to.equal(1); 102 | expect(results[0].messages[0].message).to.equal('Unnecessarily quoted property \'license\' found.'); 103 | expect(results[0].messages[0].line).to.equal(2); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-json-files 2 | 3 | [![npm version](https://badge.fury.io/js/eslint-plugin-json-files.svg)](https://badge.fury.io/js/eslint-plugin-json-files) 4 | 5 | ESLint JSON processor and rules 6 | 7 | ## Installation 8 | 9 | You'll first need to install [ESLint](http://eslint.org): 10 | 11 | ``` 12 | $ npm i eslint --save-dev 13 | ``` 14 | 15 | Next, install `eslint-plugin-json-files`: 16 | 17 | ``` 18 | $ npm install eslint-plugin-json-files --save-dev 19 | ``` 20 | 21 | **Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-json-files` globally. 22 | 23 | ## Usage 24 | 25 | Add `json-files` to the plugins and processor section of your `eslint.config.js` configuration file. 26 | You can omit the `eslint-plugin-` prefix: 27 | 28 | ```js 29 | const jsonFiles = require('eslint-plugin-json-files'); 30 | 31 | // ... 32 | 33 | { 34 | plugins: { 35 | 'json-files': jsonFiles, 36 | }, 37 | processor: 'json-files/json', 38 | } 39 | ``` 40 | 41 | Then configure the rules you want to use under the rules section. 42 | 43 | ```js 44 | { 45 | rules: { 46 | 'json-files/rule-name': 'error', 47 | }, 48 | } 49 | ``` 50 | 51 | ## Supported Rules 52 | 53 | | Rule ID | Description | 54 | |:--------|:------------| 55 | | [json-files/ensure-repository-directory](./docs/rules/ensure-repository-directory.md) | ensure repository/directory in package.json | 56 | | [json-files/ensure-volta-extends](./docs/rules/ensure-volta-extends.md) | ensure volta-extends in package.json | 57 | | [json-files/ensure-workspaces](./docs/rules/ensure-workspaces.md) | ensure workspace globs in package.json resolve to directories | 58 | | [json-files/eol-last](./docs/rules/eol-last.md) | require or disallow newline at the end of package.json | 59 | | [json-files/no-branch-in-dependencies](./docs/rules/no-branch-in-dependencies.md) | prevent branches in package.json dependencies | 60 | | [json-files/require-engines](./docs/rules/require-engines.md) | require the engines field in package.json | 61 | | [json-files/require-license](./docs/rules/require-license.md) | require a license in package.json | 62 | | [json-files/require-unique-dependency-names](./docs/rules/require-unique-dependency-names.md) | prevent duplicate packages in dependencies and devDependencies | 63 | | [json-files/restrict-ranges](./docs/rules/restrict-ranges.md) | restrict the dependency ranges in package.json | 64 | | [json-files/sort-package-json](./docs/rules/sort-package-json.md) | enforce package.json sorting | 65 | | [json-files/validate-schema](./docs/rules/validate-schema.md) | require a valid JSON Schema | 66 | 67 | ## Footnotes 68 | 69 | I wouldn't mind getting this merged into [eslint-plugin-node](https://github.com/mysticatea/eslint-plugin-node) or [eslint-plugin-json](https://github.com/azeemba/eslint-plugin-json). 70 | -------------------------------------------------------------------------------- /tests/lib/rules/validate-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/validate-schema'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | // const stripAnsi = require('strip-ansi'); 7 | 8 | function schema(json) { 9 | return JSON.stringify({ 10 | '$schema': 'http://json-schema.org/draft-07/schema#', 11 | ...json, 12 | }); 13 | } 14 | 15 | function color(s) { 16 | // return process.stdout.isTTY ? s : stripAnsi(s); 17 | return s; 18 | } 19 | 20 | new RuleTester().run('validate-schema', rule, preprocess({ 21 | valid: [ 22 | { 23 | code: '{"foo":"bar"}', 24 | options: [{ 25 | schema: schema({ 26 | 'type': 'object', 27 | 'properties': { 28 | 'foo': { 'type': 'string' }, 29 | }, 30 | }), 31 | }], 32 | }, 33 | ], 34 | invalid: [ 35 | { 36 | code: '{"foo":"bar"}', 37 | options: [{ 38 | schema: schema({ 39 | 'type': 'object', 40 | 'not': { 'required': ['foo'] }, 41 | }), 42 | }], 43 | errors: [{ 44 | message: color( 45 | process.env.CI ? 46 | 'NOT must NOT be valid\n\n\x1B[0m\x1B[31m\x1B[1m>\x1B[22m\x1B[39m\x1B[90m 1 |\x1B[39m {\x1B[32m"foo"\x1B[39m\x1B[33m:\x1B[39m\x1B[32m"bar"\x1B[39m}\n \x1B[90m |\x1B[39m \x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m\x1B[31m\x1B[1m^\x1B[22m\x1B[39m \x1B[31m\x1B[1m👈🏽 not must NOT be valid\x1B[22m\x1B[39m\x1B[0m' : 47 | '\x1b[31m\x1b[1mNOT\x1b[22m\x1b[39m\x1b[31m must NOT be valid\x1b[39m\n\n\x1b[0m\x1b[31m\x1b[1m>\x1b[22m\x1b[39m\x1b[90m 1 |\x1b[39m {\x1b[32m"foo"\x1b[39m\x1b[33m:\x1b[39m\x1b[32m"bar"\x1b[39m}\n \x1b[90m |\x1b[39m \x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m\x1b[31m\x1b[1m^\x1b[22m\x1b[39m \x1b[31m\x1b[1m👈🏽 \x1b[95mnot\x1b[31m must NOT be valid\x1b[22m\x1b[39m\x1b[0m', 48 | ), 49 | type: 'ObjectExpression', 50 | }], 51 | output: `{ 52 | "foo": "bar" 53 | }`, 54 | }, 55 | { 56 | code: '{"foo":"bar"}', 57 | options: [{ 58 | schema: schema({ 59 | 'type': 'object', 60 | 'not': { 'required': ['foo'] }, 61 | }), 62 | prettyErrors: false, 63 | }], 64 | errors: [{ 65 | message: '#/not must NOT be valid', 66 | type: 'ObjectExpression', 67 | }], 68 | output: `{ 69 | "foo": "bar" 70 | }`, 71 | }, 72 | { 73 | code: '{"foo":"bar","bar":"foo"}', 74 | options: [{ 75 | schema: schema({ 76 | 'type': 'object', 77 | 'properties': { 78 | 'foo': { 79 | 'const': 'bar', 80 | }, 81 | }, 82 | 'additionalProperties': false, 83 | }), 84 | prettyErrors: false, 85 | avjFixerOptions: { 86 | removeAdditional: true, 87 | }, 88 | }], 89 | errors: [{ 90 | message: '#/additionalProperties must NOT have additional properties', 91 | type: 'ObjectExpression', 92 | }], 93 | output: `{ 94 | "foo": "bar" 95 | }`, 96 | }, 97 | ], 98 | })); 99 | -------------------------------------------------------------------------------- /lib/rules/restrict-ranges.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const semver = require('semver'); 5 | 6 | const defaultDependencyTypes = [ 7 | 'dependencies', 8 | 'devDependencies', 9 | 'optionalDependencies', 10 | ]; 11 | 12 | const packages = { 13 | 'packages': { 14 | 'type': 'array', 15 | 'items': { 16 | 'type': 'string', 17 | }, 18 | }, 19 | }; 20 | const packageRegex = { 21 | 'packageRegex': { 22 | 'type': 'string', 23 | }, 24 | }; 25 | const versionHint = { 26 | 'versionHint': { 27 | 'enum': [ 28 | 'caret', 29 | 'tilde', 30 | 'pin', 31 | ], 32 | }, 33 | }; 34 | const versionRegex = { 35 | 'versionRegex': { 36 | 'type': 'string', 37 | }, 38 | }; 39 | const pinUnstable = { 40 | 'pinUnstable': { 41 | 'type': 'boolean', 42 | }, 43 | }; 44 | 45 | let anyOf = []; 46 | 47 | for (let prop1 of [packages, packageRegex]) { 48 | for (let prop2 of [versionHint, versionRegex, pinUnstable]) { 49 | anyOf.push(Object.assign({ 50 | 'type': 'object', 51 | 'additionalProperties': false, 52 | 'required': Object.keys(prop2), 53 | }, { 54 | 'properties': Object.assign({ 55 | 'dependencyTypes': { 56 | 'type': 'array', 57 | 'items': { 58 | 'type': 'string', 59 | }, 60 | }, 61 | }, prop1, prop2), 62 | })); 63 | } 64 | } 65 | 66 | function isUnstable(versionString) { 67 | if (semver.valid(versionString)) { 68 | // already pinned 69 | return; 70 | } 71 | 72 | let range; 73 | try { 74 | range = new semver.Range(versionString); 75 | } catch { 76 | // something like a git url 77 | return; 78 | } 79 | 80 | // `` or `*` 81 | if (range.set[0][0].operator === '') { 82 | return; 83 | } 84 | 85 | let parsed = semver.minVersion(versionString); 86 | if (parsed.major === 0 || parsed.prerelease.length) { 87 | return true; 88 | } 89 | } 90 | 91 | module.exports = { 92 | meta: { 93 | docs: { 94 | description: 'prevent branches in package.json dependencies', 95 | }, 96 | schema: [ 97 | { 98 | 'oneOf': [ 99 | { 100 | anyOf, 101 | }, 102 | { 103 | 'type': 'array', 104 | 'items': { 105 | anyOf, 106 | }, 107 | }, 108 | ], 109 | }, 110 | ], 111 | }, 112 | 113 | create(context) { 114 | let filename = context.getFilename(); 115 | if (path.basename(filename) !== 'package.json' || !context.options[0]) { 116 | return {}; 117 | } 118 | 119 | let optionsArray = Array.isArray(context.options[0]) ? context.options[0] : context.options; 120 | 121 | return { 122 | AssignmentExpression(node) { 123 | let json = node.right; 124 | for (let property of json.properties.filter(p => defaultDependencyTypes.includes(p.key.value))) { 125 | let deps = property.value; 126 | for (let p of deps.properties) { 127 | for (let options of optionsArray) { 128 | if (options.dependencyTypes && !options.dependencyTypes.includes(property.key.value)) { 129 | continue; 130 | } 131 | 132 | let packages = options.packages; 133 | let packageRegex = new RegExp(options.packageRegex === undefined ? '.*' : options.packageRegex); 134 | let versionHint = options.versionHint; 135 | let versionRegex = new RegExp(options.versionRegex === undefined ? '.*' : options.versionRegex); 136 | let pinUnstable = options.pinUnstable; 137 | 138 | let versionHintRegex; 139 | switch (versionHint) { 140 | case 'caret': 141 | versionHintRegex = /^[\^~\d]/; 142 | break; 143 | case 'tilde': 144 | versionHintRegex = /^[~\d]/; 145 | break; 146 | case 'pin': 147 | versionHintRegex = /^\d/; 148 | break; 149 | default: 150 | versionHintRegex = /.*/; 151 | } 152 | 153 | let packageString = p.key.value; 154 | if (packages && !packages.includes(packageString)) { 155 | continue; 156 | } 157 | if (!packageRegex.test(packageString)) { 158 | continue; 159 | } 160 | 161 | let versionString = p.value.value; 162 | let failed; 163 | let message; 164 | if (!versionHintRegex.test(versionString)) { 165 | failed = true; 166 | message = `Invalid SemVer hint (${versionHint}).`; 167 | } else if (!versionRegex.test(versionString)) { 168 | failed = true; 169 | message = `Regex does not pass (${versionRegex}).`; 170 | } else if (pinUnstable && isUnstable(versionString)) { 171 | failed = true; 172 | message = 'Invalid SemVer hint on unstable.'; 173 | } 174 | 175 | if (!failed) { 176 | break; 177 | } 178 | 179 | context.report({ 180 | node: p.value, 181 | message, 182 | }); 183 | } 184 | } 185 | } 186 | }, 187 | }; 188 | }, 189 | }; 190 | -------------------------------------------------------------------------------- /tests/lib/rules/restrict-ranges.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { RuleTester } = require('eslint'); 4 | const rule = require('../../../lib/rules/restrict-ranges'); 5 | const preprocess = require('../../helpers/preprocess'); 6 | 7 | new RuleTester().run('restrict-ranges', rule, preprocess({ 8 | valid: [ 9 | // no options 10 | { 11 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 12 | filename: 'package.json', 13 | }, 14 | ...[ // versionHint 15 | { 16 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 17 | filename: 'package.json', 18 | options: [{ versionHint: 'caret' }], 19 | }, 20 | { 21 | code: '{ "dependencies": { "foo": "~1.2.3" } }', 22 | filename: 'package.json', 23 | options: [{ versionHint: 'caret' }], 24 | }, 25 | { 26 | code: '{ "dependencies": { "foo": "1.2.3" } }', 27 | filename: 'package.json', 28 | options: [{ versionHint: 'caret' }], 29 | }, 30 | { 31 | code: '{ "dependencies": { "foo": "~1.2.3" } }', 32 | filename: 'package.json', 33 | options: [{ versionHint: 'tilde' }], 34 | }, 35 | { 36 | code: '{ "dependencies": { "foo": "1.2.3" } }', 37 | filename: 'package.json', 38 | options: [{ versionHint: 'tilde' }], 39 | }, 40 | { 41 | code: '{ "dependencies": { "foo": "1.2.3" } }', 42 | filename: 'package.json', 43 | options: [{ versionHint: 'pin' }], 44 | }, 45 | ], 46 | ...[ // versionRegex 47 | { 48 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 49 | filename: 'package.json', 50 | options: [{ versionRegex: '^' }], 51 | }, 52 | ], 53 | ...[ // dependencyTypes 54 | { 55 | code: '{ "dependencies": { "foo": "1.2.3" }, "devDependencies": { "bar": "^1.2.3" } }', 56 | filename: 'package.json', 57 | options: [{ dependencyTypes: ['dependencies'], versionHint: 'pin' }], 58 | }, 59 | ], 60 | ...[ // packages 61 | { 62 | code: '{ "dependencies": { "foo": "1.2.3", "bar": "^1.2.3" } }', 63 | filename: 'package.json', 64 | options: [{ packages: ['foo'], versionHint: 'pin' }], 65 | }, 66 | ], 67 | ...[ // packageRegex 68 | { 69 | code: '{ "dependencies": { "foo": "1.2.3", "bar": "^1.2.3" } }', 70 | filename: 'package.json', 71 | options: [{ packageRegex: 'foo', versionHint: 'pin' }], 72 | }, 73 | ], 74 | ...[ // grouping 75 | { 76 | code: '{ "dependencies": { "foo": "1.2.3", "bar": "^1.2.3" } }', 77 | filename: 'package.json', 78 | options: [[ 79 | { packages: ['foo'], versionHint: 'pin' }, 80 | { packageRegex: 'foo', versionHint: 'pin' }, 81 | ]], 82 | }, 83 | ], 84 | ...[ // pinUnstable 85 | { 86 | code: '{ "dependencies": { "foo": "0.1.2", "bar": "^1.2.3" } }', 87 | filename: 'package.json', 88 | options: [{ pinUnstable: true }], 89 | }, 90 | { 91 | code: '{ "dependencies": { "foo": "1.2.3-alpha.0", "bar": "^1.2.3" } }', 92 | filename: 'package.json', 93 | options: [{ pinUnstable: true }], 94 | }, 95 | { 96 | code: '{ "dependencies": { "foo": "*" } }', 97 | filename: 'package.json', 98 | options: [{ pinUnstable: true }], 99 | }, 100 | { 101 | code: '{ "dependencies": { "foo": "" } }', 102 | filename: 'package.json', 103 | options: [{ pinUnstable: true }], 104 | }, 105 | { 106 | code: '{ "dependencies": { "foo": ">= 1" } }', 107 | filename: 'package.json', 108 | options: [{ pinUnstable: true }], 109 | }, 110 | { 111 | code: '{ "dependencies": { "foo": ">= 1 < 2" } }', 112 | filename: 'package.json', 113 | options: [{ pinUnstable: true }], 114 | }, 115 | { 116 | code: '{ "dependencies": { "foo": "1 || 2" } }', 117 | filename: 'package.json', 118 | options: [{ pinUnstable: true }], 119 | }, 120 | ], 121 | // stop searching on first match 122 | { 123 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 124 | filename: 'package.json', 125 | options: [[ 126 | { versionRegex: '^' }, 127 | { versionRegex: '~' }, 128 | ]], 129 | }, 130 | ], 131 | invalid: [ 132 | ...[ // versionHint 133 | { 134 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 135 | filename: 'package.json', 136 | options: [{ versionHint: 'tilde' }], 137 | errors: [{ 138 | message: 'Invalid SemVer hint (tilde).', 139 | type: 'Literal', 140 | }], 141 | }, 142 | { 143 | code: '{ "dependencies": { "foo": "~1.2.3" } }', 144 | filename: 'package.json', 145 | options: [{ versionHint: 'pin' }], 146 | errors: [{ 147 | message: 'Invalid SemVer hint (pin).', 148 | type: 'Literal', 149 | }], 150 | }, 151 | { 152 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 153 | filename: 'package.json', 154 | options: [{ versionHint: 'pin' }], 155 | errors: [{ 156 | message: 'Invalid SemVer hint (pin).', 157 | type: 'Literal', 158 | }], 159 | }, 160 | { 161 | code: '{ "dependencies": { "foo": "*" } }', 162 | filename: 'package.json', 163 | options: [{ versionHint: 'caret' }], 164 | errors: [{ 165 | message: 'Invalid SemVer hint (caret).', 166 | type: 'Literal', 167 | }], 168 | }, 169 | ], 170 | ...[ // versionRegex 171 | { 172 | code: '{ "dependencies": { "foo": "^1.2.3" } }', 173 | filename: 'package.json', 174 | options: [{ versionRegex: '~' }], 175 | errors: [{ 176 | message: 'Regex does not pass (/~/).', 177 | type: 'Literal', 178 | }], 179 | }, 180 | ], 181 | ...[ // dependencyTypes 182 | { 183 | code: '{ "dependencies": { "foo": "^1.2.3" }, "devDependencies": { "bar": "^1.2.3" } }', 184 | filename: 'package.json', 185 | options: [{ dependencyTypes: ['dependencies'], versionHint: 'pin' }], 186 | errors: [{ 187 | message: 'Invalid SemVer hint (pin).', 188 | type: 'Literal', 189 | }], 190 | }, 191 | ], 192 | ...[ // packages 193 | { 194 | code: '{ "dependencies": { "foo": "^1.2.3", "bar": "^1.2.3" } }', 195 | filename: 'package.json', 196 | options: [{ packages: ['foo'], versionHint: 'pin' }], 197 | errors: [{ 198 | message: 'Invalid SemVer hint (pin).', 199 | type: 'Literal', 200 | }], 201 | }, 202 | ], 203 | ...[ // packageRegex 204 | { 205 | code: '{ "dependencies": { "foo": "^1.2.3", "bar": "^1.2.3" } }', 206 | filename: 'package.json', 207 | options: [{ packageRegex: 'foo', versionHint: 'pin' }], 208 | errors: [{ 209 | message: 'Invalid SemVer hint (pin).', 210 | type: 'Literal', 211 | }], 212 | }, 213 | ], 214 | ...[ // grouping 215 | { 216 | code: '{ "dependencies": { "foo": "^1.2.3", "bar": "^1.2.3" } }', 217 | filename: 'package.json', 218 | options: [[ 219 | { packages: ['foo'], versionHint: 'caret' }, 220 | { packageRegex: 'bar', versionHint: 'pin' }, 221 | ]], 222 | errors: [{ 223 | message: 'Invalid SemVer hint (pin).', 224 | type: 'Literal', 225 | }], 226 | }, 227 | ], 228 | ...[ // pinUnstable 229 | { 230 | code: '{ "dependencies": { "foo": "^0.1.2", "bar": "^1.2.3" } }', 231 | filename: 'package.json', 232 | options: [{ pinUnstable: true }], 233 | errors: [{ 234 | message: 'Invalid SemVer hint on unstable.', 235 | type: 'Literal', 236 | }], 237 | }, 238 | { 239 | code: '{ "dependencies": { "foo": "^1.2.3-alpha.0", "bar": "^1.2.3" } }', 240 | filename: 'package.json', 241 | options: [{ pinUnstable: true }], 242 | errors: [{ 243 | message: 'Invalid SemVer hint on unstable.', 244 | type: 'Literal', 245 | }], 246 | }, 247 | { 248 | code: '{ "dependencies": { "foo": ">= 0.1.1" } }', 249 | filename: 'package.json', 250 | options: [{ pinUnstable: true }], 251 | errors: [{ 252 | message: 'Invalid SemVer hint on unstable.', 253 | type: 'Literal', 254 | }], 255 | }, 256 | { 257 | code: '{ "dependencies": { "foo": ">= 0.1.1 < 1" } }', 258 | filename: 'package.json', 259 | options: [{ pinUnstable: true }], 260 | errors: [{ 261 | message: 'Invalid SemVer hint on unstable.', 262 | type: 'Literal', 263 | }], 264 | }, 265 | { 266 | code: '{ "dependencies": { "foo": "0.1 || 1" } }', 267 | filename: 'package.json', 268 | options: [{ pinUnstable: true }], 269 | errors: [{ 270 | message: 'Invalid SemVer hint on unstable.', 271 | type: 'Literal', 272 | }], 273 | }, 274 | ], 275 | ], 276 | })); 277 | --------------------------------------------------------------------------------