diff --git a/.prettierrc b/.prettierrc index fa9699b..aed2abb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,13 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 80, - "tabWidth": 2 + "tabWidth": 2, + "overrides": [ + { + "files": ["*.md", "*.mdx"], + "options": { + "proseWrap": "always" + } + } + ] } diff --git a/README.md b/README.md index 5007971..ab5008b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # js-lint -A batteries-included, TypeScript-aware linting CLI and ESLint flat config bundle for use in Matrix AI JavaScript/TypeScript projects. +A batteries-included, TypeScript-aware linting CLI and ESLint flat config bundle +for use in Matrix AI JavaScript/TypeScript projects. -- Type-aware linting powered by `@typescript-eslint` using one or more `tsconfig.json` files -- Built-in support for React, Tailwind, JSX a11y, Prettier, and Matrix AI custom rules +- Type-aware linting powered by `@typescript-eslint` using one or more + `tsconfig.json` files +- Built-in support for React, Tailwind, JSX a11y, Prettier, and Matrix AI custom + rules - Supports Prettier formatting for Markdown and ShellCheck for shell scripts - Single command to lint JavaScript/TypeScript, Markdown, and shell scripts -- Customizable via `matrixai-lint-config.json` and extensible with your own ESLint config +- Customizable via `matrixai-lint-config.json` and extensible with your own + ESLint config - CLI options to override config and enable auto-fix ## Installation @@ -46,7 +50,8 @@ matrixai-lint --config ./eslint.config.js --fix ### TypeScript Support -The linter is TypeScript-aware and requires a `tsconfig.json` to determine which files to lint and how to parse them. +The linter is TypeScript-aware and requires a `tsconfig.json` to determine which +files to lint and how to parse them. By default: @@ -55,7 +60,9 @@ By default: ### Working with multiple tsconfigs -If your project uses more than one `tsconfig.json` or doesn't have one at the root, you can configure the linter using a `matrixai-lint-config.json` file at the root: +If your project uses more than one `tsconfig.json` or doesn't have one at the +root, you can configure the linter using a `matrixai-lint-config.json` file at +the root: ```json { @@ -69,7 +76,8 @@ If your project uses more than one `tsconfig.json` or doesn't have one at the ro | `tsconfigPaths` | `string[]` | One or more paths to `tsconfig.json` files | | `forceInclude` | `string[]` | Paths to always include, even if excluded by tsconfig (must be included by at least one) | -> ⚠ If a path in `forceInclude` is not included in any of the `tsconfigPaths`, TypeScript will throw a parsing error. +> ⚠ If a path in `forceInclude` is not included in any of the `tsconfigPaths`, +> TypeScript will throw a parsing error. ### ESLint Config Override @@ -100,14 +108,14 @@ Valid config filenames: ```ts // eslint.config.js -import matrixai from '@matrixai/lint/config'; +import matrixai from "@matrixai/lint/config"; export default [ ...matrixai, { rules: { - '@typescript-eslint/no-explicit-any': 'error', - 'no-console': 'off', + "@typescript-eslint/no-explicit-any": "error", + "no-console": "off", }, }, ]; diff --git a/src/bin/lint.ts b/src/bin/lint.ts index 4e63914..936dabb 100644 --- a/src/bin/lint.ts +++ b/src/bin/lint.ts @@ -6,6 +6,7 @@ import process from 'node:process'; import childProcess from 'node:child_process'; import fs from 'node:fs'; import { createRequire } from 'node:module'; +import url from 'node:url'; import { Command } from 'commander'; import * as utils from '../utils.js'; @@ -13,6 +14,12 @@ const platform = os.platform(); const program = new Command(); const DEFAULT_SHELLCHECK_SEARCH_ROOTS = ['./src', './scripts', './tests']; +const dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const builtinPrettierCfg = path.resolve( + dirname, + '../configs/prettier.config.mjs', +); + program .name('matrixai-lint') .description( @@ -139,7 +146,16 @@ async function main(argv = process.argv) { return; } - const prettierArgs = [fix ? '--write' : '--check', ...markdownFiles]; + const prettierArgs = [ + '--config', + builtinPrettierCfg, + '--config-precedence', + 'cli-override', + '--no-editorconfig', + fix ? '--write' : '--check', + ...markdownFiles, + ]; + console.error('Running prettier:'); const require = createRequire(import.meta.url); @@ -153,9 +169,7 @@ async function main(argv = process.argv) { try { if (prettierBin) { - console.error( - ` ${process.execPath} ${prettierBin} ${prettierArgs.join(' ')}`, - ); + console.error(` ${prettierBin} \n ${prettierArgs.join('\n' + ' ')}`); childProcess.execFileSync( process.execPath, [prettierBin, ...prettierArgs], @@ -167,7 +181,7 @@ async function main(argv = process.argv) { }, ); } else { - console.error(' prettier ' + prettierArgs.join(' ')); + console.error('prettier' + prettierArgs.join('\n' + ' ')); childProcess.execFileSync('prettier', prettierArgs, { stdio: 'inherit', windowsHide: true, diff --git a/src/configs/js.ts b/src/configs/js.ts index 03db240..a09bce0 100644 --- a/src/configs/js.ts +++ b/src/configs/js.ts @@ -1,14 +1,14 @@ -import type { EcmaVersion } from '@typescript-eslint/utils/ts-eslint'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import globals from 'globals'; -import _import from 'eslint-plugin-import'; -import js from '@eslint/js'; -import tsParser from '@typescript-eslint/parser'; -import { FlatCompat } from '@eslint/eslintrc'; -import { fixupPluginRules } from '@eslint/compat'; -import matrixaiPlugin from '../plugins/eslint-plugin-matrixai.js'; -import { resolveMatrixConfig } from '../utils.js'; +import type { EcmaVersion } from "@typescript-eslint/utils/ts-eslint"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import globals from "globals"; +import _import from "eslint-plugin-import"; +import js from "@eslint/js"; +import tsParser from "@typescript-eslint/parser"; +import { FlatCompat } from "@eslint/eslintrc"; +import { fixupPluginRules } from "@eslint/compat"; +import matrixaiPlugin from "../plugins/eslint-plugin-matrixai.js"; +import { resolveMatrixConfig } from "../utils.js"; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -21,23 +21,23 @@ const compat = new FlatCompat({ const config = [ ...compat.extends( - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:tailwindcss/recommended', - 'plugin:jsx-a11y/recommended', + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:tailwindcss/recommended", + "plugin:jsx-a11y/recommended", ), { plugins: { import: fixupPluginRules(_import), - '@matrixai': matrixaiPlugin, + "@matrixai": matrixaiPlugin, }, settings: { react: { - version: 'detect', + version: "detect", }, }, @@ -50,210 +50,210 @@ const config = [ }, parser: tsParser, ecmaVersion: 5 as EcmaVersion, - sourceType: 'module', + sourceType: "module", parserOptions: { project: resolveMatrixConfig().tsconfigPaths, }, }, rules: { // MatrixAI rules - '@matrixai/no-aliased-imports': [ - 'error', + "@matrixai/no-aliased-imports": [ + "error", { - aliases: [{ prefix: '#', target: 'src' }], - includeFolders: ['src'], + aliases: [{ prefix: "#", target: "src" }], + includeFolders: ["src"], autoFix: true, }, ], // React rules - 'react/react-in-jsx-scope': 0, - 'react/no-unknown-property': 'off', - 'react/button-has-type': 'error', - 'react/no-unused-prop-types': 'error', - 'react/jsx-pascal-case': 'error', - 'react/jsx-no-script-url': 'error', - 'react/no-children-prop': 'error', - 'react/no-danger': 'error', - 'react/no-danger-with-children': 'error', - 'react/no-unstable-nested-components': ['error', { allowAsProps: true }], - 'react/jsx-fragments': 'error', - 'react/destructuring-assignment': [ - 'error', - 'always', - { destructureInSignature: 'always' }, + "react/react-in-jsx-scope": 0, + "react/no-unknown-property": "off", + "react/button-has-type": "error", + "react/no-unused-prop-types": "error", + "react/jsx-pascal-case": "error", + "react/jsx-no-script-url": "error", + "react/no-children-prop": "error", + "react/no-danger": "error", + "react/no-danger-with-children": "error", + "react/no-unstable-nested-components": ["error", { allowAsProps: true }], + "react/jsx-fragments": "error", + "react/destructuring-assignment": [ + "error", + "always", + { destructureInSignature: "always" }, ], - 'react/jsx-no-leaked-render': ['error', { validStrategies: ['ternary'] }], - 'react/function-component-definition': [ - 'warn', - { namedComponents: 'arrow-function' }, + "react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary"] }], + "react/function-component-definition": [ + "warn", + { namedComponents: "arrow-function" }, ], - 'react/jsx-key': [ - 'error', + "react/jsx-key": [ + "error", { checkFragmentShorthand: true, checkKeyMustBeforeSpread: true, warnOnDuplicates: true, }, ], - 'react/jsx-no-useless-fragment': 'warn', - 'react/jsx-curly-brace-presence': 'warn', - 'react/no-typos': 'warn', - 'react/display-name': 'warn', - 'react/jsx-sort-props': 'warn', - 'react/jsx-one-expression-per-line': 'off', - 'react/prop-types': 'off', + "react/jsx-no-useless-fragment": "warn", + "react/jsx-curly-brace-presence": "warn", + "react/no-typos": "warn", + "react/display-name": "warn", + "react/jsx-sort-props": "warn", + "react/jsx-one-expression-per-line": "off", + "react/prop-types": "off", - '@typescript-eslint/no-floating-promises': [ - 'error', + "@typescript-eslint/no-floating-promises": [ + "error", { ignoreVoid: true, ignoreIIFE: true, }, ], - '@typescript-eslint/no-misused-promises': [ - 'error', + "@typescript-eslint/no-misused-promises": [ + "error", { checksVoidReturn: false, }, ], - '@typescript-eslint/await-thenable': ['error'], - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-unsafe-declaration-merging': 'off', + "@typescript-eslint/await-thenable": ["error"], + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "off", - 'linebreak-style': ['error', 'unix'], - 'no-empty': 1, - 'no-useless-catch': 1, - 'no-prototype-builtins': 1, - 'no-constant-condition': 0, - 'no-useless-escape': 0, - 'no-console': 'error', - 'no-restricted-globals': [ - 'error', + "linebreak-style": ["error", "unix"], + "no-empty": 1, + "no-useless-catch": 1, + "no-prototype-builtins": 1, + "no-constant-condition": 0, + "no-useless-escape": 0, + "no-console": "error", + "no-restricted-globals": [ + "error", { - name: 'global', - message: 'Use `globalThis` instead', + name: "global", + message: "Use `globalThis` instead", }, { - name: 'window', - message: 'Use `globalThis` instead', + name: "window", + message: "Use `globalThis` instead", }, ], - 'prefer-rest-params': 0, - 'require-yield': 0, - eqeqeq: ['error', 'smart'], - 'spaced-comment': [ - 'warn', - 'always', + "prefer-rest-params": 0, + "require-yield": 0, + eqeqeq: ["error", "smart"], + "spaced-comment": [ + "warn", + "always", { line: { - exceptions: ['-'], + exceptions: ["-"], }, block: { - exceptions: ['*'], + exceptions: ["*"], }, - markers: ['/'], + markers: ["/"], }, ], - 'capitalized-comments': [ - 'warn', - 'always', + "capitalized-comments": [ + "warn", + "always", { ignoreInlineComments: true, ignoreConsecutiveComments: true, }, ], - curly: ['error', 'multi-line', 'consistent'], - 'import/order': [ - 'error', + curly: ["error", "multi-line", "consistent"], + "import/order": [ + "error", { groups: [ - 'type', - 'builtin', - 'external', - 'internal', - 'index', - 'sibling', - 'parent', - 'object', + "type", + "builtin", + "external", + "internal", + "index", + "sibling", + "parent", + "object", ], pathGroups: [ { - pattern: '@', - group: 'internal', + pattern: "@", + group: "internal", }, { - pattern: '@/**', - group: 'internal', + pattern: "@/**", + group: "internal", }, ], - pathGroupsExcludedImportTypes: ['type'], - 'newlines-between': 'never', + pathGroupsExcludedImportTypes: ["type"], + "newlines-between": "never", }, ], - '@typescript-eslint/no-require-imports': 0, - '@typescript-eslint/no-namespace': 0, - '@typescript-eslint/no-explicit-any': 0, - '@typescript-eslint/explicit-module-boundary-types': 0, - '@typescript-eslint/no-unused-vars': [ - 'warn', + "@typescript-eslint/no-require-imports": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-unused-vars": [ + "warn", { - varsIgnorePattern: '^_', - argsIgnorePattern: '^_', + varsIgnorePattern: "^_", + argsIgnorePattern: "^_", }, ], - '@typescript-eslint/no-inferrable-types': 0, - '@typescript-eslint/no-non-null-assertion': 0, - '@typescript-eslint/no-this-alias': 0, - '@typescript-eslint/no-var-requires': 0, - '@typescript-eslint/no-empty-function': 0, - '@typescript-eslint/no-empty-interface': 0, - '@typescript-eslint/consistent-type-imports': ['error'], - '@typescript-eslint/consistent-type-exports': ['error'], - 'no-throw-literal': 'off', - '@typescript-eslint/no-throw-literal': 'off', - '@typescript-eslint/naming-convention': [ - 'error', + "@typescript-eslint/no-inferrable-types": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/no-this-alias": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-exports": ["error"], + "no-throw-literal": "off", + "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/naming-convention": [ + "error", { - selector: 'function', - format: ['camelCase', 'PascalCase'], - leadingUnderscore: 'allow', - trailingUnderscore: 'allowSingleOrDouble', + selector: "function", + format: ["camelCase", "PascalCase"], + leadingUnderscore: "allow", + trailingUnderscore: "allowSingleOrDouble", }, { - selector: 'variable', - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - leadingUnderscore: 'allow', - trailingUnderscore: 'allowSingleOrDouble', + selector: "variable", + format: ["camelCase", "UPPER_CASE", "PascalCase"], + leadingUnderscore: "allow", + trailingUnderscore: "allowSingleOrDouble", }, { - selector: 'parameter', - format: ['camelCase'], - leadingUnderscore: 'allow', - trailingUnderscore: 'allowSingleOrDouble', + selector: "parameter", + format: ["camelCase"], + leadingUnderscore: "allow", + trailingUnderscore: "allowSingleOrDouble", }, { - selector: 'typeLike', - format: ['PascalCase'], - trailingUnderscore: 'allowSingleOrDouble', + selector: "typeLike", + format: ["PascalCase"], + trailingUnderscore: "allowSingleOrDouble", }, { - selector: 'enumMember', - format: ['PascalCase', 'UPPER_CASE'], + selector: "enumMember", + format: ["PascalCase", "UPPER_CASE"], }, { - selector: 'objectLiteralProperty', + selector: "objectLiteralProperty", format: null, }, { - selector: 'typeProperty', + selector: "typeProperty", format: null, }, ], - '@typescript-eslint/ban-ts-comment': [ - 'error', + "@typescript-eslint/ban-ts-comment": [ + "error", { - 'ts-ignore': 'allow-with-description', + "ts-ignore": "allow-with-description", }, ], }, diff --git a/src/configs/prettier.config.mjs b/src/configs/prettier.config.mjs new file mode 100644 index 0000000..b2ab7f4 --- /dev/null +++ b/src/configs/prettier.config.mjs @@ -0,0 +1,14 @@ +/** @type {import('prettier').Config} */ +export const semi = true; +export const trailingComma = "all"; +export const singleQuote = true; +export const printWidth = 80; +export const tabWidth = 2; +export const useTabs = false; +export const endOfLine = "lf"; +export const overrides = [ + { + files: ["*.md", "*.mdx"], + options: { proseWrap: "always" }, + }, +];