Skip to content

Instantly share code, notes, and snippets.

@kesor
Last active January 13, 2025 17:26
Show Gist options
  • Save kesor/616a529f783571d01a94f0f03edc747d to your computer and use it in GitHub Desktop.
Save kesor/616a529f783571d01a94f0f03edc747d to your computer and use it in GitHub Desktop.
ESLing Flat Config Typescript
/**
* @file
* ESLint configuration for a monorepo project
*/
import { default as pluginTs, Config } from 'typescript-eslint'
import { FlatCompat } from '@eslint/eslintrc'
import globals from 'globals'
import jsdoc from 'eslint-plugin-jsdoc'
import pluginJs from '@eslint/js'
import pluginJson from 'eslint-plugin-json'
import pluginMocha from 'eslint-plugin-mocha'
import pluginTW from 'eslint-plugin-tailwindcss'
import pluginVue from 'eslint-plugin-vue'
import pluginRE from 'eslint-plugin-regexp'
import vueParser from 'vue-eslint-parser'
import pluginESx from 'eslint-plugin-es-x'
import pluginChai from 'eslint-plugin-chai-expect'
import pluginVitest from 'eslint-plugin-vitest'
const compat = new FlatCompat()
/**
* Configuration is using internal ESLint merging
* every object in the configuration is standalone,
* and other objects below are merged to override it
* this happens internally inside of ESLint by specifying
* the same `files:[]` with extra rules.
* The `plugins:[]` directive does not have/need `files:[]`
* which is why it is standalone.
* Objects without `files:[]` are global and apply to everything.
*/
export const recommended: Config = pluginTs.config(
{ linterOptions: { reportUnusedDisableDirectives: 'error' } },
{ ignores: ['!.*', '**/node_modules/', '.npm/', '**/dist/', '**/*.min.*',] },
// jsdoc in javascript
{ plugins: jsdoc.configs['flat/recommended'].plugins },
{ ...jsdoc.configs['flat/recommended-error'], files: ['**/*.js'] },
{
name: '@myproject/eslint-shared-jsdoc-js',
files: ['**/*.js'],
rules: {
'jsdoc/require-file-overview': 'warn',
'jsdoc/require-description': 'warn'
}
},
// jsdoc in typescript
{ ...jsdoc.configs['flat/recommended-error'], files: ['**/*.ts'] },
{ ...jsdoc.configs['flat/recommended-typescript-error'], files: ['**/*.ts'] },
{
name: '@myproject/eslint-shared-jsdoc-typescript',
files: ['**/*.ts'],
rules: {
'jsdoc/require-file-overview': ['warn', { tags: { file: { initialCommentsOnly: false } } }],
'jsdoc/require-description': 'warn',
}
},
// json files
{ ...pluginJson.configs['recommended'], files: ['**/*.json'] },
// javascript files
{
name: '@myproject/eslint-shared-js-recommended',
...pluginJs.configs.recommended,
files: ['**/*.js'],
},
{ ...pluginTs.configs.disableTypeChecked, files: ['**/*.js'], },
{ ...pluginESx.configs['flat/restrict-to-es2022'], files: ['**/*.js'], },
{
name: '@myproject/eslint-shared-js',
files: ['**/*.js'],
languageOptions: {
globals: { ...globals.node },
sourceType: 'module',
ecmaVersion: 2022
},
rules: {
'new-cap': ['error'],
'no-invalid-this': ['error'],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'object-curly-spacing': ['error', 'always'],
'quote-props': ['error', 'consistent-as-needed'],
'quotes': ['error', 'single'],
'semi': ['error', 'never'],
}
},
// typescript files
...pluginTs.configs.strictTypeChecked.map(cfg => {
cfg.files = ['**/*.ts']
return cfg
}),
{ ...pluginESx.configs['flat/no-new-in-esnext'], files: ['**/*.ts'] },
{
files: ['**/*.ts'],
name: '@myproject/eslint-shared-typescript',
plugins: {
'@typescript-eslint': pluginTs.plugin,
// ...compat.plugins('tsdoc')[0].plugins,
},
languageOptions: {
parser: pluginTs.parser,
globals: globals.node,
sourceType: 'module',
ecmaVersion: 2022,
parserOptions: {
project: true,
EXPERIMENTAL_useProjectService: true,
}
},
rules: {
'new-cap': ['error'],
'no-invalid-this': ['error'],
'object-curly-spacing': ['error', 'always'],
'no-unused-vars': 'off',
'quote-props': 'off',
'quotes': 'off',
'semi': 'off',
'no-dupe-class-members': 'off',
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^(_|log$)', argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }],
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/indent': ['error', 2],
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/strict-boolean-expressions': ['error', { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: true }],
// 'tsdoc/syntax': 'error',
}
},
// promises
...compat.extends('plugin:promise/recommended').map(cfg => {
cfg.files = ['**/*.js', '**/*.ts', '**/web/**/*.vue']
return cfg
}),
// imports in javascript files
...compat.extends('plugin:import-x/recommended').map(cfg => {
cfg.files = ['**/*.js']
return cfg
}),
{
name: '@myproject/eslint-shared-import-x-js',
files: ['**/*.js'],
rules: {
'import-x/order': 'error',
'import-x/first': 'error',
'import-x/newline-after-import': 'error',
},
settings: {
'import-x/internal-regex': '^@myproject/',
},
},
// imports in typescript files
...compat.extends('plugin:import-x/recommended').map(cfg => {
cfg.files = ['**/*.ts', '**/web/**/*.vue']
return cfg
}),
...compat.extends('plugin:import-x/typescript').map(cfg => {
cfg.files = ['**/*.ts', '**/web/**/*.vue']
return cfg
}),
{
name: '@myproject/eslint-shared-import-x-typescript',
files: ['**/*.ts', '**/web/**/*.vue'],
rules: {
'import-x/order': 'error',
'import-x/first': 'error',
'import-x/newline-after-import': 'error',
},
settings: {
'import-x/internal-regex': '^@myproject/',
'import-x/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
[vueParser.meta.name]: ['.vue'],
},
'import-x/resolver': {
'eslint-import-resolver-vite-ts': {
extensions: ['.vue']
},
typescript: {
extensions: ['.ts'],
project: [
'packages/*/tsconfig.app.json',
'apps/*/tsconfig.app.json',
]
},
node: { extensions: ['.js', '.jsx'] },
}
},
},
// vue files
...pluginVue.configs['flat/recommended'].map(cfg => {
cfg.files = ['**/web/**/*.ts', '**/web/**/*.vue']
return cfg
}),
{
files: ['**/web/test/**/*.ts'],
plugins: { vitest: pluginVitest },
rules: pluginVitest.configs.recommended.rules,
settings: {
vitest: { typecheck: true }
},
languageOptions: {
sourceType: 'module',
globals: pluginVitest.environments.env.globals,
},
},
{
name: '@myproject/eslint-vue-typescript',
files: ['**/web/**/*.ts', '**/web/**/*.vue'],
languageOptions: {
globals: { ...globals.browser },
parser: vueParser,
parserOptions: {
parser: {
'ts': '@typescript-eslint/parser',
'<template>': 'espree'
},
project: true,
EXPERIMENTAL_useProjectService: true,
ecmaFeatures: { jsx: true }
}
},
},
// regex
{ ...pluginRE.configs['flat/recommended'], files: ['**/*.js', '**/*.ts', '**/*.vue'] },
// mocha tests
{ ...pluginMocha.configs.flat.recommended, files: ['**/test/**.js', '**/test/**.ts'], },
{ ...pluginChai.configs['recommended-flat'], files: ['**/test/**.js', '**/test/**.ts'], },
// tailwind css
...pluginTW.configs['flat/recommended'].map(cfg => {
cfg.files = ['**/web/**/*.ts', '**/web/**/*.vue']
return cfg
}),
{
name: '@myproject/eslint-shared-tailwindcss',
files: ['**/web/**/*.ts', '**/web/**/*.vue'],
settings: {
tailwindcss: { config: 'tailwind.config.ts', },
},
rules: {
'tailwindcss/classnames-order': 'warn'
}
},
// css
...compat.extends('plugin:css/recommended').map(cfg => {
cfg.files = ['**/web/**/*.ts', '**/web/**/*.vue']
return cfg
}),
// {
// files: ['**/*'],
// rules: {
// 'linebreak-style': ['error', 'unix'],
// }
// },
// editorconfig overrides js/ts rules with its own
...compat.plugins('editorconfig').map(cfg => {
cfg.files = ['**/*.js', '**/*.json', '**/*.ts', '**/*.vue']
return cfg
}),
...compat.extends('plugin:editorconfig/all').map(cfg => {
cfg.files = ['**/*.js', '**/*.json', '**/*.ts', '**/*.vue']
return cfg
}),
)
// DEBUG the generated configuration by inspecting /tmp/eslint-config.js
// import fs from 'node:fs'
// import { inspect } from 'node:util'
// fs.writeFileSync('/tmp/eslint-config.js', inspect(recommended))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment