├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── dependency-review.yml │ ├── lint.yml │ └── nodejs.yml ├── .gitignore ├── .husky └── pre-push ├── .knip.jsonc ├── .npmrc ├── LICENSE ├── README.md ├── cli.js ├── declaration.tsconfig.json ├── docs └── cli-output2.png ├── lib ├── advanced-types.d.ts ├── ajv-helper.js ├── ajv.cjs ├── commands │ ├── command-types.d.ts │ ├── compare.js │ ├── diff.js │ ├── index.js │ └── summary.js ├── compare.js ├── flags │ ├── flag-types.d.ts │ ├── input.js │ ├── misc.js │ └── output.js ├── format-json.js ├── print-diff.js ├── print-result.js ├── print-summary.js └── utils │ ├── diff.js │ ├── errors.js │ └── misc.js ├── new.eslintrc ├── package.json ├── renovate.json ├── test ├── .eslintrc ├── compare.spec.js └── data │ └── voxpelli-eslint.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage/**/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli", 3 | "root": true, 4 | "rules": { 5 | "func-style": ["warn", "declaration", { "allowArrowFunctions": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dependency-review: 10 | uses: voxpelli/ghatemplates/.github/workflows/dependency-review.yml@main 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | uses: voxpelli/ghatemplates/.github/workflows/test.yml@main 19 | with: 20 | node-versions: '18,20' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Basic ones 2 | /coverage 3 | /coverage-ts 4 | /node_modules 5 | /.env 6 | /.nyc_output 7 | 8 | # We're a library, so please, no lock files 9 | /package-lock.json 10 | /yarn.lock 11 | 12 | # Generated types 13 | *.d.*ts 14 | *.d.*ts.map 15 | !/lib/**/*-types.d.*ts 16 | 17 | # Library specific ones 18 | /test.md 19 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.knip.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@2/schema.json", 3 | "entry": ["cli.js!"], 4 | "ignoreDependencies": ["@types/mdast", "@types/mocha"] 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Pelle Wessman 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 | # Compare ESLint configs 2 | 3 | [![npm version](https://img.shields.io/npm/v/compare-eslint-configs.svg?style=flat)](https://www.npmjs.com/package/compare-eslint-configs) 4 | [![npm downloads](https://img.shields.io/npm/dm/compare-eslint-configs.svg?style=flat)](https://www.npmjs.com/package/compare-eslint-configs) 5 | [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg)](https://github.com/voxpelli/eslint-config) 6 | [![ES Module Ready Badge](https://img.shields.io/badge/es%20module%20ready-yes-success.svg)](https://esmodules.dev/) 7 | [![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js) 8 | 9 | ## Usage 10 | 11 | ```bash 12 | npm install -g compare-eslint-configs 13 | compare-eslint-configs compare .eslintrc new.eslintrc 14 | ``` 15 | 16 | Or simply: 17 | 18 | ```bash 19 | npx compare-eslint-configs compare .eslintrc new.eslintrc 20 | ``` 21 | 22 | ## Commands 23 | 24 | Found by running `compare-eslint-configs --help` 25 | 26 | * **compare** - compares the provided eslint config file(s) 27 | * **diff** - prints what's changed between the second and the first file 28 | * **summary** - prints a summary of the specified configs 29 | 30 | ## Options 31 | 32 | Found by running `compare-eslint-configs --help`, eg: `compare-eslint-configs compare --help` 33 | 34 | ## Example 35 | 36 | ### CLI output 37 | 38 | ```bash 39 | npx compare-eslint-configs compare new.eslintrc -t cli.js 40 | ``` 41 | 42 | ![CLI output](docs/cli-output2.png) 43 | 44 | ### Markdown output 45 | 46 | ```bash 47 | npx compare-eslint-configs compare new.eslintrc -t cli.js -m 48 | ``` 49 | 50 | > # Only active in some: 51 | > 52 | > * **new.eslintrc** 53 | > * [for-direction](https://eslint.org/docs/rules/for-direction) 54 | > 55 | > 56 | > # Mixed severities: 57 | > 58 | > * [**func-style**](https://eslint.org/docs/rules/func-style) 59 | > * _warn_: .eslintrc 60 | > * _error_: new.eslintrc 61 | > * [**unicorn/prefer-event-target**](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v43.0.2/docs/rules/prefer-event-target.md) 62 | > * _warn_: .eslintrc 63 | > * _error_: new.eslintrc 64 | > 65 | > 66 | > # Mixed configs where otherwise okay: 67 | > 68 | > * [**no-console**](https://eslint.org/docs/rules/no-console) 69 | > * _new.eslintrc_: 70 | > ```json 71 | > [{"allow":["warn","error"]}] 72 | > ``` 73 | 74 | ## See also 75 | 76 | * [`@voxpelli/eslint-formatter-summary`](https://github.com/voxpelli/eslint-formatter-summary) – can summarize errors/warnings by ESLint rule + print that list as markdown 77 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { isErrorWithCode } from '@voxpelli/typed-utils'; 4 | import chalk from 'chalk'; 5 | import { peowlyCommands } from 'peowly-commands'; 6 | import { messageWithCauses, stackWithCauses } from 'pony-cause'; 7 | 8 | import { cliCommands } from './lib/commands/index.js'; 9 | import { InputError, ResultError } from './lib/utils/errors.js'; 10 | 11 | try { 12 | await peowlyCommands(cliCommands, { 13 | importMeta: import.meta, 14 | name: 'compare-eslint-configs', 15 | }); 16 | } catch (err) { 17 | /** @type {string|undefined} */ 18 | let errorTitle; 19 | /** @type {string} */ 20 | let errorMessage = ''; 21 | /** @type {string|undefined} */ 22 | let errorBody; 23 | 24 | if (err instanceof ResultError) { 25 | process.exit(2); 26 | } 27 | 28 | if (err instanceof InputError) { 29 | errorTitle = 'Invalid input'; 30 | errorMessage = err.message; 31 | errorBody = err.body; 32 | } else if (isErrorWithCode(err) && (err.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION' || err.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE')) { 33 | errorTitle = 'Invalid input'; 34 | errorMessage = err.message; 35 | } 36 | 37 | if (!errorTitle) { 38 | if (err instanceof Error) { 39 | errorTitle = 'Unexpected error'; 40 | errorMessage = messageWithCauses(err); 41 | errorBody = stackWithCauses(err); 42 | } else { 43 | errorTitle = 'Unexpected error with no details'; 44 | } 45 | } 46 | 47 | // eslint-disable-next-line no-console 48 | console.error(`${chalk.white.bgRed(errorTitle + ':')} ${errorMessage}`); 49 | if (errorBody) { 50 | // eslint-disable-next-line no-console 51 | console.error('\n' + errorBody); 52 | } 53 | 54 | process.exit(1); 55 | } 56 | -------------------------------------------------------------------------------- /declaration.tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": [ 5 | "test/**/*.js" 6 | ], 7 | "compilerOptions": { 8 | "declaration": true, 9 | "declarationMap": true, 10 | "noEmit": false, 11 | "emitDeclarationOnly": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/cli-output2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpelli/compare-eslint-configs/36347af7f5f725e46b13b21512397288e52397a4/docs/cli-output2.png -------------------------------------------------------------------------------- /lib/advanced-types.d.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = 2 | T extends Record 3 | ? {[Key in keyof T]?: DeepPartial} 4 | : T extends unknown[] 5 | ? Array> 6 | : T; 7 | -------------------------------------------------------------------------------- /lib/ajv-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {unknown} schema 3 | * @returns {object|undefined} 4 | */ 5 | export function getRuleOptionsSchema (schema) { 6 | if (Array.isArray(schema)) { 7 | if (schema.length) { 8 | return { 9 | type: 'array', 10 | items: schema, 11 | minItems: 0, 12 | maxItems: schema.length, 13 | }; 14 | } 15 | return { 16 | type: 'array', 17 | minItems: 0, 18 | maxItems: 0, 19 | }; 20 | } 21 | 22 | return schema && typeof schema === 'object' ? schema : undefined; 23 | } 24 | -------------------------------------------------------------------------------- /lib/ajv.cjs: -------------------------------------------------------------------------------- 1 | // Copied from https://raw.githubusercontent.com/eslint/eslint/main/lib/shared/ajv.js 2 | // Copyright OpenJS Foundation and other contributors, 3 | // MIT Licensed 4 | 5 | 'use strict'; 6 | 7 | // ------------------------------------------------------------------------------ 8 | // Requirements 9 | // ------------------------------------------------------------------------------ 10 | 11 | const Ajv = require('ajv'); 12 | const metaSchema = require('ajv/lib/refs/json-schema-draft-04.json'); 13 | 14 | // ------------------------------------------------------------------------------ 15 | // Public Interface 16 | // ------------------------------------------------------------------------------ 17 | 18 | module.exports = (additionalOptions = {}) => { 19 | const ajv = new Ajv({ 20 | meta: false, 21 | useDefaults: true, 22 | validateSchema: false, 23 | missingRefs: 'ignore', 24 | verbose: true, 25 | schemaId: 'auto', 26 | ...additionalOptions, 27 | }); 28 | 29 | ajv.addMetaSchema(metaSchema); 30 | // @ts-ignore 31 | ajv._opts.defaultMeta = metaSchema.id; 32 | 33 | return ajv; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/commands/command-types.d.ts: -------------------------------------------------------------------------------- 1 | import type { BaseFlags, InputContext, OutputFlags } from '../flags/flag-types.js'; 2 | 3 | interface CommandContextBase extends BaseFlags, InputContext {} 4 | 5 | export interface CommandContextDiff extends CommandContextBase, OutputFlags { 6 | exitCode: boolean; 7 | } 8 | 9 | export interface CommandContextSummary extends CommandContextBase, OutputFlags {} 10 | -------------------------------------------------------------------------------- /lib/commands/compare.js: -------------------------------------------------------------------------------- 1 | import { peowly } from 'peowly'; 2 | 3 | import { InputError } from '../utils/errors.js'; 4 | 5 | import { baseFlags } from '../flags/misc.js'; 6 | import { outputFlags, resolveOutputFlags } from '../flags/output.js'; 7 | import { inputFlags, resolveInputContext } from '../flags/input.js'; 8 | import { compareConfigs } from '../compare.js'; 9 | import { printComparationResult } from '../print-result.js'; 10 | 11 | /** @type {import('peowly-commands').CliCommand} */ 12 | export const compare = { 13 | description: 'Compares the provided eslint config file(s)', 14 | 15 | async run (argv, meta, { parentName }) { 16 | const name = parentName + ' compare'; 17 | 18 | const { 19 | configFiles, 20 | configs, 21 | groupByRule, 22 | // TODO: Remove flag 23 | // jsonOutput, 24 | markdownOutput, 25 | skipLinks, 26 | // TODO: Remove flag 27 | // table, 28 | // TODO: Remove flag 29 | // verboseConfigs, 30 | } = await setupCommand(name, compare.description, argv, meta); 31 | 32 | const differences = compareConfigs(configs); 33 | 34 | printComparationResult(differences, configFiles, { 35 | groupByRule, 36 | markdown: markdownOutput, 37 | skipLinks, 38 | // table, 39 | // verboseConfigs, 40 | }); 41 | }, 42 | }; 43 | 44 | // Internal functions 45 | 46 | /** 47 | * @param {string} name 48 | * @param {string} description 49 | * @param {string[]} args 50 | * @param {import('peowly-commands').CliMeta} meta 51 | * @returns {Promise} 52 | */ 53 | async function setupCommand (name, description, args, { pkg }) { 54 | const options = /** @satisfies {import('peowly').AnyFlags} */ ({ 55 | ...baseFlags, 56 | ...inputFlags, 57 | ...outputFlags, 58 | }); 59 | 60 | const { 61 | flags: { 62 | verbose, 63 | ...remainingFlags 64 | }, 65 | input, 66 | showHelp, 67 | } = peowly({ 68 | args, 69 | description, 70 | examples: [ 71 | 'target.eslintrc.json', 72 | '-t foo.js source.eslintrc.json target.eslintrc.json', 73 | ], 74 | name, 75 | options, 76 | pkg, 77 | usage: '', 78 | }); 79 | 80 | const configFiles = [...input]; 81 | 82 | if (configFiles.length === 0) { 83 | showHelp(); 84 | // eslint-disable-next-line unicorn/no-process-exit 85 | process.exit(); 86 | } 87 | if (configFiles.length === 1) { 88 | if (configFiles[0] === '.eslintrc') { 89 | throw new InputError('There is nothing to compare .eslintrc to, add another config'); 90 | } 91 | configFiles.unshift('.eslintrc'); 92 | } 93 | 94 | /** @type {import('./command-types.js').CommandContextSummary} */ 95 | const result = { 96 | verbose, 97 | ...await resolveInputContext(remainingFlags, configFiles), 98 | ...resolveOutputFlags(remainingFlags), 99 | }; 100 | 101 | return result; 102 | } 103 | -------------------------------------------------------------------------------- /lib/commands/diff.js: -------------------------------------------------------------------------------- 1 | import { peowly } from 'peowly'; 2 | 3 | import { InputError } from '../utils/errors.js'; 4 | 5 | import { baseFlags } from '../flags/misc.js'; 6 | import { outputFlags, resolveOutputFlags } from '../flags/output.js'; 7 | import { inputFlags, resolveInputContext } from '../flags/input.js'; 8 | import { diffConfigs } from '../compare.js'; 9 | import { printDiffResult } from '../print-diff.js'; 10 | 11 | /** @type {import('peowly-commands').CliCommand} */ 12 | export const diff = { 13 | description: 'Prints what\'s changed between the second and the first file', 14 | 15 | async run (argv, meta, { parentName }) { 16 | const name = parentName + ' diff'; 17 | 18 | const context = await setupCommand(name, diff.description, argv, meta); 19 | 20 | const workResult = context && await doTheWork(context); 21 | 22 | if (workResult) { 23 | formatWorkResult(workResult, context); 24 | } 25 | }, 26 | }; 27 | 28 | // Internal functions 29 | 30 | /** 31 | * @param {string} name 32 | * @param {string} description 33 | * @param {string[]} args 34 | * @param {import('peowly-commands').CliMeta} meta 35 | * @returns {Promise} 36 | */ 37 | async function setupCommand (name, description, args, { pkg }) { 38 | const options = /** @satisfies {import('peowly').AnyFlags} */ ({ 39 | ...baseFlags, 40 | ...inputFlags, 41 | ...outputFlags, 42 | 'exit-code': { 43 | description: 'Make the program exit with codes similar to diff, 1 if there were differences else 0', 44 | listGroup: 'Diff options', 45 | 'short': 'e', 46 | type: 'boolean', 47 | 'default': false, 48 | }, 49 | }); 50 | 51 | const { 52 | flags: { 53 | 'exit-code': exitCode, 54 | verbose, 55 | ...remainingFlags 56 | }, 57 | input, 58 | showHelp, 59 | } = peowly({ 60 | args, 61 | description, 62 | examples: [ 63 | 'target.eslintrc.json', 64 | '-t foo.js source.eslintrc.json target.eslintrc.json', 65 | ], 66 | name, 67 | options, 68 | pkg, 69 | usage: '[] ', 70 | }); 71 | 72 | const configFiles = [...input]; 73 | 74 | if (configFiles.length === 0) { 75 | showHelp(); 76 | // eslint-disable-next-line unicorn/no-process-exit 77 | process.exit(); 78 | } 79 | if (configFiles.length === 1) { 80 | if (configFiles[0] === '.eslintrc') { 81 | throw new InputError('There is nothing to compare .eslintrc to, add another config'); 82 | } 83 | configFiles.unshift('.eslintrc'); 84 | } 85 | 86 | /** @type {import('./command-types.js').CommandContextDiff} */ 87 | const result = { 88 | exitCode, 89 | verbose, 90 | ...await resolveInputContext(remainingFlags, configFiles), 91 | ...resolveOutputFlags(remainingFlags), 92 | }; 93 | 94 | return result; 95 | } 96 | 97 | /** 98 | * @param {Pick} context 99 | * @returns {Promise} 100 | */ 101 | async function doTheWork (context) { 102 | const { configFiles, configs, verbose } = context; 103 | 104 | const targetConfigName = configFiles[0] || ''; 105 | const sourceConfigName = configFiles[1] || ''; 106 | 107 | const targetConfig = configs[targetConfigName]; 108 | const sourceConfig = configs[sourceConfigName]; 109 | 110 | if (configFiles.length > 2 || !targetConfig || !sourceConfig) { 111 | throw new InputError('Diff calculation only works when given two configs'); 112 | } 113 | 114 | return diffConfigs( 115 | { configName: targetConfigName, config: targetConfig }, 116 | { configName: sourceConfigName, config: sourceConfig }, 117 | { verbose } 118 | ); 119 | } 120 | 121 | /** 122 | * @param {import('../compare.js').ConfigDiff} diffResult 123 | * @param {Omit} context 124 | * @returns {void} 125 | */ 126 | function formatWorkResult (diffResult, context) { 127 | const { 128 | exitCode, 129 | // TODO: Remove from available options 130 | // groupByRule, 131 | jsonOutput, 132 | markdownOutput, 133 | skipLinks, 134 | // TODO: Remove from available options 135 | // table, 136 | verboseConfigs, 137 | } = context; 138 | 139 | if (jsonOutput) { 140 | const { 141 | added, 142 | changedConfig, 143 | changedSeverity, 144 | removed, 145 | } = diffResult; 146 | 147 | // eslint-disable-next-line no-console 148 | console.log(JSON.stringify({ 149 | added, 150 | changedConfig, 151 | changedSeverity, 152 | removed, 153 | }, undefined, 2)); 154 | } else { 155 | printDiffResult(diffResult, { 156 | markdown: markdownOutput, 157 | skipLinks, 158 | // table, 159 | verboseConfigs, 160 | }); 161 | } 162 | 163 | if (exitCode) { 164 | process.exitCode = 1; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/commands/index.js: -------------------------------------------------------------------------------- 1 | import { compare } from './compare.js'; 2 | import { diff } from './diff.js'; 3 | import { summary } from './summary.js'; 4 | 5 | export const cliCommands = { 6 | compare, 7 | diff, 8 | summary, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/commands/summary.js: -------------------------------------------------------------------------------- 1 | import { peowly } from 'peowly'; 2 | 3 | import { baseFlags } from '../flags/misc.js'; 4 | import { outputFlags, resolveOutputFlags } from '../flags/output.js'; 5 | import { inputFlags, resolveInputContext } from '../flags/input.js'; 6 | import { summarizeConfigs } from '../compare.js'; 7 | import { printConfigSummary } from '../print-summary.js'; 8 | 9 | /** @type {import('peowly-commands').CliCommand} */ 10 | export const summary = { 11 | description: 'Prints a summary of the specified configs', 12 | 13 | async run (argv, meta, { parentName }) { 14 | const name = parentName + ' summary'; 15 | 16 | const { 17 | configs, 18 | markdownOutput, 19 | skipLinks, 20 | table, 21 | verboseConfigs, 22 | // TODO: Remove flag 23 | // groupByRule, 24 | // TODO: Remove flag 25 | // jsonOutput, 26 | } = await setupCommand(name, summary.description, argv, meta); 27 | 28 | for (const [configName, config] of Object.entries(configs)) { 29 | const summarizedRules = summarizeConfigs({ [configName]: config }); 30 | 31 | printConfigSummary(configName, summarizedRules, { 32 | markdown: markdownOutput, 33 | skipLinks, 34 | table, 35 | verboseConfigs, 36 | }); 37 | } 38 | }, 39 | }; 40 | 41 | // Internal functions 42 | 43 | /** 44 | * @param {string} name 45 | * @param {string} description 46 | * @param {string[]} args 47 | * @param {import('peowly-commands').CliMeta} meta 48 | * @returns {Promise} 49 | */ 50 | async function setupCommand (name, description, args, { pkg }) { 51 | const options = /** @satisfies {import('peowly').AnyFlags} */ ({ 52 | ...baseFlags, 53 | ...inputFlags, 54 | ...outputFlags, 55 | }); 56 | 57 | const { 58 | flags: { 59 | verbose, 60 | ...remainingFlags 61 | }, 62 | input, 63 | } = peowly({ 64 | args, 65 | description, 66 | examples: [ 67 | '', 68 | '-t foo.js alternative.eslintrc.json', 69 | ], 70 | name, 71 | options, 72 | pkg, 73 | usage: '', 74 | }); 75 | 76 | const configFiles = [...input]; 77 | 78 | if (configFiles.length === 0) { 79 | configFiles.unshift('.eslintrc'); 80 | } 81 | 82 | /** @type {import('./command-types.js').CommandContextSummary} */ 83 | const result = { 84 | verbose, 85 | ...await resolveInputContext(remainingFlags, configFiles), 86 | ...resolveOutputFlags(remainingFlags), 87 | }; 88 | 89 | return result; 90 | } 91 | -------------------------------------------------------------------------------- /lib/compare.js: -------------------------------------------------------------------------------- 1 | import equal from 'fast-deep-equal'; 2 | import ajvImport from './ajv.cjs'; 3 | import { getRuleOptionsSchema } from './ajv-helper.js'; 4 | import { getDeepDifference } from './utils/diff.js'; 5 | 6 | const ajv = ajvImport(); 7 | 8 | /** @type {Record} */ 9 | const LEVEL_TO_SEVERITY = { 10 | '0': undefined, 11 | '1': 'warn', 12 | '2': 'error', 13 | 'off': undefined, 14 | 'warn': 'warn', 15 | 'error': 'error', 16 | }; 17 | 18 | /** 19 | * @typedef Config 20 | * @property {import('eslint').Linter.Config} config 21 | * @property {import('eslint').ESLint} engine 22 | */ 23 | 24 | // TODO: Type the JSON Schema better here 25 | /** 26 | * @typedef RuleSummary 27 | * @property {string[]} [warn] 28 | * @property {string[]} [error] 29 | * @property {Set} [docUrls] 30 | * @property {{ [configName: string]: object }} [schemas] 31 | * @property {{ [configName: string]: unknown[] }} [options] 32 | * @property {{ [configName: string]: unknown[] }} [overridenOptions] 33 | */ 34 | 35 | /** 36 | * @param {{ [configName: string]: Config }} configs 37 | * @param {{ verbose?: boolean }} [options] 38 | * @returns {{ [ruleName: string]: RuleSummary}} 39 | */ 40 | export function summarizeConfigs (configs, { verbose } = {}) { 41 | /** @type {{ [ruleName: string]: RuleSummary}} */ 42 | const foundRules = {}; 43 | 44 | for (const [configName, { config, engine }] of Object.entries(configs)) { 45 | const relevantConfigRules = []; 46 | const { rules = {} } = config; 47 | 48 | for (const [ruleName, rule] of Object.entries(rules)) { 49 | if (rule === undefined) continue; 50 | 51 | const [level, ...options] = Array.isArray(rule) ? rule : [rule]; 52 | 53 | const severity = LEVEL_TO_SEVERITY[level]; 54 | 55 | if (!severity) continue; 56 | 57 | relevantConfigRules.push(ruleName); 58 | 59 | const foundRulesData = foundRules[ruleName] || (foundRules[ruleName] = {}); 60 | const ruleSeverityToConfig = foundRulesData[severity] || (foundRulesData[severity] = []); 61 | 62 | ruleSeverityToConfig.push(configName); 63 | 64 | if (options.length) { 65 | const ruleOptionsToConfig = foundRulesData.options || (foundRulesData.options = {}); 66 | ruleOptionsToConfig[configName] = options; 67 | } 68 | } 69 | 70 | const rulesMeta = engine.getRulesMetaForResults([{ 71 | filePath: '', 72 | messages: relevantConfigRules.map(ruleId => ({ ruleId, column: 1, line: 1, message: '', severity: 2 })), 73 | suppressedMessages: [], 74 | errorCount: 1, 75 | fixableErrorCount: 1, 76 | fatalErrorCount: 1, 77 | warningCount: 1, 78 | fixableWarningCount: 1, 79 | usedDeprecatedRules: [], 80 | }]); 81 | for (const ruleId in rulesMeta) { 82 | const docUrl = rulesMeta[ruleId]?.docs?.url; 83 | const schema = getRuleOptionsSchema(rulesMeta[ruleId]?.schema); 84 | const foundRule = foundRules[ruleId]; 85 | 86 | if (!foundRule) continue; 87 | 88 | if (docUrl) { 89 | const docUrls = foundRule.docUrls || new Set(); 90 | docUrls.add(docUrl); 91 | foundRule.docUrls = docUrls; 92 | } 93 | 94 | if (schema) { 95 | const schemas = foundRule.schemas || {}; 96 | schemas[configName] = schema; 97 | foundRule.schemas = schemas; 98 | } 99 | } 100 | } 101 | 102 | for (const ruleId in foundRules) { 103 | const foundRule = foundRules[ruleId]; 104 | 105 | if (!foundRule) continue; 106 | if (!foundRule.schemas) continue; 107 | if (!foundRule.options) continue; 108 | 109 | for (const configName in foundRule.options) { 110 | const ruleSchema = foundRule.schemas[configName]; 111 | const ruleOptions = foundRule.options[configName]; 112 | 113 | if (!ruleSchema || !ruleOptions) continue; 114 | 115 | const isObjectConfig = ( 116 | 'type' in ruleSchema && ruleSchema.type === 'array' && 117 | 'items' in ruleSchema && Array.isArray(ruleSchema.items) && ruleSchema.items[0]?.type === 'object' 118 | ); 119 | /** @type {unknown[]} */ 120 | const defaults = isObjectConfig ? [{}] : []; 121 | const valid = ajv.validate(ruleSchema, defaults); 122 | 123 | if (!valid) { 124 | // eslint-disable-next-line no-console 125 | verbose && console.error(`Failed calculating default for rule id "${ruleId}" with given options:`, ruleOptions); 126 | } else { 127 | const overridenOptions = foundRule.overridenOptions || {}; 128 | overridenOptions[configName] = getDeepDifference(defaults, ruleOptions) || []; 129 | foundRule.overridenOptions = overridenOptions; 130 | } 131 | } 132 | } 133 | 134 | return foundRules; 135 | } 136 | 137 | /** 138 | * @typedef ConfigDifferences 139 | * @property {{ [ruleName: string]: string[] }} onlyActiveIn 140 | * @property {{ [ruleName: string]: { warn?: string[]|undefined, error?: string[]|undefined } }} mixedSeverity 141 | * @property {{ [ruleName: string]: { [configName: string]: unknown[] } }} mixedConfigs 142 | * @property {{ [ruleName: string]: string[] }} ruleDocs 143 | */ 144 | 145 | /** 146 | * @param {{ [configName: string]: Config }} configs 147 | * @returns {ConfigDifferences} 148 | */ 149 | export function compareConfigs (configs) { 150 | const configCount = Object.keys(configs).length; 151 | const summarizedRules = summarizeConfigs(configs); 152 | 153 | /** @type {ConfigDifferences["onlyActiveIn"]} */ 154 | const onlyActiveIn = {}; 155 | /** @type {ConfigDifferences["mixedSeverity"]} */ 156 | const mixedSeverity = {}; 157 | /** @type {ConfigDifferences["mixedConfigs"]} */ 158 | const mixedConfigs = {}; 159 | /** @type {ConfigDifferences["ruleDocs"]} */ 160 | const ruleDocs = {}; 161 | 162 | for (const [ruleName, { docUrls, error, options, warn }] of Object.entries(summarizedRules)) { 163 | const activeConfigs = [error || [], warn || []].flat(); 164 | const activeCount = activeConfigs.length; 165 | 166 | const activeEveryWhere = activeCount === configCount; 167 | 168 | if (error && warn) { 169 | mixedSeverity[ruleName] = { error, warn }; 170 | } 171 | 172 | if (!activeEveryWhere) { 173 | onlyActiveIn[ruleName] = activeConfigs; 174 | } 175 | 176 | if (docUrls) { 177 | ruleDocs[ruleName] = [...docUrls]; 178 | } 179 | 180 | const [baseOptions, ...remainingOptions] = Object.values(options || {}); 181 | 182 | if (options && baseOptions) { 183 | if (remainingOptions.length === activeCount - 1) { 184 | for (const configOptions of remainingOptions) { 185 | if (!equal(baseOptions, configOptions)) { 186 | mixedConfigs[ruleName] = options; 187 | break; 188 | } 189 | } 190 | } else { 191 | mixedConfigs[ruleName] = options; 192 | } 193 | } 194 | } 195 | 196 | return { 197 | onlyActiveIn, 198 | mixedSeverity, 199 | mixedConfigs, 200 | ruleDocs, 201 | }; 202 | } 203 | 204 | /** 205 | * @typedef RuleConfig 206 | * @property {'warn'|'error'} severity 207 | * @property {unknown[]|null} [options] 208 | * @property {unknown[]|null} [oldOptions] 209 | */ 210 | 211 | /** 212 | * @typedef ConfigDiff 213 | * @property {{ [ruleName: string]: RuleConfig }} added 214 | * @property {{ [ruleName: string]: RuleConfig }} removed 215 | * @property {{ [ruleName: string]: RuleConfig }} changedConfig 216 | * @property {{ [ruleName: string]: RuleConfig }} changedSeverity 217 | * @property {{ [ruleName: string]: string[] }} ruleDocs 218 | */ 219 | 220 | /** 221 | * @param {{ configName: string, config: Config }} target 222 | * @param {{ configName: string, config: Config }} source 223 | * @param {{ verbose?: boolean }} options 224 | * @returns {ConfigDiff | false} 225 | */ 226 | export function diffConfigs (target, source, { verbose = false }) { 227 | const summarizedRules = summarizeConfigs({ 228 | [target.configName]: target.config, 229 | [source.configName]: source.config, 230 | }, { verbose }); 231 | 232 | /** @type {ConfigDiff["added"]} */ 233 | const added = {}; 234 | /** @type {ConfigDiff["removed"]} */ 235 | const removed = {}; 236 | /** @type {ConfigDiff["changedConfig"]} */ 237 | const changedConfig = {}; 238 | /** @type {ConfigDiff["changedSeverity"]} */ 239 | const changedSeverity = {}; 240 | /** @type {ConfigDiff["ruleDocs"]} */ 241 | const ruleDocs = {}; 242 | 243 | for (const [ruleName, { docUrls, error, overridenOptions, warn }] of Object.entries(summarizedRules)) { 244 | const activeIn = (error || warn || []); 245 | 246 | if (docUrls) { 247 | ruleDocs[ruleName] = [...docUrls]; 248 | } 249 | 250 | if ((error && warn) || activeIn.length === 2) { 251 | // If active in both, check what has changed... 252 | 253 | const severity = error?.includes(source.configName) ? 'error' : 'warn'; 254 | 255 | if (error && warn) { 256 | changedSeverity[ruleName] = { severity }; 257 | } 258 | 259 | if (overridenOptions) { 260 | const targetOptions = overridenOptions[target.configName]; 261 | const sourceOptions = overridenOptions[source.configName]; 262 | 263 | if (targetOptions && sourceOptions) { 264 | if (!equal(targetOptions, sourceOptions)) { 265 | changedConfig[ruleName] = { 266 | severity, 267 | oldOptions: targetOptions, 268 | options: sourceOptions, 269 | }; 270 | } 271 | } else if (targetOptions || sourceOptions) { 272 | changedConfig[ruleName] = { 273 | severity, 274 | // eslint-disable-next-line unicorn/no-null 275 | oldOptions: targetOptions || null, 276 | // eslint-disable-next-line unicorn/no-null 277 | options: sourceOptions || null, 278 | }; 279 | } 280 | } 281 | } else if (activeIn.includes(source.configName)) { 282 | // ...else marked as added if only seen in the source config... 283 | added[ruleName] = { 284 | severity: error ? 'error' : 'warn', 285 | // eslint-disable-next-line unicorn/no-null 286 | options: (overridenOptions && overridenOptions[source.configName]) || null, 287 | }; 288 | } else { 289 | // ...and else as removed. 290 | removed[ruleName] = { 291 | severity: error ? 'error' : 'warn', 292 | // eslint-disable-next-line unicorn/no-null 293 | options: (overridenOptions && overridenOptions[target.configName]) || null, 294 | }; 295 | } 296 | } 297 | 298 | const hasDiff = Object.keys(added).length > 0 || 299 | Object.keys(removed).length > 0 || 300 | Object.keys(changedConfig).length > 0 || 301 | Object.keys(changedSeverity).length > 0; 302 | 303 | return hasDiff 304 | ? { 305 | added, 306 | removed, 307 | changedConfig, 308 | changedSeverity, 309 | ruleDocs, 310 | } 311 | : false; 312 | } 313 | -------------------------------------------------------------------------------- /lib/flags/flag-types.d.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Linter } from 'eslint'; 2 | 3 | export interface InputContext { 4 | configFiles: string[], 5 | configs: Record; 6 | } 7 | 8 | export interface BaseFlags { 9 | verbose: boolean; 10 | } 11 | 12 | export interface OutputFlags { 13 | groupByRule: boolean; 14 | jsonOutput: boolean; 15 | markdownOutput: boolean; 16 | skipLinks: boolean; 17 | table: boolean; 18 | verboseConfigs: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /lib/flags/input.js: -------------------------------------------------------------------------------- 1 | import { stat } from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { cwd } from 'node:process'; 4 | 5 | import { ESLint } from 'eslint'; 6 | 7 | import { zipObject } from '../utils/misc.js'; 8 | import { InputError } from '../utils/errors.js'; 9 | 10 | export const inputFlags = /** @satisfies {Record} */ ({ 11 | targetFile: { 12 | description: 'The file context which the eslint config should be compared in. Defaults to index.js', 13 | listGroup: 'Input options', 14 | 'short': 't', 15 | type: 'string', 16 | 'default': 'index.js', 17 | }, 18 | }); 19 | 20 | /** 21 | * @param {import('peowly').TypedFlags} flags 22 | * @param {string[]} configFiles 23 | * @returns {Promise} 24 | */ 25 | export async function resolveInputContext (flags, configFiles) { 26 | const { 27 | targetFile, 28 | } = flags; 29 | 30 | const targetAbsolute = path.resolve(cwd(), targetFile); 31 | // eslint-disable-next-line security/detect-non-literal-fs-filename 32 | const targetStat = await stat(targetAbsolute).catch(() => {}); 33 | 34 | if (!targetStat || !targetStat.isFile) { 35 | // TODO: Add a fallback that automatically finds a JS file in the current folder 36 | throw new InputError(`Can't find "${targetAbsolute}", set a target with --target-file`); 37 | } 38 | 39 | const configs = await resolveConfigs(configFiles, targetAbsolute); 40 | 41 | return { 42 | configFiles, 43 | configs, 44 | }; 45 | } 46 | 47 | /** 48 | * @param {string[]} configFiles 49 | * @param {string} targetAbsolute 50 | * @returns {Promise>} 51 | */ 52 | async function resolveConfigs (configFiles, targetAbsolute) { 53 | const configs = await Promise.all(configFiles.map(async configFile => { 54 | const engine = await resolveEngine(configFile); 55 | 56 | /** @type {import('eslint').Linter.Config} */ 57 | const config = await engine.calculateConfigForFile(targetAbsolute); 58 | 59 | return { config, engine }; 60 | })); 61 | 62 | const result = zipObject(configFiles, configs); 63 | 64 | return result; 65 | } 66 | 67 | /** 68 | * @param {string} configFile 69 | * @returns {Promise} 70 | */ 71 | async function resolveEngine (configFile) { 72 | const configFileAbsolute = path.resolve(cwd(), configFile); 73 | // eslint-disable-next-line security/detect-non-literal-fs-filename 74 | const configStat = await stat(configFileAbsolute); 75 | 76 | if (!configStat.isFile) { 77 | throw new InputError(`Can't find a config file "${configFileAbsolute}"`); 78 | } 79 | 80 | const engine = new ESLint({ 81 | useEslintrc: false, 82 | overrideConfigFile: configFileAbsolute, 83 | }); 84 | 85 | return engine; 86 | } 87 | -------------------------------------------------------------------------------- /lib/flags/misc.js: -------------------------------------------------------------------------------- 1 | export const baseFlags = /** @satisfies {Record} */ ({ 2 | verbose: { 3 | description: 'Prints verbose output such as debug messages', 4 | 'short': 'v', 5 | type: 'boolean', 6 | 'default': false, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /lib/flags/output.js: -------------------------------------------------------------------------------- 1 | import { InputError } from '../utils/errors.js'; 2 | 3 | export const outputFlags = /** @satisfies {Record} */ ({ 4 | 'group-rules': { 5 | description: 'Prints the rules that only exists in some configs grouped by rule rather than config', 6 | listGroup: 'Output options', 7 | 'short': 'r', 8 | type: 'boolean', 9 | 'default': false, 10 | }, 11 | json: { 12 | description: 'Output the results as JSON instead', 13 | listGroup: 'Output options', 14 | 'short': 'j', 15 | type: 'boolean', 16 | 'default': false, 17 | }, 18 | markdown: { 19 | description: 'Format the result as Markdown instead. Useful for copy and pasting into eg. GitHub', 20 | listGroup: 'Output options', 21 | 'short': 'm', 22 | type: 'boolean', 23 | 'default': false, 24 | }, 25 | 'no-links': { 26 | description: 'Skips adding hyperlinks to rule documentation', 27 | listGroup: 'Output options', 28 | type: 'boolean', 29 | 'default': false, 30 | }, 31 | table: { 32 | description: 'Prints the output in a table format rather than list format', 33 | listGroup: 'Output options', 34 | type: 'boolean', 35 | 'default': false, 36 | }, 37 | verboseConfigs: { 38 | description: 'Includes full config data in the output, even the complex one', 39 | listGroup: 'Output options', 40 | type: 'boolean', 41 | 'default': false, 42 | }, 43 | }); 44 | 45 | /** 46 | * @param {import('peowly').TypedFlags} flags 47 | * @returns {import('./flag-types.d.ts').OutputFlags} 48 | */ 49 | export function resolveOutputFlags (flags) { 50 | const { 51 | 'group-rules': groupByRule, 52 | json: jsonOutput, 53 | markdown: markdownOutput, 54 | 'no-links': skipLinks, 55 | table, 56 | verboseConfigs, 57 | } = flags; 58 | 59 | if (jsonOutput && markdownOutput) { 60 | throw new InputError('Can\'t chose both JSON and Markdown at once'); 61 | } 62 | 63 | return { 64 | groupByRule, 65 | jsonOutput, 66 | markdownOutput, 67 | skipLinks, 68 | table, 69 | verboseConfigs, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /lib/format-json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {object} data 3 | * @param {string} summary 4 | * @param {boolean} includeVerbose 5 | * @returns {import('mdast').PhrasingContent[]} 6 | */ 7 | export function formatJsonForMarkdown (data, summary, includeVerbose) { 8 | if (isNonVerboseJson(data)) { 9 | return [{ 10 | type: 'inlineCode', 11 | value: JSON.stringify(data), 12 | }]; 13 | } 14 | 15 | if (includeVerbose) { 16 | return [ 17 | { type: 'html', value: `
${summary}
` },
18 |       ...(
19 |         JSON.stringify(data, undefined, 2)
20 |           .split('\n')
21 |           .flatMap(value => /** @type {const} */ ([
22 |             { type: 'text', value },
23 |             { type: 'html', value: '
' }, 24 | ])) 25 | ), 26 | { type: 'html', value: '
' }, 27 | ]; 28 | } 29 | 30 | return [{ type: 'text', value: summary }]; 31 | } 32 | 33 | /** 34 | * @param {object} data 35 | * @param {boolean} includeVerbose 36 | * @returns {import('mdast').PhrasingContent|undefined} 37 | */ 38 | export function formatJsonForMdast (data, includeVerbose) { 39 | if (includeVerbose || isNonVerboseJson(data)) { 40 | return { 41 | type: 'inlineCode', 42 | value: JSON.stringify(data), 43 | }; 44 | } 45 | } 46 | 47 | /** 48 | * @param {object} data 49 | * @returns {boolean} 50 | */ 51 | function isNonVerboseJson (data) { 52 | return Array.isArray(data) && data.length === 1 && typeof data[0] === 'string'; 53 | } 54 | -------------------------------------------------------------------------------- /lib/print-diff.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { 4 | MarkdownOrChalk, 5 | mdastLinkify, 6 | mdastListHelper, 7 | } from 'markdown-or-chalk'; 8 | 9 | import { formatJsonForMdast } from './format-json.js'; 10 | import { sortRulesByName } from './utils/misc.js'; 11 | import { getDeepDifference } from './utils/diff.js'; 12 | 13 | /** @typedef {Omit} PrintOptions */ 14 | 15 | /** 16 | * @param {import('./compare.js').ConfigDiff} diffResult 17 | * @param {PrintOptions} options 18 | * @returns {void} 19 | */ 20 | export function printDiffResult (diffResult, options) { 21 | const format = new MarkdownOrChalk(options.markdown); 22 | 23 | if (Object.keys(diffResult.added).length) { 24 | console.log(format.header('Added')); 25 | console.log(format.fromMdast(formatRuleList(sortRulesByName(diffResult.added), diffResult.ruleDocs, options))); 26 | } 27 | 28 | if (Object.keys(diffResult.removed).length) { 29 | console.log(format.header('Removed')); 30 | console.log(format.fromMdast(formatRuleList(sortRulesByName(diffResult.removed), diffResult.ruleDocs, options))); 31 | } 32 | 33 | if (Object.keys(diffResult.changedSeverity).length) { 34 | console.log(format.header('Changed severity')); 35 | console.log(format.fromMdast(formatRuleList(sortRulesByName(diffResult.changedSeverity), diffResult.ruleDocs, options))); 36 | } 37 | 38 | if (Object.keys(diffResult.changedConfig).length) { 39 | console.log(format.header('Changed config')); 40 | console.log(format.fromMdast(formatRuleList(sortRulesByName(diffResult.changedConfig), diffResult.ruleDocs, options))); 41 | } 42 | } 43 | 44 | /** 45 | * @param {import('./compare.js').ConfigDiff["added"]} rules 46 | * @param {import('./compare.js').ConfigDiff["ruleDocs"]} ruleDocs 47 | * @param {PrintOptions} options 48 | * @returns {import('mdast').List} 49 | */ 50 | function formatRuleList (rules, ruleDocs, { markdown, skipLinks, verboseConfigs }) { 51 | let longestRuleNameWithOption = 0; 52 | 53 | for (const [ruleName, { options }] of Object.entries(rules)) { 54 | if (options && ruleName.length > longestRuleNameWithOption) { 55 | longestRuleNameWithOption = ruleName.length; 56 | } 57 | } 58 | 59 | const format = new MarkdownOrChalk(markdown); 60 | 61 | /** @type {import('markdown-or-chalk').PhrasingContentOrStringList[]} */ 62 | const formattedRules = []; 63 | 64 | for (const [ruleName, ruleConfig] of Object.entries(rules)) { 65 | const docUrl = (ruleDocs[ruleName] || [])[0]; 66 | 67 | // TODO: Improve diff presentation in chalk and markdown output, use maybe eg. jest-diff 68 | // TODO: When `verbosed-configs` is used, print the config on their own lines here 69 | const addedOptions = getDeepDifference(ruleConfig.oldOptions || [], ruleConfig.options || []); 70 | const removedOptions = getDeepDifference(ruleConfig.options || [], ruleConfig.oldOptions || []); 71 | 72 | const formattedAddedOptions = addedOptions ? formatJsonForMdast(addedOptions, verboseConfigs) : undefined; 73 | const formattedRemovedOptions = removedOptions ? formatJsonForMdast(removedOptions, verboseConfigs) : undefined; 74 | 75 | formattedRules.push([ 76 | format.logSymbolsMdast[ruleConfig.severity === 'error' ? 'error' : 'warning'], 77 | (ruleConfig.options ? (format.chalkOnly ? format.logSymbolsMdast.info : ':wrench:') : ''), 78 | ' ', 79 | mdastLinkify(ruleName, docUrl, skipLinks), 80 | ruleConfig.options && format.chalkOnly ? ''.padEnd(longestRuleNameWithOption - ruleName.length + 2, ' ') : '', 81 | removedOptions ? ' config removed' + (formattedRemovedOptions ? ': ' : '') : '', 82 | formattedRemovedOptions ?? '', 83 | addedOptions ? (removedOptions ? ',' : '') + ' config added' + (formattedAddedOptions ? ': ' : '') : '', 84 | formattedAddedOptions ?? '', 85 | ]); 86 | } 87 | 88 | return mdastListHelper(formattedRules); 89 | } 90 | -------------------------------------------------------------------------------- /lib/print-result.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { MarkdownOrChalk } from 'markdown-or-chalk'; 4 | 5 | /** 6 | * @param {string} level 7 | * @returns {string} 8 | */ 9 | const getReadableLevel = (level) => { 10 | if (level === '0') return 'off'; 11 | if (level === '1') return 'warn'; 12 | if (level === '2') return 'error'; 13 | return level; 14 | }; 15 | 16 | /** 17 | * @typedef PrintOptions 18 | * @property {boolean} markdown 19 | * @property {boolean} skipLinks 20 | * @property {boolean} table 21 | * @property {boolean} verboseConfigs 22 | */ 23 | 24 | /** 25 | * @param {import('./compare.js').ConfigDifferences} differences 26 | * @param {string[]} configNames 27 | * @param {Omit & { groupByRule: boolean }} options 28 | * @returns {void} 29 | */ 30 | export function printComparationResult (differences, configNames, options) { 31 | const { 32 | mixedConfigs, 33 | mixedSeverity, 34 | onlyActiveIn, 35 | ruleDocs, 36 | } = differences; 37 | 38 | const { 39 | groupByRule = false, 40 | markdown = false, 41 | } = options || {}; 42 | 43 | const format = new MarkdownOrChalk(markdown); 44 | 45 | const onlyActiveInKeys = Object.keys(onlyActiveIn).sort(); 46 | if (onlyActiveInKeys.length) { 47 | /** @type {string[]} */ 48 | const items = []; 49 | 50 | if (groupByRule) { 51 | for (const ruleName of onlyActiveInKeys) { 52 | /** @type {string[]} */ 53 | const items = []; 54 | for (const configName of onlyActiveIn[ruleName]?.sort() || []) { 55 | items.push(configName); 56 | } 57 | if (items.length) { 58 | console.log(format.header( 59 | `${format.hyperlink(format.bold(ruleName), (ruleDocs[ruleName] || [])[0], { fallback: false })} only active in:`, 60 | 2 61 | )); 62 | console.log(' ' + items.join(', ' + '\n')); 63 | } 64 | } 65 | } else { 66 | for (const configName of configNames) { 67 | /** @type {string[]} */ 68 | const innerItems = []; 69 | for (const ruleName of onlyActiveInKeys) { 70 | if (onlyActiveIn[ruleName]?.includes(configName)) { 71 | innerItems.push(format.hyperlink(ruleName, (ruleDocs[ruleName] || [])[0], { fallback: false })); 72 | } 73 | } 74 | if (innerItems.length) { 75 | items.push(format.bold(configName) + '\n' + format.list(innerItems).trimEnd()); 76 | } 77 | } 78 | } 79 | 80 | if (items.length) { 81 | console.log(format.header('Only active in some:')); 82 | console.log(format.list(items)); 83 | } 84 | } 85 | 86 | const mixedSeverityEntries = Object.entries(mixedSeverity); 87 | if (mixedSeverityEntries.length) { 88 | /** @type {string[]} */ 89 | const items = []; 90 | for (const [ruleName, severities] of mixedSeverityEntries) { 91 | const formattedSeverities = Object.entries(severities) 92 | .map( 93 | ([level, configNames]) => 94 | format.italic(getReadableLevel(level)) + ': ' + (configNames || []).join(', ') 95 | ); 96 | items.push(format.hyperlink(format.bold(ruleName), (ruleDocs[ruleName] || [])[0], { fallback: false }) + '\n' + format.list(formattedSeverities).trimEnd()); 97 | } 98 | console.log(format.header('Mixed severities:')); 99 | console.log(format.list(items)); 100 | } 101 | 102 | const mixedConfigsKeys = Object.keys(mixedConfigs).sort(); 103 | if (mixedConfigsKeys.length) { 104 | /** @type {string[]} */ 105 | const items = []; 106 | for (const ruleName of mixedConfigsKeys) { 107 | /** @type {string[]} */ 108 | const innerItems = []; 109 | const mixedConfig = mixedConfigs[ruleName]; 110 | if (mixedConfig) { 111 | for (const configName of Object.keys(mixedConfig).sort()) { 112 | innerItems.push(`${format.italic(configName)}:\n${format.json(mixedConfig[configName])}`); 113 | } 114 | } 115 | items.push(format.hyperlink(format.bold(ruleName), (ruleDocs[ruleName] || [])[0], { fallback: false }) + '\n' + format.list(innerItems).trimEnd()); 116 | } 117 | console.log(format.header('Mixed configs where otherwise okay:')); 118 | console.log(format.list(items)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/print-summary.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { 4 | MarkdownOrChalk, 5 | mdastLinkify, 6 | mdastListHelper, 7 | mdastTableHelper, 8 | } from 'markdown-or-chalk'; 9 | 10 | import { formatJsonForMarkdown, formatJsonForMdast } from './format-json.js'; 11 | import { sortRulesByName } from './utils/misc.js'; 12 | 13 | /** 14 | * @param {string} configName 15 | * @param {{ [ruleName: string]: import('./compare.js').RuleSummary }} summary 16 | * @param {import('./print-result.js').PrintOptions} options 17 | * @returns {void} 18 | */ 19 | export function printConfigSummary (configName, summary, options) { 20 | const format = new MarkdownOrChalk(options.markdown); 21 | 22 | console.log(format.header(configName)); 23 | 24 | console.log(format.fromMdast(formatConfigSummary(configName, summary, options))); 25 | } 26 | 27 | /** 28 | * @param {string} _configName 29 | * @param {{ [ruleName: string]: import('./compare.js').RuleSummary }} summary 30 | * @param {import('./print-result.js').PrintOptions} options 31 | * @returns {import('mdast').Content} 32 | */ 33 | function formatConfigSummary (_configName, summary, options) { 34 | const sortedRules = Object.entries(sortRulesByName(summary)); 35 | 36 | return options.table 37 | ? formatConfigSummaryTable(sortedRules, options) 38 | : formatConfigSummaryList(sortedRules, options); 39 | } 40 | 41 | /** 42 | * @param {Array<[string, import('./compare.js').RuleSummary]>} sortedRules 43 | * @param {import('./print-result.js').PrintOptions} options 44 | * @returns {import('mdast').List} 45 | */ 46 | function formatConfigSummaryList (sortedRules, { markdown, skipLinks, verboseConfigs }) { 47 | let longestRuleNameWithOption = 0; 48 | for (const [ruleName, { options }] of sortedRules) { 49 | if (options && ruleName.length > longestRuleNameWithOption) { 50 | longestRuleNameWithOption = ruleName.length; 51 | } 52 | } 53 | 54 | const format = new MarkdownOrChalk(markdown); 55 | 56 | /** @type {import('markdown-or-chalk').PhrasingContentOrStringList[]} */ 57 | const formattedRules = []; 58 | 59 | for (const [ 60 | ruleName, 61 | { docUrls, error, options }, 62 | ] of sortedRules) { 63 | const docUrl = [...(docUrls || [])][0]; 64 | const optionsValue = options && Object.values(options)?.[0]; 65 | 66 | const formattedOptions = (verboseConfigs && optionsValue) ? formatJsonForMdast(optionsValue, verboseConfigs) : undefined; 67 | 68 | formattedRules.push([ 69 | format.logSymbolsMdast[error?.length ? 'error' : 'warning'], 70 | (options ? (format.chalkOnly ? format.logSymbolsMdast.info : ':wrench:') : ''), 71 | ' ', 72 | mdastLinkify(ruleName, docUrl, skipLinks), 73 | (verboseConfigs && options && format.chalkOnly ? ''.padEnd(longestRuleNameWithOption - ruleName.length + 2, ' ') : ''), 74 | formattedOptions ? ' ' : '', 75 | formattedOptions ?? '', 76 | ]); 77 | } 78 | 79 | return mdastListHelper(formattedRules); 80 | } 81 | 82 | /** @typedef {import('markdown-or-chalk').PhrasingContentOrStringList} PhrasingContentOrStringList */ 83 | 84 | /** 85 | * @param {Array<[string, import('./compare.js').RuleSummary]>} sortedRules 86 | * @param {import('./print-result.js').PrintOptions} options 87 | * @returns {import('mdast').Table} 88 | */ 89 | function formatConfigSummaryTable (sortedRules, { markdown, skipLinks, verboseConfigs }) { 90 | // TODO: Add ability for ruleset to add comments / justification for including rule 91 | 92 | /** @type {Array<[PhrasingContentOrStringList, string, string, PhrasingContentOrStringList]>} */ 93 | const tableData = [ 94 | ['Rule', 'Error', 'Warning', 'Config'], 95 | ]; 96 | 97 | for (const [ 98 | ruleName, 99 | { docUrls, error, options: config }, 100 | ] of sortedRules) { 101 | const docUrl = [...(docUrls || [])][0]; 102 | const optionsValue = config && Object.values(config)?.[0]; 103 | 104 | tableData.push([ 105 | mdastLinkify(ruleName, docUrl, skipLinks), 106 | error?.length ? 'Yes' : '', 107 | error?.length ? '' : 'Yes', 108 | optionsValue ? formatJsonForMarkdown(optionsValue, 'See config', verboseConfigs) : '', 109 | ]); 110 | } 111 | 112 | return mdastTableHelper( 113 | tableData, 114 | markdown ? ['left', 'center', 'center', 'left'] : undefined 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /lib/utils/diff.js: -------------------------------------------------------------------------------- 1 | import { filter, typesafeIsArray } from '@voxpelli/typed-utils'; 2 | import equal from 'fast-deep-equal'; 3 | import isPlainObject from 'is-plain-obj'; 4 | 5 | /** 6 | * @param {unknown} value 7 | * @returns {value is Record} 8 | */ 9 | function improvedIsPlainObject (value) { 10 | return isPlainObject(value); 11 | } 12 | 13 | /** 14 | * @template ValueA 15 | * @template {ValueA | import('../advanced-types.js').DeepPartial} ValueB 16 | * @param {ValueA | undefined} valueA 17 | * @param {ValueB | undefined} valueB 18 | * @param {number} depth 19 | * @returns {ValueB | import('../advanced-types.js').DeepPartial | undefined} 20 | */ 21 | function innerGetDeepDifference (valueA, valueB, depth) { 22 | if (valueA === valueB || equal(valueA, valueB)) { 23 | return; 24 | } 25 | 26 | if (depth > 20) { 27 | throw new Error('Too deep difference calculation'); 28 | } 29 | 30 | if (typesafeIsArray(valueB)) { 31 | if (!typesafeIsArray(valueA)) { 32 | return valueB; 33 | } 34 | 35 | const result = filter( 36 | valueB.map((value, i) => innerGetDeepDifference(valueA[i], value, depth + 1)) 37 | ); 38 | 39 | if (result.length === 0) { 40 | return; 41 | } 42 | 43 | return /** @type {import('../advanced-types.js').DeepPartial} */ (result); 44 | } 45 | 46 | if (improvedIsPlainObject(valueB)) { 47 | if (!improvedIsPlainObject(valueA)) { 48 | return valueB; 49 | } 50 | 51 | const difference = Object.entries(valueB) 52 | .map( 53 | /** 54 | * @param {[PropertyKey, unknown]} value 55 | * @returns {[PropertyKey, unknown]|undefined} 56 | */ 57 | ([key, value]) => { 58 | const valueFromValueA = valueA[key]; 59 | 60 | const innerDifference = innerGetDeepDifference(valueFromValueA, value, depth + 1); 61 | 62 | return innerDifference ? [key, innerDifference] : undefined; 63 | } 64 | ); 65 | 66 | const filteredDifference = filter(difference); 67 | 68 | const result = Object.fromEntries(filteredDifference); 69 | 70 | if (Object.keys(result).length === 0) { 71 | return; 72 | } 73 | 74 | return /** @type {import('../advanced-types.js').DeepPartial>} */ (result); 75 | } 76 | 77 | return valueB; 78 | } 79 | 80 | /** 81 | * @template ValueA 82 | * @template {ValueA | import('../advanced-types.js').DeepPartial} ValueB 83 | * @param {ValueA | undefined} valueA 84 | * @param {ValueB | undefined} valueB 85 | * @returns {ValueB | import('../advanced-types.js').DeepPartial | undefined} 86 | */ 87 | export function getDeepDifference (valueA, valueB) { 88 | return innerGetDeepDifference(valueA, valueB, 0); 89 | } 90 | -------------------------------------------------------------------------------- /lib/utils/errors.js: -------------------------------------------------------------------------------- 1 | export class InputError extends Error { 2 | /** @override */ 3 | name = 'InputError'; 4 | 5 | /** 6 | * @param {string} message 7 | * @param {string} [body] 8 | */ 9 | constructor (message, body) { 10 | super(message); 11 | 12 | /** @type {string|undefined} */ 13 | this.body = body; 14 | } 15 | } 16 | 17 | export class ResultError extends Error { 18 | /** @override */ 19 | name = 'ResultError'; 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template {keyof any} Keys 3 | * @template Values 4 | * @param {Keys[]} keys 5 | * @param {Values[]} values 6 | * @returns {Record} 7 | */ 8 | export function zipObject (keys, values) { 9 | /** @type {Partial>} */ 10 | const result = {}; 11 | 12 | for (const [i, key] of keys.entries()) { 13 | result[key] = values[i]; 14 | } 15 | 16 | return /** @type {Record} */ (result); 17 | } 18 | 19 | /** 20 | * @template T 21 | * @param {{ [configName: string]: T }} ruleCollection 22 | * @returns {{ [configName: string]: T }} 23 | */ 24 | export function sortRulesByName (ruleCollection) { 25 | return Object.fromEntries(Object.entries(ruleCollection).sort(([ruleNameA], [ruleNameB]) => { 26 | const [ruleOrPluginA, pluginRuleA] = ruleNameA.split('/'); 27 | const [ruleOrPluginB, pluginRuleB] = ruleNameB.split('/'); 28 | 29 | if (pluginRuleA && pluginRuleB) { 30 | if (ruleOrPluginA === ruleOrPluginB) { 31 | return pluginRuleA < pluginRuleB ? -1 : 1; 32 | } 33 | return (ruleOrPluginA || '') < (ruleOrPluginB || '') ? -1 : 1; 34 | } 35 | 36 | if (pluginRuleA || pluginRuleB) { 37 | return pluginRuleA ? 1 : -1; 38 | } 39 | 40 | return (ruleOrPluginA || '') < (ruleOrPluginB || '') ? -1 : 1; 41 | })); 42 | } 43 | -------------------------------------------------------------------------------- /new.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.eslintrc", 3 | "root": true, 4 | "rules": { 5 | "for-direction": "error", 6 | "func-style": ["error", "expression", { "allowArrowFunctions": true }], 7 | "no-console": ["warn", { "allow": ["warn", "error"] }], 8 | "no-multi-spaces": ["error", {}], 9 | "jsdoc/require-returns": ["warn", { "publicOnly": true }], 10 | "unicorn/prefer-event-target": ["error"], 11 | "unicorn/catch-error-name": ["error", { "name": "foo", "ignore": ["^cause$"] }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compare-eslint-configs", 3 | "version": "2.1.0", 4 | "description": "Compares ESLint configs", 5 | "homepage": "http://github.com/voxpelli/compare-eslint-configs", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/voxpelli/compare-eslint-configs.git" 9 | }, 10 | "keywords": [], 11 | "author": "Pelle Wessman (http://kodfabrik.se/)", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.6.0" 15 | }, 16 | "type": "module", 17 | "bin": { 18 | "compare-eslint-configs": "cli.js" 19 | }, 20 | "files": [ 21 | "cli.js", 22 | "lib/**/*.cjs", 23 | "lib/**/*.js" 24 | ], 25 | "scripts": { 26 | "build:0": "run-s clean", 27 | "build:1-declaration": "tsc -p declaration.tsconfig.json", 28 | "build": "run-s build:*", 29 | "check:installed-check": "installed-check -i eslint-plugin-jsdoc -i knip", 30 | "check:knip": "knip", 31 | "check:lint": "eslint --report-unused-disable-directives .", 32 | "check:tsc": "tsc", 33 | "check:type-coverage": "type-coverage --detail --strict --at-least 95 --ignore-files 'test/*'", 34 | "check": "run-s clean && run-p check:*", 35 | "clean:declarations-top": "rm -rf $(find . -maxdepth 1 -type f -name '*.d.*ts*')", 36 | "clean:declarations-lib": "rm -rf $(find lib -type f -name '*.d.*ts*' ! -name '*-types.d.ts')", 37 | "clean": "run-p clean:*", 38 | "prepare": "husky install", 39 | "prepublishOnly": "run-s build", 40 | "test:mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js'", 41 | "test-ci": "run-s test:*", 42 | "test": "run-s check test:*", 43 | "try": "node cli.js new.eslintrc -t cli.js", 44 | "try-table": "node cli.js new.eslintrc -t cli.js -m -s --table" 45 | }, 46 | "devDependencies": { 47 | "@types/chai": "^4.3.16", 48 | "@types/eslint": "^8.44.9", 49 | "@types/mdast": "^4.0.3", 50 | "@types/mocha": "^10.0.7", 51 | "@types/node": "^18.19.50", 52 | "@voxpelli/eslint-config": "^19.0.0", 53 | "@voxpelli/tsconfig": "^14.0.0", 54 | "c8": "^8.0.1", 55 | "chai": "^4.4.1", 56 | "eslint-plugin-es-x": "^7.5.0", 57 | "eslint-plugin-import": "^2.29.1", 58 | "eslint-plugin-jsdoc": "^46.9.1", 59 | "eslint-plugin-mocha": "^10.2.0", 60 | "eslint-plugin-n": "^16.4.0", 61 | "eslint-plugin-promise": "^6.1.1", 62 | "eslint-plugin-security": "^1.7.1", 63 | "eslint-plugin-sort-destructure-keys": "^1.5.0", 64 | "eslint-plugin-unicorn": "^48.0.1", 65 | "husky": "^8.0.3", 66 | "installed-check": "^8.0.1", 67 | "knip": "^3.8.1", 68 | "mocha": "^10.5.1", 69 | "npm-run-all2": "^6.2.0", 70 | "type-coverage": "^2.29.1", 71 | "typescript": "~5.5.4" 72 | }, 73 | "dependencies": { 74 | "@voxpelli/typed-utils": "^1.10.2", 75 | "ajv": "^6.12.6", 76 | "chalk": "^5.2.0", 77 | "eslint": "^8.55.0", 78 | "fast-deep-equal": "^3.1.3", 79 | "is-plain-obj": "^4.1.0", 80 | "markdown-or-chalk": "^0.2.0", 81 | "peowly": "^1.3.2", 82 | "peowly-commands": "^1.1.0", 83 | "pony-cause": "^2.1.8" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>voxpelli/renovate-config" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/compare.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import { compareConfigs } from '../lib/compare.js'; 4 | 5 | chai.should(); 6 | 7 | describe('compare', () => { 8 | it('should work', async () => { 9 | // TODO: Make it sensible 10 | const differences = await compareConfigs({}); 11 | 12 | differences.should.deep.equal({ 13 | 'mixedConfigs': {}, 14 | 'mixedSeverity': {}, 15 | 'onlyActiveIn': {}, 16 | 'ruleDocs': {}, 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/data/voxpelli-eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2022": true, 4 | "mocha": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "globals": { 9 | "ArrayBuffer": "readonly", 10 | "Atomics": "readonly", 11 | "BigInt": "readonly", 12 | "BigInt64Array": "readonly", 13 | "BigUint64Array": "readonly", 14 | "DataView": "readonly", 15 | "Float32Array": "readonly", 16 | "Float64Array": "readonly", 17 | "Int16Array": "readonly", 18 | "Int32Array": "readonly", 19 | "Int8Array": "readonly", 20 | "Map": "readonly", 21 | "Promise": "readonly", 22 | "Proxy": "readonly", 23 | "Reflect": "readonly", 24 | "Set": "readonly", 25 | "SharedArrayBuffer": "readonly", 26 | "Symbol": "readonly", 27 | "Uint16Array": "readonly", 28 | "Uint32Array": "readonly", 29 | "Uint8Array": "readonly", 30 | "Uint8ClampedArray": "readonly", 31 | "WeakMap": "readonly", 32 | "WeakSet": "readonly", 33 | "globalThis": "readonly", 34 | "Intl": "readonly", 35 | "TextDecoder": "readonly", 36 | "TextEncoder": "readonly", 37 | "URL": "readonly", 38 | "URLSearchParams": "readonly", 39 | "WebAssembly": "readonly", 40 | "clearInterval": "readonly", 41 | "clearTimeout": "readonly", 42 | "console": "readonly", 43 | "queueMicrotask": "readonly", 44 | "setInterval": "readonly", 45 | "setTimeout": "readonly", 46 | "Buffer": "readonly", 47 | "GLOBAL": "readonly", 48 | "clearImmediate": "readonly", 49 | "global": "readonly", 50 | "process": "readonly", 51 | "root": "readonly", 52 | "setImmediate": "readonly", 53 | "__dirname": "off", 54 | "__filename": "off", 55 | "exports": "off", 56 | "module": "off", 57 | "require": "off", 58 | "document": "readonly", 59 | "navigator": "readonly", 60 | "window": "readonly" 61 | }, 62 | "parser": null, 63 | "parserOptions": { 64 | "ecmaVersion": "latest", 65 | "sourceType": "module", 66 | "ecmaFeatures": { 67 | "globalReturn": false, 68 | "jsx": true 69 | } 70 | }, 71 | "plugins": [ 72 | "n", 73 | "unicorn", 74 | "sort-destructure-keys", 75 | "security", 76 | "promise", 77 | "mocha", 78 | "jsdoc", 79 | "import", 80 | "es" 81 | ], 82 | "rules": { 83 | "func-style": [ 84 | "warn", 85 | "declaration", 86 | { 87 | "allowArrowFunctions": true 88 | } 89 | ], 90 | "semi": [ 91 | "error", 92 | "always" 93 | ], 94 | "no-extra-semi": [ 95 | "error" 96 | ], 97 | "comma-dangle": [ 98 | "warn", 99 | { 100 | "arrays": "ignore", 101 | "objects": "ignore", 102 | "imports": "ignore", 103 | "exports": "ignore", 104 | "functions": "never" 105 | } 106 | ], 107 | "dot-notation": [ 108 | "off", 109 | { 110 | "allowKeywords": true, 111 | "allowPattern": "" 112 | } 113 | ], 114 | "no-multi-spaces": [ 115 | "error", 116 | { 117 | "ignoreEOLComments": true 118 | } 119 | ], 120 | "no-unused-vars": [ 121 | "error", 122 | { 123 | "vars": "all", 124 | "args": "all", 125 | "argsIgnorePattern": "^_", 126 | "ignoreRestSiblings": true 127 | } 128 | ], 129 | "n/no-deprecated-api": [ 130 | "warn" 131 | ], 132 | "no-console": [ 133 | "warn" 134 | ], 135 | "no-constant-binary-expression": [ 136 | "error" 137 | ], 138 | "no-unsafe-optional-chaining": [ 139 | "error", 140 | { 141 | "disallowArithmeticOperators": true 142 | } 143 | ], 144 | "no-warning-comments": [ 145 | "warn", 146 | { 147 | "terms": [ 148 | "fixme" 149 | ] 150 | } 151 | ], 152 | "object-shorthand": [ 153 | "error", 154 | "properties", 155 | { 156 | "avoidQuotes": true 157 | } 158 | ], 159 | "quote-props": [ 160 | "error", 161 | "as-needed", 162 | { 163 | "keywords": true, 164 | "numbers": true, 165 | "unnecessary": false 166 | } 167 | ], 168 | "jsdoc/check-types": [ 169 | "off" 170 | ], 171 | "jsdoc/no-undefined-types": [ 172 | "off" 173 | ], 174 | "jsdoc/require-jsdoc": [ 175 | "off" 176 | ], 177 | "jsdoc/require-param-description": [ 178 | "off" 179 | ], 180 | "jsdoc/require-property-description": [ 181 | "off" 182 | ], 183 | "jsdoc/require-returns-description": [ 184 | "off" 185 | ], 186 | "jsdoc/require-yields": [ 187 | "off" 188 | ], 189 | "jsdoc/valid-types": [ 190 | "off" 191 | ], 192 | "mocha/no-mocha-arrows": [ 193 | "off" 194 | ], 195 | "n/no-process-exit": [ 196 | "off" 197 | ], 198 | "security/detect-object-injection": [ 199 | "off" 200 | ], 201 | "security/detect-unsafe-regex": [ 202 | "off" 203 | ], 204 | "unicorn/catch-error-name": [ 205 | "error", 206 | { 207 | "name": "err", 208 | "ignore": [ 209 | "^cause$" 210 | ] 211 | } 212 | ], 213 | "unicorn/explicit-length-check": [ 214 | "off" 215 | ], 216 | "unicorn/no-await-expression-member": [ 217 | "warn" 218 | ], 219 | "unicorn/numeric-separators-style": [ 220 | "off" 221 | ], 222 | "unicorn/prefer-add-event-listener": [ 223 | "warn" 224 | ], 225 | "unicorn/prefer-event-target": [ 226 | "warn" 227 | ], 228 | "unicorn/prefer-module": [ 229 | "off" 230 | ], 231 | "unicorn/prefer-spread": [ 232 | "warn" 233 | ], 234 | "unicorn/prevent-abbreviations": [ 235 | "off" 236 | ], 237 | "es/no-exponential-operators": [ 238 | "warn" 239 | ], 240 | "n/prefer-global/console": [ 241 | "warn" 242 | ], 243 | "n/prefer-promises/fs": [ 244 | "warn" 245 | ], 246 | "n/no-process-env": [ 247 | "warn" 248 | ], 249 | "n/no-sync": [ 250 | "error" 251 | ], 252 | "promise/prefer-await-to-then": [ 253 | "error" 254 | ], 255 | "sort-destructure-keys/sort-destructure-keys": [ 256 | "error" 257 | ], 258 | "jsdoc/check-access": [ 259 | "warn" 260 | ], 261 | "jsdoc/check-alignment": [ 262 | "warn" 263 | ], 264 | "jsdoc/check-examples": [ 265 | "off" 266 | ], 267 | "jsdoc/check-indentation": [ 268 | "off" 269 | ], 270 | "jsdoc/check-line-alignment": [ 271 | "off" 272 | ], 273 | "jsdoc/check-param-names": [ 274 | "warn" 275 | ], 276 | "jsdoc/check-property-names": [ 277 | "warn" 278 | ], 279 | "jsdoc/check-syntax": [ 280 | "off" 281 | ], 282 | "jsdoc/check-tag-names": [ 283 | "warn" 284 | ], 285 | "jsdoc/check-values": [ 286 | "warn" 287 | ], 288 | "jsdoc/empty-tags": [ 289 | "warn" 290 | ], 291 | "jsdoc/implements-on-classes": [ 292 | "warn" 293 | ], 294 | "jsdoc/match-description": [ 295 | "off" 296 | ], 297 | "jsdoc/match-name": [ 298 | "off" 299 | ], 300 | "jsdoc/multiline-blocks": [ 301 | "warn" 302 | ], 303 | "jsdoc/newline-after-description": [ 304 | "warn" 305 | ], 306 | "jsdoc/no-bad-blocks": [ 307 | "off" 308 | ], 309 | "jsdoc/no-defaults": [ 310 | "off" 311 | ], 312 | "jsdoc/no-missing-syntax": [ 313 | "off" 314 | ], 315 | "jsdoc/no-multi-asterisks": [ 316 | "warn" 317 | ], 318 | "jsdoc/no-restricted-syntax": [ 319 | "off" 320 | ], 321 | "jsdoc/no-types": [ 322 | "off" 323 | ], 324 | "jsdoc/require-asterisk-prefix": [ 325 | "off" 326 | ], 327 | "jsdoc/require-description": [ 328 | "off" 329 | ], 330 | "jsdoc/require-description-complete-sentence": [ 331 | "off" 332 | ], 333 | "jsdoc/require-example": [ 334 | "off" 335 | ], 336 | "jsdoc/require-file-overview": [ 337 | "off" 338 | ], 339 | "jsdoc/require-hyphen-before-param-description": [ 340 | "off" 341 | ], 342 | "jsdoc/require-param": [ 343 | "warn" 344 | ], 345 | "jsdoc/require-param-name": [ 346 | "warn" 347 | ], 348 | "jsdoc/require-param-type": [ 349 | "warn" 350 | ], 351 | "jsdoc/require-property": [ 352 | "warn" 353 | ], 354 | "jsdoc/require-property-name": [ 355 | "warn" 356 | ], 357 | "jsdoc/require-property-type": [ 358 | "warn" 359 | ], 360 | "jsdoc/require-returns": [ 361 | "warn" 362 | ], 363 | "jsdoc/require-returns-check": [ 364 | "warn" 365 | ], 366 | "jsdoc/require-returns-type": [ 367 | "warn" 368 | ], 369 | "jsdoc/require-throws": [ 370 | "off" 371 | ], 372 | "jsdoc/require-yields-check": [ 373 | "warn" 374 | ], 375 | "jsdoc/sort-tags": [ 376 | "off" 377 | ], 378 | "jsdoc/tag-lines": [ 379 | "warn" 380 | ], 381 | "promise/always-return": [ 382 | "error" 383 | ], 384 | "promise/no-return-wrap": [ 385 | "error" 386 | ], 387 | "promise/param-names": [ 388 | "error" 389 | ], 390 | "promise/catch-or-return": [ 391 | "error" 392 | ], 393 | "promise/no-native": [ 394 | "off" 395 | ], 396 | "promise/no-nesting": [ 397 | "warn" 398 | ], 399 | "promise/no-promise-in-callback": [ 400 | "warn" 401 | ], 402 | "promise/no-callback-in-promise": [ 403 | "warn" 404 | ], 405 | "promise/avoid-new": [ 406 | "off" 407 | ], 408 | "promise/no-new-statics": [ 409 | "error" 410 | ], 411 | "promise/no-return-in-finally": [ 412 | "warn" 413 | ], 414 | "promise/valid-params": [ 415 | "warn" 416 | ], 417 | "unicorn/better-regex": [ 418 | "error" 419 | ], 420 | "unicorn/consistent-destructuring": [ 421 | "error" 422 | ], 423 | "unicorn/consistent-function-scoping": [ 424 | "error" 425 | ], 426 | "unicorn/custom-error-definition": [ 427 | "off" 428 | ], 429 | "unicorn/empty-brace-spaces": [ 430 | "error" 431 | ], 432 | "unicorn/error-message": [ 433 | "error" 434 | ], 435 | "unicorn/escape-case": [ 436 | "error" 437 | ], 438 | "unicorn/expiring-todo-comments": [ 439 | "error" 440 | ], 441 | "unicorn/filename-case": [ 442 | "error" 443 | ], 444 | "unicorn/import-style": [ 445 | "error" 446 | ], 447 | "unicorn/new-for-builtins": [ 448 | "error" 449 | ], 450 | "unicorn/no-abusive-eslint-disable": [ 451 | "error" 452 | ], 453 | "unicorn/no-array-callback-reference": [ 454 | "error" 455 | ], 456 | "unicorn/no-array-for-each": [ 457 | "error" 458 | ], 459 | "unicorn/no-array-method-this-argument": [ 460 | "error" 461 | ], 462 | "unicorn/no-array-push-push": [ 463 | "error" 464 | ], 465 | "unicorn/no-array-reduce": [ 466 | "error" 467 | ], 468 | "unicorn/no-console-spaces": [ 469 | "error" 470 | ], 471 | "unicorn/no-document-cookie": [ 472 | "error" 473 | ], 474 | "unicorn/no-empty-file": [ 475 | "error" 476 | ], 477 | "unicorn/no-for-loop": [ 478 | "error" 479 | ], 480 | "unicorn/no-hex-escape": [ 481 | "error" 482 | ], 483 | "unicorn/no-instanceof-array": [ 484 | "error" 485 | ], 486 | "unicorn/no-invalid-remove-event-listener": [ 487 | "error" 488 | ], 489 | "unicorn/no-keyword-prefix": [ 490 | "off" 491 | ], 492 | "unicorn/no-lonely-if": [ 493 | "error" 494 | ], 495 | "no-nested-ternary": [ 496 | "off" 497 | ], 498 | "unicorn/no-nested-ternary": [ 499 | "error" 500 | ], 501 | "unicorn/no-new-array": [ 502 | "error" 503 | ], 504 | "unicorn/no-new-buffer": [ 505 | "error" 506 | ], 507 | "unicorn/no-null": [ 508 | "error" 509 | ], 510 | "unicorn/no-object-as-default-parameter": [ 511 | "error" 512 | ], 513 | "unicorn/no-process-exit": [ 514 | "error" 515 | ], 516 | "unicorn/no-static-only-class": [ 517 | "error" 518 | ], 519 | "unicorn/no-thenable": [ 520 | "error" 521 | ], 522 | "unicorn/no-this-assignment": [ 523 | "error" 524 | ], 525 | "unicorn/no-unreadable-array-destructuring": [ 526 | "error" 527 | ], 528 | "unicorn/no-unreadable-iife": [ 529 | "error" 530 | ], 531 | "unicorn/no-unsafe-regex": [ 532 | "off" 533 | ], 534 | "unicorn/no-unused-properties": [ 535 | "off" 536 | ], 537 | "unicorn/no-useless-fallback-in-spread": [ 538 | "error" 539 | ], 540 | "unicorn/no-useless-length-check": [ 541 | "error" 542 | ], 543 | "unicorn/no-useless-promise-resolve-reject": [ 544 | "error" 545 | ], 546 | "unicorn/no-useless-spread": [ 547 | "error" 548 | ], 549 | "unicorn/no-useless-switch-case": [ 550 | "error" 551 | ], 552 | "unicorn/no-useless-undefined": [ 553 | "error" 554 | ], 555 | "unicorn/no-zero-fractions": [ 556 | "error" 557 | ], 558 | "unicorn/number-literal-case": [ 559 | "error" 560 | ], 561 | "unicorn/prefer-array-find": [ 562 | "error" 563 | ], 564 | "unicorn/prefer-array-flat": [ 565 | "error" 566 | ], 567 | "unicorn/prefer-array-flat-map": [ 568 | "error" 569 | ], 570 | "unicorn/prefer-array-index-of": [ 571 | "error" 572 | ], 573 | "unicorn/prefer-array-some": [ 574 | "error" 575 | ], 576 | "unicorn/prefer-at": [ 577 | "off" 578 | ], 579 | "unicorn/prefer-code-point": [ 580 | "error" 581 | ], 582 | "unicorn/prefer-date-now": [ 583 | "error" 584 | ], 585 | "unicorn/prefer-default-parameters": [ 586 | "error" 587 | ], 588 | "unicorn/prefer-dom-node-append": [ 589 | "error" 590 | ], 591 | "unicorn/prefer-dom-node-dataset": [ 592 | "error" 593 | ], 594 | "unicorn/prefer-dom-node-remove": [ 595 | "error" 596 | ], 597 | "unicorn/prefer-dom-node-text-content": [ 598 | "error" 599 | ], 600 | "unicorn/prefer-export-from": [ 601 | "error" 602 | ], 603 | "unicorn/prefer-includes": [ 604 | "error" 605 | ], 606 | "unicorn/prefer-json-parse-buffer": [ 607 | "off" 608 | ], 609 | "unicorn/prefer-keyboard-event-key": [ 610 | "error" 611 | ], 612 | "unicorn/prefer-logical-operator-over-ternary": [ 613 | "error" 614 | ], 615 | "unicorn/prefer-math-trunc": [ 616 | "error" 617 | ], 618 | "unicorn/prefer-modern-dom-apis": [ 619 | "error" 620 | ], 621 | "unicorn/prefer-modern-math-apis": [ 622 | "error" 623 | ], 624 | "unicorn/prefer-native-coercion-functions": [ 625 | "error" 626 | ], 627 | "unicorn/prefer-negative-index": [ 628 | "error" 629 | ], 630 | "unicorn/prefer-node-protocol": [ 631 | "error" 632 | ], 633 | "unicorn/prefer-number-properties": [ 634 | "error" 635 | ], 636 | "unicorn/prefer-object-from-entries": [ 637 | "error" 638 | ], 639 | "unicorn/prefer-optional-catch-binding": [ 640 | "error" 641 | ], 642 | "unicorn/prefer-prototype-methods": [ 643 | "error" 644 | ], 645 | "unicorn/prefer-query-selector": [ 646 | "error" 647 | ], 648 | "unicorn/prefer-reflect-apply": [ 649 | "error" 650 | ], 651 | "unicorn/prefer-regexp-test": [ 652 | "error" 653 | ], 654 | "unicorn/prefer-set-has": [ 655 | "error" 656 | ], 657 | "unicorn/prefer-string-replace-all": [ 658 | "off" 659 | ], 660 | "unicorn/prefer-string-slice": [ 661 | "error" 662 | ], 663 | "unicorn/prefer-string-starts-ends-with": [ 664 | "error" 665 | ], 666 | "unicorn/prefer-string-trim-start-end": [ 667 | "error" 668 | ], 669 | "unicorn/prefer-switch": [ 670 | "error" 671 | ], 672 | "unicorn/prefer-ternary": [ 673 | "error" 674 | ], 675 | "unicorn/prefer-top-level-await": [ 676 | "error" 677 | ], 678 | "unicorn/prefer-type-error": [ 679 | "error" 680 | ], 681 | "unicorn/relative-url-style": [ 682 | "error" 683 | ], 684 | "unicorn/require-array-join-separator": [ 685 | "error" 686 | ], 687 | "unicorn/require-number-to-fixed-digits-argument": [ 688 | "error" 689 | ], 690 | "unicorn/require-post-message-target-origin": [ 691 | "off" 692 | ], 693 | "unicorn/string-content": [ 694 | "off" 695 | ], 696 | "unicorn/template-indent": [ 697 | "error" 698 | ], 699 | "unicorn/text-encoding-identifier-case": [ 700 | "error" 701 | ], 702 | "unicorn/throw-new-error": [ 703 | "error" 704 | ], 705 | "mocha/handle-done-callback": [ 706 | "error" 707 | ], 708 | "mocha/max-top-level-suites": [ 709 | "error", 710 | { 711 | "limit": 1 712 | } 713 | ], 714 | "mocha/no-async-describe": [ 715 | "error" 716 | ], 717 | "mocha/no-exclusive-tests": [ 718 | "warn" 719 | ], 720 | "mocha/no-exports": [ 721 | "error" 722 | ], 723 | "mocha/no-global-tests": [ 724 | "error" 725 | ], 726 | "mocha/no-hooks": [ 727 | "off" 728 | ], 729 | "mocha/no-hooks-for-single-case": [ 730 | "off" 731 | ], 732 | "mocha/no-identical-title": [ 733 | "error" 734 | ], 735 | "mocha/no-nested-tests": [ 736 | "error" 737 | ], 738 | "mocha/no-pending-tests": [ 739 | "warn" 740 | ], 741 | "mocha/no-return-and-callback": [ 742 | "error" 743 | ], 744 | "mocha/no-return-from-async": [ 745 | "off" 746 | ], 747 | "mocha/no-setup-in-describe": [ 748 | "error" 749 | ], 750 | "mocha/no-sibling-hooks": [ 751 | "error" 752 | ], 753 | "mocha/no-skipped-tests": [ 754 | "warn" 755 | ], 756 | "mocha/no-synchronous-tests": [ 757 | "off" 758 | ], 759 | "mocha/no-top-level-hooks": [ 760 | "warn" 761 | ], 762 | "mocha/prefer-arrow-callback": [ 763 | "off" 764 | ], 765 | "mocha/valid-suite-description": [ 766 | "off" 767 | ], 768 | "mocha/valid-test-description": [ 769 | "off" 770 | ], 771 | "mocha/no-empty-description": [ 772 | "error" 773 | ], 774 | "security/detect-buffer-noassert": [ 775 | "warn" 776 | ], 777 | "security/detect-child-process": [ 778 | "warn" 779 | ], 780 | "security/detect-disable-mustache-escape": [ 781 | "warn" 782 | ], 783 | "security/detect-eval-with-expression": [ 784 | "warn" 785 | ], 786 | "security/detect-new-buffer": [ 787 | "warn" 788 | ], 789 | "security/detect-no-csrf-before-method-override": [ 790 | "warn" 791 | ], 792 | "security/detect-non-literal-fs-filename": [ 793 | "warn" 794 | ], 795 | "security/detect-non-literal-regexp": [ 796 | "warn" 797 | ], 798 | "security/detect-non-literal-require": [ 799 | "warn" 800 | ], 801 | "security/detect-possible-timing-attacks": [ 802 | "warn" 803 | ], 804 | "security/detect-pseudoRandomBytes": [ 805 | "warn" 806 | ], 807 | "n/no-extraneous-import": [ 808 | "error" 809 | ], 810 | "n/no-extraneous-require": [ 811 | "error" 812 | ], 813 | "n/no-exports-assign": [ 814 | "error" 815 | ], 816 | "n/no-missing-import": [ 817 | "error" 818 | ], 819 | "n/no-missing-require": [ 820 | "error" 821 | ], 822 | "n/no-unpublished-bin": [ 823 | "error" 824 | ], 825 | "n/no-unpublished-import": [ 826 | "error" 827 | ], 828 | "n/no-unpublished-require": [ 829 | "error" 830 | ], 831 | "n/no-unsupported-features/es-builtins": [ 832 | "error" 833 | ], 834 | "n/no-unsupported-features/es-syntax": [ 835 | "error", 836 | { 837 | "ignores": [ 838 | "modules" 839 | ] 840 | } 841 | ], 842 | "n/no-unsupported-features/node-builtins": [ 843 | "error" 844 | ], 845 | "n/process-exit-as-throw": [ 846 | "error" 847 | ], 848 | "n/shebang": [ 849 | "error" 850 | ], 851 | "no-var": [ 852 | "warn" 853 | ], 854 | "accessor-pairs": [ 855 | "error", 856 | { 857 | "setWithoutGet": true, 858 | "enforceForClassMembers": true, 859 | "getWithoutSet": false 860 | } 861 | ], 862 | "array-bracket-spacing": [ 863 | "error", 864 | "never" 865 | ], 866 | "array-callback-return": [ 867 | "error", 868 | { 869 | "allowImplicit": false, 870 | "checkForEach": false 871 | } 872 | ], 873 | "arrow-spacing": [ 874 | "error", 875 | { 876 | "before": true, 877 | "after": true 878 | } 879 | ], 880 | "block-spacing": [ 881 | "error", 882 | "always" 883 | ], 884 | "brace-style": [ 885 | "error", 886 | "1tbs", 887 | { 888 | "allowSingleLine": true 889 | } 890 | ], 891 | "camelcase": [ 892 | "error", 893 | { 894 | "allow": [ 895 | "^UNSAFE_" 896 | ], 897 | "properties": "never", 898 | "ignoreGlobals": true, 899 | "ignoreDestructuring": false, 900 | "ignoreImports": false 901 | } 902 | ], 903 | "comma-spacing": [ 904 | "error", 905 | { 906 | "before": false, 907 | "after": true 908 | } 909 | ], 910 | "comma-style": [ 911 | "error", 912 | "last" 913 | ], 914 | "computed-property-spacing": [ 915 | "error", 916 | "never", 917 | { 918 | "enforceForClassMembers": true 919 | } 920 | ], 921 | "constructor-super": [ 922 | "error" 923 | ], 924 | "curly": [ 925 | "error", 926 | "multi-line" 927 | ], 928 | "default-case-last": [ 929 | "error" 930 | ], 931 | "dot-location": [ 932 | "error", 933 | "property" 934 | ], 935 | "eol-last": [ 936 | "error" 937 | ], 938 | "eqeqeq": [ 939 | "error", 940 | "always", 941 | { 942 | "null": "ignore" 943 | } 944 | ], 945 | "func-call-spacing": [ 946 | "error", 947 | "never" 948 | ], 949 | "generator-star-spacing": [ 950 | "error", 951 | { 952 | "before": true, 953 | "after": true 954 | } 955 | ], 956 | "indent": [ 957 | "error", 958 | 2, 959 | { 960 | "SwitchCase": 1, 961 | "VariableDeclarator": 1, 962 | "outerIIFEBody": 1, 963 | "MemberExpression": 1, 964 | "FunctionDeclaration": { 965 | "parameters": 1, 966 | "body": 1 967 | }, 968 | "FunctionExpression": { 969 | "parameters": 1, 970 | "body": 1 971 | }, 972 | "CallExpression": { 973 | "arguments": 1 974 | }, 975 | "ArrayExpression": 1, 976 | "ObjectExpression": 1, 977 | "ImportDeclaration": 1, 978 | "flatTernaryExpressions": false, 979 | "ignoreComments": false, 980 | "ignoredNodes": [ 981 | "TemplateLiteral *", 982 | "JSXElement", 983 | "JSXElement > *", 984 | "JSXAttribute", 985 | "JSXIdentifier", 986 | "JSXNamespacedName", 987 | "JSXMemberExpression", 988 | "JSXSpreadAttribute", 989 | "JSXExpressionContainer", 990 | "JSXOpeningElement", 991 | "JSXClosingElement", 992 | "JSXFragment", 993 | "JSXOpeningFragment", 994 | "JSXClosingFragment", 995 | "JSXText", 996 | "JSXEmptyExpression", 997 | "JSXSpreadChild" 998 | ], 999 | "offsetTernaryExpressions": true 1000 | } 1001 | ], 1002 | "key-spacing": [ 1003 | "error", 1004 | { 1005 | "beforeColon": false, 1006 | "afterColon": true 1007 | } 1008 | ], 1009 | "keyword-spacing": [ 1010 | "error", 1011 | { 1012 | "before": true, 1013 | "after": true 1014 | } 1015 | ], 1016 | "lines-between-class-members": [ 1017 | "error", 1018 | "always", 1019 | { 1020 | "exceptAfterSingleLine": true 1021 | } 1022 | ], 1023 | "multiline-ternary": [ 1024 | "error", 1025 | "always-multiline" 1026 | ], 1027 | "new-cap": [ 1028 | "error", 1029 | { 1030 | "newIsCap": true, 1031 | "capIsNew": false, 1032 | "properties": true 1033 | } 1034 | ], 1035 | "new-parens": [ 1036 | "error" 1037 | ], 1038 | "no-array-constructor": [ 1039 | "error" 1040 | ], 1041 | "no-async-promise-executor": [ 1042 | "error" 1043 | ], 1044 | "no-caller": [ 1045 | "error" 1046 | ], 1047 | "no-case-declarations": [ 1048 | "error" 1049 | ], 1050 | "no-class-assign": [ 1051 | "error" 1052 | ], 1053 | "no-compare-neg-zero": [ 1054 | "error" 1055 | ], 1056 | "no-cond-assign": [ 1057 | "error" 1058 | ], 1059 | "no-const-assign": [ 1060 | "error" 1061 | ], 1062 | "no-constant-condition": [ 1063 | "error", 1064 | { 1065 | "checkLoops": false 1066 | } 1067 | ], 1068 | "no-control-regex": [ 1069 | "error" 1070 | ], 1071 | "no-debugger": [ 1072 | "error" 1073 | ], 1074 | "no-delete-var": [ 1075 | "error" 1076 | ], 1077 | "no-dupe-args": [ 1078 | "error" 1079 | ], 1080 | "no-dupe-class-members": [ 1081 | "error" 1082 | ], 1083 | "no-dupe-keys": [ 1084 | "error" 1085 | ], 1086 | "no-duplicate-case": [ 1087 | "error" 1088 | ], 1089 | "no-useless-backreference": [ 1090 | "error" 1091 | ], 1092 | "no-empty": [ 1093 | "error", 1094 | { 1095 | "allowEmptyCatch": true 1096 | } 1097 | ], 1098 | "no-empty-character-class": [ 1099 | "error" 1100 | ], 1101 | "no-empty-pattern": [ 1102 | "error" 1103 | ], 1104 | "no-eval": [ 1105 | "error" 1106 | ], 1107 | "no-ex-assign": [ 1108 | "error" 1109 | ], 1110 | "no-extend-native": [ 1111 | "error" 1112 | ], 1113 | "no-extra-bind": [ 1114 | "error" 1115 | ], 1116 | "no-extra-boolean-cast": [ 1117 | "error" 1118 | ], 1119 | "no-extra-parens": [ 1120 | "error", 1121 | "functions" 1122 | ], 1123 | "no-fallthrough": [ 1124 | "error" 1125 | ], 1126 | "no-floating-decimal": [ 1127 | "error" 1128 | ], 1129 | "no-func-assign": [ 1130 | "error" 1131 | ], 1132 | "no-global-assign": [ 1133 | "error" 1134 | ], 1135 | "no-implied-eval": [ 1136 | "error" 1137 | ], 1138 | "no-import-assign": [ 1139 | "error" 1140 | ], 1141 | "no-invalid-regexp": [ 1142 | "error" 1143 | ], 1144 | "no-irregular-whitespace": [ 1145 | "error" 1146 | ], 1147 | "no-iterator": [ 1148 | "error" 1149 | ], 1150 | "no-labels": [ 1151 | "error", 1152 | { 1153 | "allowLoop": false, 1154 | "allowSwitch": false 1155 | } 1156 | ], 1157 | "no-lone-blocks": [ 1158 | "error" 1159 | ], 1160 | "no-loss-of-precision": [ 1161 | "error" 1162 | ], 1163 | "no-misleading-character-class": [ 1164 | "error" 1165 | ], 1166 | "no-prototype-builtins": [ 1167 | "error" 1168 | ], 1169 | "no-useless-catch": [ 1170 | "error" 1171 | ], 1172 | "no-mixed-operators": [ 1173 | "error", 1174 | { 1175 | "groups": [ 1176 | [ 1177 | "==", 1178 | "!=", 1179 | "===", 1180 | "!==", 1181 | ">", 1182 | ">=", 1183 | "<", 1184 | "<=" 1185 | ], 1186 | [ 1187 | "&&", 1188 | "||" 1189 | ], 1190 | [ 1191 | "in", 1192 | "instanceof" 1193 | ] 1194 | ], 1195 | "allowSamePrecedence": true 1196 | } 1197 | ], 1198 | "no-mixed-spaces-and-tabs": [ 1199 | "error" 1200 | ], 1201 | "no-multi-str": [ 1202 | "error" 1203 | ], 1204 | "no-multiple-empty-lines": [ 1205 | "error", 1206 | { 1207 | "max": 1, 1208 | "maxEOF": 0 1209 | } 1210 | ], 1211 | "no-new": [ 1212 | "error" 1213 | ], 1214 | "no-new-func": [ 1215 | "error" 1216 | ], 1217 | "no-new-object": [ 1218 | "error" 1219 | ], 1220 | "no-new-symbol": [ 1221 | "error" 1222 | ], 1223 | "no-new-wrappers": [ 1224 | "error" 1225 | ], 1226 | "no-obj-calls": [ 1227 | "error" 1228 | ], 1229 | "no-octal": [ 1230 | "error" 1231 | ], 1232 | "no-octal-escape": [ 1233 | "error" 1234 | ], 1235 | "no-proto": [ 1236 | "error" 1237 | ], 1238 | "no-redeclare": [ 1239 | "error", 1240 | { 1241 | "builtinGlobals": false 1242 | } 1243 | ], 1244 | "no-regex-spaces": [ 1245 | "error" 1246 | ], 1247 | "no-return-assign": [ 1248 | "error", 1249 | "except-parens" 1250 | ], 1251 | "no-self-assign": [ 1252 | "error", 1253 | { 1254 | "props": true 1255 | } 1256 | ], 1257 | "no-self-compare": [ 1258 | "error" 1259 | ], 1260 | "no-sequences": [ 1261 | "error" 1262 | ], 1263 | "no-shadow-restricted-names": [ 1264 | "error" 1265 | ], 1266 | "no-sparse-arrays": [ 1267 | "error" 1268 | ], 1269 | "no-tabs": [ 1270 | "error" 1271 | ], 1272 | "no-template-curly-in-string": [ 1273 | "error" 1274 | ], 1275 | "no-this-before-super": [ 1276 | "error" 1277 | ], 1278 | "no-throw-literal": [ 1279 | "error" 1280 | ], 1281 | "no-trailing-spaces": [ 1282 | "error" 1283 | ], 1284 | "no-undef": [ 1285 | "error" 1286 | ], 1287 | "no-undef-init": [ 1288 | "error" 1289 | ], 1290 | "no-unexpected-multiline": [ 1291 | "error" 1292 | ], 1293 | "no-unmodified-loop-condition": [ 1294 | "error" 1295 | ], 1296 | "no-unneeded-ternary": [ 1297 | "error", 1298 | { 1299 | "defaultAssignment": false 1300 | } 1301 | ], 1302 | "no-unreachable": [ 1303 | "error" 1304 | ], 1305 | "no-unreachable-loop": [ 1306 | "error" 1307 | ], 1308 | "no-unsafe-finally": [ 1309 | "error" 1310 | ], 1311 | "no-unsafe-negation": [ 1312 | "error" 1313 | ], 1314 | "no-unused-expressions": [ 1315 | "error", 1316 | { 1317 | "allowShortCircuit": true, 1318 | "allowTernary": true, 1319 | "allowTaggedTemplates": true, 1320 | "enforceForJSX": false 1321 | } 1322 | ], 1323 | "no-use-before-define": [ 1324 | "error", 1325 | { 1326 | "functions": false, 1327 | "classes": false, 1328 | "variables": false 1329 | } 1330 | ], 1331 | "no-useless-call": [ 1332 | "error" 1333 | ], 1334 | "no-useless-computed-key": [ 1335 | "error" 1336 | ], 1337 | "no-useless-constructor": [ 1338 | "error" 1339 | ], 1340 | "no-useless-escape": [ 1341 | "error" 1342 | ], 1343 | "no-useless-rename": [ 1344 | "error" 1345 | ], 1346 | "no-useless-return": [ 1347 | "error" 1348 | ], 1349 | "no-void": [ 1350 | "error" 1351 | ], 1352 | "no-whitespace-before-property": [ 1353 | "error" 1354 | ], 1355 | "no-with": [ 1356 | "error" 1357 | ], 1358 | "object-curly-newline": [ 1359 | "error", 1360 | { 1361 | "multiline": true, 1362 | "consistent": true 1363 | } 1364 | ], 1365 | "object-curly-spacing": [ 1366 | "error", 1367 | "always" 1368 | ], 1369 | "object-property-newline": [ 1370 | "error", 1371 | { 1372 | "allowMultiplePropertiesPerLine": true, 1373 | "allowAllPropertiesOnSameLine": false 1374 | } 1375 | ], 1376 | "one-var": [ 1377 | "error", 1378 | { 1379 | "initialized": "never" 1380 | } 1381 | ], 1382 | "operator-linebreak": [ 1383 | "error", 1384 | "after", 1385 | { 1386 | "overrides": { 1387 | "?": "before", 1388 | ":": "before", 1389 | "|>": "before" 1390 | } 1391 | } 1392 | ], 1393 | "padded-blocks": [ 1394 | "error", 1395 | { 1396 | "blocks": "never", 1397 | "switches": "never", 1398 | "classes": "never" 1399 | } 1400 | ], 1401 | "prefer-const": [ 1402 | "error", 1403 | { 1404 | "destructuring": "all", 1405 | "ignoreReadBeforeAssign": false 1406 | } 1407 | ], 1408 | "prefer-promise-reject-errors": [ 1409 | "error" 1410 | ], 1411 | "prefer-regex-literals": [ 1412 | "error", 1413 | { 1414 | "disallowRedundantWrapping": true 1415 | } 1416 | ], 1417 | "quotes": [ 1418 | "error", 1419 | "single", 1420 | { 1421 | "avoidEscape": true, 1422 | "allowTemplateLiterals": false 1423 | } 1424 | ], 1425 | "rest-spread-spacing": [ 1426 | "error", 1427 | "never" 1428 | ], 1429 | "semi-spacing": [ 1430 | "error", 1431 | { 1432 | "before": false, 1433 | "after": true 1434 | } 1435 | ], 1436 | "space-before-blocks": [ 1437 | "error", 1438 | "always" 1439 | ], 1440 | "space-before-function-paren": [ 1441 | "error", 1442 | "always" 1443 | ], 1444 | "space-in-parens": [ 1445 | "error", 1446 | "never" 1447 | ], 1448 | "space-infix-ops": [ 1449 | "error" 1450 | ], 1451 | "space-unary-ops": [ 1452 | "error", 1453 | { 1454 | "words": true, 1455 | "nonwords": false 1456 | } 1457 | ], 1458 | "spaced-comment": [ 1459 | "error", 1460 | "always", 1461 | { 1462 | "line": { 1463 | "markers": [ 1464 | "*package", 1465 | "!", 1466 | "/", 1467 | ",", 1468 | "=" 1469 | ] 1470 | }, 1471 | "block": { 1472 | "balanced": true, 1473 | "markers": [ 1474 | "*package", 1475 | "!", 1476 | ",", 1477 | ":", 1478 | "::", 1479 | "flow-include" 1480 | ], 1481 | "exceptions": [ 1482 | "*" 1483 | ] 1484 | } 1485 | } 1486 | ], 1487 | "symbol-description": [ 1488 | "error" 1489 | ], 1490 | "template-curly-spacing": [ 1491 | "error", 1492 | "never" 1493 | ], 1494 | "template-tag-spacing": [ 1495 | "error", 1496 | "never" 1497 | ], 1498 | "unicode-bom": [ 1499 | "error", 1500 | "never" 1501 | ], 1502 | "use-isnan": [ 1503 | "error", 1504 | { 1505 | "enforceForSwitchCase": true, 1506 | "enforceForIndexOf": true 1507 | } 1508 | ], 1509 | "valid-typeof": [ 1510 | "error", 1511 | { 1512 | "requireStringLiterals": true 1513 | } 1514 | ], 1515 | "wrap-iife": [ 1516 | "error", 1517 | "any", 1518 | { 1519 | "functionPrototypeMethods": true 1520 | } 1521 | ], 1522 | "yield-star-spacing": [ 1523 | "error", 1524 | "both" 1525 | ], 1526 | "yoda": [ 1527 | "error", 1528 | "never" 1529 | ], 1530 | "import/export": [ 1531 | "error" 1532 | ], 1533 | "import/first": [ 1534 | "error" 1535 | ], 1536 | "import/no-absolute-path": [ 1537 | "error", 1538 | { 1539 | "esmodule": true, 1540 | "commonjs": true, 1541 | "amd": false 1542 | } 1543 | ], 1544 | "import/no-duplicates": [ 1545 | "error" 1546 | ], 1547 | "import/no-named-default": [ 1548 | "error" 1549 | ], 1550 | "import/no-webpack-loader-syntax": [ 1551 | "error" 1552 | ], 1553 | "n/handle-callback-err": [ 1554 | "error", 1555 | "^(err|error)$" 1556 | ], 1557 | "n/no-callback-literal": [ 1558 | "error" 1559 | ], 1560 | "n/no-new-require": [ 1561 | "error" 1562 | ], 1563 | "n/no-path-concat": [ 1564 | "error" 1565 | ] 1566 | }, 1567 | "settings": { 1568 | "jsdoc": { 1569 | "mode": "typescript" 1570 | } 1571 | }, 1572 | "ignorePatterns": [ 1573 | "/coverage/**/*" 1574 | ] 1575 | } 1576 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli/tsconfig/node18.json", 3 | "files": [ 4 | "cli.js" 5 | ], 6 | "include": [ 7 | "lib/**/*", 8 | "test/**/*", 9 | ], 10 | "compilerOptions": { 11 | "types": ["node", "mocha"] 12 | } 13 | } 14 | --------------------------------------------------------------------------------