Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ yarn-error.log*
!.yarn/plugins
!.yarn/sdks
!.yarn/versions

.eslintcache
43 changes: 0 additions & 43 deletions base.yml

This file was deleted.

25 changes: 25 additions & 0 deletions configs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @ts-check

import basicConfig from './lib/basic.mjs'
import stylisticConfig from './lib/style.mjs'
import typescriptConfig from './lib/typescript.mjs'
import reactConfig from './lib/react.mjs'

/** @type {import('eslint').Linter.Config[]} */
const base = [
...stylisticConfig,
...basicConfig,
// Using a @type comment before a parenthesized expression like this is a
// type cast (like doing "as …" in TypeScript). Unsure why these types are
// incompatible though.
...(/** @type {import('eslint').Linter.Config[]} */ (typescriptConfig)),
]

/** @type {import('eslint').Linter.Config[]} */
export const basic = base

/** @type {import('eslint').Linter.Config[]} */
export const react = [
...base,
...reactConfig,
]
39 changes: 39 additions & 0 deletions eslint-plugin-react.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Non-exhaustive.
declare module 'eslint-plugin-react' {
import type { ESLint, Linter } from 'eslint'

const plugin: ESLint.Plugin & {
configs: {
flat: {
all: Linter.Config
recommended: Linter.Config
'jsx-runtime': Linter.Config
}
}
}

export default plugin
}

declare module 'eslint-plugin-react-hooks' {
import type { ESLint, Rule, Linter } from 'eslint'

const plugin: ESLint.Plugin & {
configs: { recommended: Linter.Config }
rules: {
'rules-of-hooks': Rule.RuleModule
'exhaustive-deps': Rule.RuleModule
}
}
export default plugin
}

// Non-exhaustive.
declare module 'eslint-plugin-import' {
import type { Linter } from 'eslint'

export const flatConfigs: {
typescript: Linter.Config
recommended: Linter.Config
}
}
8 changes: 8 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* WARNING: This ESLint config file should only be used while you are developing
* this ESLint config. In other words, this lets us lint the lint config with
* itself. If you want to _consume_ the ESLint configs exported by this
* package, just import it and use it within a flat config.
*/

export { basic as default } from './configs.mjs'
59 changes: 0 additions & 59 deletions index.yml

This file was deleted.

104 changes: 104 additions & 0 deletions lib/basic.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// @ts-check

import { fileURLToPath } from 'node:url'
import path from 'node:path'

import { fixupPluginRules } from '@eslint/compat'
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'
import eslintPluginImport from 'eslint-plugin-import'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
})

// https://github.com/import-js/eslint-plugin-import/issues/2948#issuecomment-2148832701
/**
* @param {string} name
* @param {string} alias
* @returns {import('eslint').ESLint.Plugin}
*/
function legacyPlugin(name, alias = name) {
const plugin = compat.plugins(name)[0]?.plugins?.[alias]
if (!plugin) throw new Error(`Unable to resolve plugin ${name} and/or alias ${alias}`)
return fixupPluginRules(plugin)
}

const WARNING_IN_CI_ONLY = process.env.CI ? 'warn' : 'off'

/** @type {import('eslint').Linter.Config[]} */
export const importPlugin = [
{ ...eslintPluginImport.flatConfigs.recommended, plugins: {} },
// TypeScript one doesn't have a name, but recommended does.
{ ...eslintPluginImport.flatConfigs.typescript, name: 'import/typescript', plugins: {} },

{
name: '@textshq/eslint-config.basic.import',
plugins: { import: legacyPlugin('eslint-plugin-import', 'import') },
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
node: true,
},
},
rules: {
// https://typescript-eslint.io/troubleshooting/typed-linting/performance#eslint-plugin-import
'import/named': 'off',
'import/namespace': 'off',
'import/default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-unresolved': 'off',
'import/no-named-as-default': WARNING_IN_CI_ONLY,
'import/no-cycle': WARNING_IN_CI_ONLY,
'import/no-unused-modules': WARNING_IN_CI_ONLY,
'import/no-deprecated': WARNING_IN_CI_ONLY,
},
},
]

/** @type {import('eslint').Linter.Config[]} */
const config = [
// Using a @type comment before a parenthesized expression like this is a
// type cast (like doing "as …" in TypeScript).

{ ...js.configs.recommended, name: '@eslint/js/recommended' },

{
name: '@textshq/eslint-config.basic',
rules: {
// NOTE: Stylistic rules such as `semi` and `arrow-parens` have been
// deprecated in mainline ESLint and now live in ESLint Stylistic
// (https://eslint.style/) instead. This is configured in style.mjs.
'no-constant-binary-expression': 'error',
'no-param-reassign': 'warn',
'import/no-webpack-loader-syntax': 'off',
'import/prefer-default-export': 'off',
camelcase: 'off',
'consistent-return': 'off',
'max-classes-per-file': 'off',
'no-alert': 'off',
'no-await-in-loop': 'off',
'no-bitwise': 'off',
'no-console': 'off',
'no-continue': 'off',
'no-nested-ternary': 'off',
'no-new': 'off',
'no-plusplus': 'off',
'no-restricted-syntax': 'off',
'no-underscore-dangle': 'off',
'prefer-template': 'off',
},
},

...importPlugin,
]

export default config
53 changes: 53 additions & 0 deletions lib/react.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// @ts-check

import eslintPluginJsxA11y from 'eslint-plugin-jsx-a11y'
import eslintPluginReact from 'eslint-plugin-react'
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'

/** @type {import('eslint').Linter.Config[]} */
const config = [
eslintPluginJsxA11y.flatConfigs.recommended,
{ ...eslintPluginReact.configs.flat.recommended, name: 'eslint-plugin-react/recommended' },

{
name: '@textshq/eslint-config.react',
plugins: {
react: eslintPluginReact,
'react-hooks': eslintPluginReactHooks,
},
languageOptions: {
parserOptions: {
jsx: true,
},
},
settings: {
react: {
version: '18.2',
},
},
rules: {
'jsx-a11y/anchor-is-valid': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/control-has-associated-label': 'warn',
'jsx-a11y/label-has-associated-control': 'warn',
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/no-autofocus': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'warn',
'jsx-a11y/no-static-element-interactions': 'off',
'react/function-component-definition': 'off',
'react/jsx-key': 'warn',
'react/jsx-one-expression-per-line': 'off',
'react/jsx-props-no-spreading': 'off',
'react/no-array-index-key': 'off',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',

// eslint-plugin-react-hooks supports ESLint v9, but doesn't export a
// flat config. We can just inline the recommended (legacy) config's
// rules here.
...eslintPluginReactHooks.configs.recommended.rules,
},
},
]

export default config
47 changes: 47 additions & 0 deletions lib/style.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-check

import stylistic from '@stylistic/eslint-plugin'

// N.B. The <true> means "we're using ESLint flat config". Parameterization is
// important because the types would be different otherwise.

/** @type {import('@stylistic/eslint-plugin').StylisticCustomizeOptions<true>} */
export const stylisticCustomizationOptions = {
indent: 2,
quotes: 'single',
semi: false,
braceStyle: '1tbs',
}

/** @type {import('@stylistic/eslint-plugin').StylisticCustomizeOptions<true>} */
export const stylisticCustomizationOptionsJSX = {
...stylisticCustomizationOptions,
jsx: true,
}

/** @type {import('eslint').Linter.Config[]} */
const config = [
// "Recommended" rules added by this call: https://github.com/eslint-stylistic/eslint-stylistic/blob/ff6905308fa7cca27b0131d27e0c8f5b964ecb5a/packages/eslint-plugin/configs/customize.ts#L33
{ ...stylistic.configs.customize(stylisticCustomizationOptions), name: '@stylistic/eslint-plugin' },

{
name: '@textshq/eslint-config.stylistic',
rules: {
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/semi': ['error', 'never'],
'@stylistic/arrow-parens': ['error', 'as-needed'],
'@stylistic/implicit-arrow-linebreak': 'off',
'@stylistic/max-len': 'off',
'@stylistic/newline-per-chained-call': 'off',
'@stylistic/object-curly-newline': 'off',
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'none' },
singleline: { delimiter: 'comma' },
}],
'@stylistic/quote-props': ['warn', 'as-needed'],
},
},

]

export default config
Loading