mirror of
https://github.com/scratchfoundation/eslint-config-scratch.git
synced 2025-07-04 10:20:34 -04:00
refactor!: simplify and flatten configurations
BREAKING CHANGE: Configurations are now returned as objects, like most shared ESLint configurations. The `make*Config` functions are no more. Also, everything is now exported through one file.
This commit is contained in:
parent
329b4de61c
commit
a7dda101f2
8 changed files with 316 additions and 400 deletions
201
README.md
201
README.md
|
@ -12,26 +12,40 @@ Install the config along with its peer dependencies, `eslint` and `prettier`:
|
||||||
npm install -D eslint-config-scratch eslint@^9 prettier@^3
|
npm install -D eslint-config-scratch eslint@^9 prettier@^3
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `eslint.config.mjs` to your project root (pick the `export` line appropriate for your project):
|
Add `eslint.config.mjs` to your project root and, optionally, configure parser options for rules that require type
|
||||||
|
information:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// myProjectRoot/eslint.config.mjs
|
// myProjectRoot/eslint.config.mjs
|
||||||
import { makeEslintConfig } from 'eslint-config-scratch'
|
import { eslintConfigScratch } from 'eslint-config-scratch'
|
||||||
|
|
||||||
// for a TypeScript project:
|
|
||||||
export default makeEslintConfig({ globals: 'browser', tsconfigRootDir: import.meta.dirname })
|
|
||||||
|
|
||||||
// for plain JavaScript:
|
// for plain JavaScript:
|
||||||
export default makeEslintConfig({ globals: 'browser' })
|
export default eslintConfigScratch.recommended
|
||||||
|
|
||||||
|
// for a TypeScript project:
|
||||||
|
export default eslintConfigScratch.config(
|
||||||
|
eslintConfigScratch.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The function `eslintConfigScratch.config` is a re-export of the `config` function from `typescript-eslint`, and helps
|
||||||
|
with merging and extending configurations.
|
||||||
|
|
||||||
Add `prettier.config.mjs` to your project root as well:
|
Add `prettier.config.mjs` to your project root as well:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// myProjectRoot/prettier.config.mjs
|
// myProjectRoot/prettier.config.mjs
|
||||||
import { makePrettierConfig } from 'eslint-config-scratch'
|
import { prettierConfigScratch } from 'eslint-config-scratch'
|
||||||
|
|
||||||
export default makePrettierConfig()
|
export default prettierConfigScratch.recommended
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, add scripts like these to your `package.json`:
|
Finally, add scripts like these to your `package.json`:
|
||||||
|
@ -45,147 +59,88 @@ Finally, add scripts like these to your `package.json`:
|
||||||
|
|
||||||
## Basic Configuration
|
## Basic Configuration
|
||||||
|
|
||||||
The `makeEslintConfig` function takes options to adjust the ESLint configuration object for your project. Most
|
The `eslintConfigScratch.config` is a re-export of the `config` function from `typescript-eslint`. Full documentation
|
||||||
projects should start with something like this:
|
is available here: <https://typescript-eslint.io/packages/typescript-eslint#config>.
|
||||||
|
|
||||||
```mjs
|
The `config` function can be used to add or override rules, plugins, and other configuration options. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
// myProjectRoot/eslint.config.mjs
|
// myProjectRoot/eslint.config.mjs
|
||||||
import { makeEslintConfig } from 'eslint-config-scratch'
|
import { eslintConfigScratch } from 'eslint-config-scratch'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
export default makeEslintConfig({
|
|
||||||
// Optional: specify global variables available in your environment
|
|
||||||
globals: 'browser',
|
|
||||||
|
|
||||||
// Optional: enables rules that use type info, some of which work in JS too
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have no `tsconfig.json` (or `jsconfig.json`) in your project, you can skip the `tsconfigRootDir` option. Rules
|
|
||||||
that require type information will be disabled or replaced with less strict alternatives that work without type info.
|
|
||||||
|
|
||||||
### Globals
|
|
||||||
|
|
||||||
The `globals` property is optional. If present, it can take several forms:
|
|
||||||
|
|
||||||
- a string, interpreted as a key in the `globals` object exported by the `globals` package.
|
|
||||||
- Examples: `'browser'`, `'node'`, `'es2021'`, `'jest'`, etc.
|
|
||||||
- an object, set up as described in the "Specifying Globals" section of the [ESLint documentation](https://eslint.org/docs/latest/use/configure/language-options#using-configuration-files)
|
|
||||||
- Example: `{ myGlobal: 'readonly', anotherGlobal: 'writable' }`
|
|
||||||
- an array of zero or more of any mixture of the above
|
|
||||||
|
|
||||||
```mjs
|
|
||||||
// myProjectRoot/eslint.config.mjs
|
|
||||||
import { makeEslintConfig } from 'eslint-config-scratch'
|
|
||||||
|
|
||||||
export default makeEslintConfig({
|
|
||||||
// Optional: enables rules that use type info, some of which work in JS too
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
|
|
||||||
// Optional: specify global variables available in your environment
|
|
||||||
// Warning: this is a very silly configuration
|
|
||||||
globals: [
|
|
||||||
'shared-node-browser',
|
|
||||||
{
|
|
||||||
fun: 'readonly',
|
|
||||||
thing: false,
|
|
||||||
},
|
|
||||||
'es2021',
|
|
||||||
{
|
|
||||||
whyNot: 'writable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Further Customization
|
|
||||||
|
|
||||||
The first parameter to `makeEslintConfig` is covered above. Any further parameters passed to `makeEslintConfig` are
|
|
||||||
appended to the resulting ESLint configuration array. This means you can customize your configuration further like
|
|
||||||
this:
|
|
||||||
|
|
||||||
```mjs
|
|
||||||
// myProjectRoot/eslint.config.mjs
|
|
||||||
import { makeEslintConfig } from 'eslint-config-scratch'
|
|
||||||
|
|
||||||
export default makeEslintConfig(
|
|
||||||
{
|
|
||||||
// Optional: enables rules that use type info, some of which work in JS too
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
|
|
||||||
// Optional: specify global variables available in your environment
|
|
||||||
globals: 'browser',
|
|
||||||
},
|
|
||||||
// Add custom rules or overrides here
|
|
||||||
{
|
|
||||||
files: ['*.test.js'],
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off', // Allow console logs in test files
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
You could concatenate more configuration objects onto the array returned by `makeEslintConfig` with equivalent
|
|
||||||
results, but this approach offers better editor hints for autocomplete and type checking.
|
|
||||||
|
|
||||||
All ESLint configuration options are available this way. You can use this to handle globals yourself if the simplified
|
|
||||||
`globals` configuration from above doesn't meet your needs:
|
|
||||||
|
|
||||||
```mjs
|
|
||||||
// myProjectRoot/eslint.config.mjs
|
|
||||||
import { makeEslintConfig } from 'eslint-config-scratch'
|
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
|
|
||||||
export default makeEslintConfig(
|
export default eslintConfigScratch.config(
|
||||||
|
eslintConfigScratch.recommended,
|
||||||
{
|
{
|
||||||
// Optional: enables rules that use type info, some of which work in JS too
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['src/main/**.js'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['src/renderer/**.js'],
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.node,
|
||||||
MY_CUSTOM_GLOBAL: 'readonly',
|
MY_CUSTOM_GLOBAL: 'readonly',
|
||||||
},
|
},
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Ignore all files in the dist directory
|
||||||
|
globalIgnores(['dist/**/*']),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Of course, another option would be to place a different `eslint.config.mjs` file in each subdirectory. If you have
|
## Granular Configuration
|
||||||
multiple `tsconfig.json` or `jsconfig.json` files in your project, it likely makes sense to have an
|
|
||||||
`eslint.config.mjs` file beside each one.
|
The `eslintConfigScratch` object contains granular configurations as well:
|
||||||
|
|
||||||
|
- `recommendedTypeFree`: A configuration suitable for contexts without type information, such as a JavaScript project.
|
||||||
|
- `recommendedTypeChecked`: A configuration suitable for contexts with type information, such as a TypeScript project.
|
||||||
|
You must provide extra configuration to `parserOptions` to enable type checking. See here:
|
||||||
|
<https://typescript-eslint.io/getting-started/typed-linting/>
|
||||||
|
|
||||||
|
The `recommended` configuration is a combination of the two, and should be suitable for most projects. Features
|
||||||
|
requiring type information are enabled for TypeScript files, and features that don't require type information are
|
||||||
|
enabled for all files.
|
||||||
|
|
||||||
## Legacy Styles
|
## Legacy Styles
|
||||||
|
|
||||||
Scratch used very different styling rules in `eslint-config-scratch@^9` and below. If you need to use those rules, you
|
Scratch used very different styling rules in `eslint-config-scratch@^9` and below. If you need to use those rules, you
|
||||||
can use the rule sets under `legacy/`:
|
can use these legacy configurations:
|
||||||
|
|
||||||
- `eslint-config-scratch/legacy`: Legacy base configuration, not configured for any particular environment
|
- `eslintConfigScratch.legacy.base`: Legacy base configuration, not configured for any particular environment
|
||||||
- `eslint-config-scratch/legacy/es6`: Legacy rules for targeting Scratch's supported web browsers
|
- `eslintConfigScratch.legacy.es6`: Legacy rules for targeting Scratch's supported web browsers
|
||||||
- `eslint-config-scratch/legacy/node`: Legacy rules for targeting Node.js
|
- `eslintConfigScratch.legacy.node`: Legacy rules for targeting Node.js
|
||||||
- `eslint-config-scratch/legacy/react`: Legacy rules for targeting Scratch's supported web browsers with React
|
- `eslintConfigScratch.legacy.react`: Legacy rules for targeting Scratch's supported web browsers with React
|
||||||
|
|
||||||
New projects should not use these rule sets. They may disappear in the future. Scratch did not use Prettier at this
|
New projects should not use these rule sets. They may disappear in the future. Scratch did not use Prettier at this
|
||||||
time, so there is no legacy Prettier configuration.
|
time, so there is no legacy Prettier configuration.
|
||||||
|
|
||||||
Use these rule sets by importing them directly:
|
Legacy Scratch projects usually `extend` more than one of these at a time, and potentially a different set per
|
||||||
|
subdirectory. To do that in this new flat configuration format:
|
||||||
|
|
||||||
```mjs
|
```js
|
||||||
// myProjectRoot/eslint.config.mjs
|
// scratch-gui/eslint.config.mjs
|
||||||
import webConfig from 'eslint-config-scratch/legacy/es6'
|
import { eslintConfigScratch } from 'eslint-config-scratch'
|
||||||
import { globalIgnores } from 'eslint/config'
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import globals from 'globals'
|
||||||
|
|
||||||
/** @returns {import('eslint').Linter.Config[]} */
|
export default eslintConfigScratch.config(
|
||||||
export default [...webConfig, globalIgnores(['dist/**/*'])]
|
eslintConfigScratch.legacy.base,
|
||||||
|
eslintConfigScratch.legacy.es6,
|
||||||
|
{
|
||||||
|
files: ['src/**/*.js', 'src/**/*.jsx'],
|
||||||
|
extends: [eslintConfigScratch.legacy.react],
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// ...customized rules for `src/`...
|
||||||
|
},
|
||||||
|
// ...other settings for `src/`...
|
||||||
|
},
|
||||||
|
// ...settings for `test/`, etc...
|
||||||
|
globalIgnores(['dist/**/*']),
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Committing
|
## Committing
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { makeEslintConfig } from './lib/index.mjs'
|
import globals from 'globals'
|
||||||
|
import { eslintConfigScratch } from './lib/index.mjs'
|
||||||
|
|
||||||
/** @type {import('typescript-eslint').ConfigArray} */
|
export default eslintConfigScratch.config(eslintConfigScratch.recommended, {
|
||||||
export default makeEslintConfig({ globals: 'node' })
|
languageOptions: {
|
||||||
|
globals: globals.node,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
479
lib/eslint.mjs
479
lib/eslint.mjs
|
@ -8,10 +8,20 @@ import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
import markdown from 'eslint-plugin-markdown'
|
import markdown from 'eslint-plugin-markdown'
|
||||||
import react from 'eslint-plugin-react'
|
import react from 'eslint-plugin-react'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import globals from 'globals'
|
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs'
|
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs'
|
||||||
import eslint from '@eslint/js'
|
import eslint from '@eslint/js'
|
||||||
|
import legacyES6 from './legacy/es6.mjs'
|
||||||
|
import legacyBase from './legacy/index.mjs'
|
||||||
|
import legacyNode from './legacy/node.mjs'
|
||||||
|
import legacyReact from './legacy/react.mjs'
|
||||||
|
|
||||||
|
const legacy = {
|
||||||
|
base: legacyBase,
|
||||||
|
es6: legacyES6,
|
||||||
|
node: legacyNode,
|
||||||
|
react: legacyReact,
|
||||||
|
}
|
||||||
|
|
||||||
// See https://www.npmjs.com/package/eslint-plugin-html#user-content-settings
|
// See https://www.npmjs.com/package/eslint-plugin-html#user-content-settings
|
||||||
const htmlExtensions = htmlSettings.getSettings({}).htmlExtensions
|
const htmlExtensions = htmlSettings.getSettings({}).htmlExtensions
|
||||||
|
@ -19,274 +29,233 @@ const htmlExtensions = htmlSettings.getSettings({}).htmlExtensions
|
||||||
// '.html' => '**/*.html'
|
// '.html' => '**/*.html'
|
||||||
const htmlGlobs = htmlExtensions.map(ext => `**/*${ext}`)
|
const htmlGlobs = htmlExtensions.map(ext => `**/*${ext}`)
|
||||||
|
|
||||||
/**
|
const typeScriptExtensions = ['.ts', '.tsx', '.mts', '.cts']
|
||||||
* @typedef {import('eslint').Linter.Globals} Globals
|
|
||||||
* @typedef {keyof globals} GlobalsKey
|
|
||||||
* @typedef {Globals | GlobalsKey} GlobalsObjOrKey
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flatten the globals passed to `makeScratchConfig` into an object suitable for ESLint's `globals` option.
|
* Base rules recommended when type information is not available.
|
||||||
* @param {GlobalsObjOrKey | GlobalsObjOrKey[]} [globalsIn] The globals to flatten.
|
* These rules are also safe to use when type information is available.
|
||||||
* @returns {Globals|undefined} Flattened globals object for ESLint.
|
|
||||||
*/
|
*/
|
||||||
const flattenGlobals = globalsIn => {
|
const typeFreeRules = tseslint.config(
|
||||||
if (!globalsIn) return
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
tseslint.configs.stylistic,
|
||||||
|
|
||||||
/**
|
// eslint-plugin-formatjs
|
||||||
*
|
{
|
||||||
* @param {Globals} globalsAcc Globals accumulator.
|
plugins: {
|
||||||
* @param {GlobalsObjOrKey} objOrKey A globals object or key to add to the accumulator.
|
formatjs,
|
||||||
* @returns {Globals} The accumulator after adding the current globals object or key.
|
},
|
||||||
*/
|
rules: {
|
||||||
const globalsReducer = (globalsAcc, objOrKey) => {
|
'formatjs/no-offset': ['error'],
|
||||||
if (typeof objOrKey === 'string') {
|
},
|
||||||
const globalsForKey = globals[objOrKey]
|
},
|
||||||
if (!globalsForKey) {
|
// eslint-plugin-html
|
||||||
throw new Error(`Invalid globals name. Not a key from the globals package: ${objOrKey}`)
|
{
|
||||||
}
|
files: htmlGlobs,
|
||||||
Object.assign(globalsAcc, globalsForKey)
|
plugins: { html },
|
||||||
} else {
|
settings: {
|
||||||
Object.assign(globalsAcc, objOrKey)
|
'html/html-extensions': htmlExtensions,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
// eslint-plugin-import
|
||||||
|
{
|
||||||
|
plugins: importPlugin.flatConfigs.recommended.plugins,
|
||||||
|
rules: {
|
||||||
|
'import/no-duplicates': 'error', // Forbid duplicate imports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-plugin-jsdoc
|
||||||
|
jsdoc.configs['flat/recommended-error'],
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
||||||
|
extends: [jsdoc.configs['flat/recommended-typescript-error']],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// If JSDoc comments are present, they must be informative (non-trivial).
|
||||||
|
// For example, the description "The foo." on a variable called "foo" is not informative.
|
||||||
|
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/informative-docs.md
|
||||||
|
'jsdoc/informative-docs': ['error'],
|
||||||
|
|
||||||
return globalsAcc
|
// Don't require JSDoc comments. Library authors should consider turning this on for external interfaces.
|
||||||
}
|
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-jsdoc.md
|
||||||
|
'jsdoc/require-jsdoc': ['off'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-plugin-jsx-a11y
|
||||||
|
jsxA11y.flatConfigs.recommended,
|
||||||
|
// eslint-plugin-markdown
|
||||||
|
markdown.configs.recommended,
|
||||||
|
// eslint-plugin-react
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-danger.md
|
||||||
|
'react/no-danger': ['error'],
|
||||||
|
|
||||||
if (Array.isArray(globalsIn)) {
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md
|
||||||
return globalsIn.reduce(globalsReducer, {})
|
'react/self-closing-comp': ['error'],
|
||||||
}
|
|
||||||
|
|
||||||
return globalsReducer({}, globalsIn)
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md
|
||||||
}
|
'react/jsx-boolean-value': ['error', 'never'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-closing-bracket-location.md
|
||||||
|
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md
|
||||||
|
'react/jsx-curly-spacing': ['error'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-equals-spacing.md
|
||||||
|
'react/jsx-equals-spacing': ['error'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md
|
||||||
|
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-first-prop-new-line.md
|
||||||
|
'react/jsx-first-prop-new-line': ['error', 'multiline'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-handler-names.md
|
||||||
|
'react/jsx-handler-names': ['error', { checkLocalVariables: true, eventHandlerPrefix: false }],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-indent.md
|
||||||
|
'react/jsx-indent': ['error'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true, allowArrowFunctions: true }],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-pascal-case.md
|
||||||
|
'react/jsx-pascal-case': ['error'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-tag-spacing.md
|
||||||
|
'react/jsx-tag-spacing': ['error'],
|
||||||
|
|
||||||
|
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-wrap-multilines.md
|
||||||
|
'react/jsx-wrap-multilines': ['error'],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-plugin-react-hooks
|
||||||
|
{
|
||||||
|
extends: [reactHooks.configs['recommended-latest']],
|
||||||
|
rules: {
|
||||||
|
// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md#advanced-configuration
|
||||||
|
'react-hooks/exhaustive-deps': ['error', { additionalHooks: '^useAsync$' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// typescript-eslint
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// https://typescript-eslint.io/rules/no-non-null-asserted-nullish-coalescing/
|
||||||
|
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': ['error'],
|
||||||
|
|
||||||
|
// https://typescript-eslint.io/rules/no-useless-constructor/
|
||||||
|
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||||
|
|
||||||
|
// https://typescript-eslint.io/rules/no-non-null-assertion
|
||||||
|
'@typescript-eslint/no-non-null-assertion': ['error'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// @eslint-community/eslint-plugin-eslint-comments
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
// @ts-expect-error This plugin's recommended rules don't quite match the type `tseslint.config` expects.
|
||||||
|
eslintComments.recommended,
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
// require a description for eslint control comments other than `eslint-enable`
|
||||||
|
'@eslint-community/eslint-comments/require-description': ['error', { ignore: ['eslint-enable'] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// @eslint/js
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// https://eslint.org/docs/latest/rules/arrow-body-style
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/no-duplicate-imports
|
||||||
|
'no-duplicate-imports': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/no-template-curly-in-string
|
||||||
|
'no-template-curly-in-string': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/no-useless-computed-key
|
||||||
|
'no-useless-computed-key': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/no-useless-rename
|
||||||
|
'no-useless-rename': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/prefer-arrow-callback
|
||||||
|
'prefer-arrow-callback': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/prefer-const#destructuring
|
||||||
|
'prefer-const': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/prefer-spread
|
||||||
|
'prefer-spread': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/require-atomic-updates
|
||||||
|
'require-atomic-updates': ['error'],
|
||||||
|
|
||||||
|
// https://eslint.org/docs/latest/rules/symbol-description
|
||||||
|
'symbol-description': ['error'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an ESLint configuration for Scratch style.
|
* Additional rules recommended when information is available.
|
||||||
* Supports JavaScript, TypeScript, and React (JSX/TSX) files.
|
* These rules require additional configuration.
|
||||||
* Setting `tsconfigRootDir` enables type-aware rules, some of which apply even in JavaScript files.
|
* @see https://typescript-eslint.io/getting-started/typed-linting/
|
||||||
* @param {object} options Configuration options
|
|
||||||
* @param {string} [options.tsconfigRootDir] Enable type checking by setting the root TypeScript config directory.
|
|
||||||
* @param {GlobalsObjOrKey | GlobalsObjOrKey[]} [options.globals] Globals to provide to ESLint.
|
|
||||||
* This can be expressed as:
|
|
||||||
* - a single string, such as `'browser'`, corresponding to a key in the `globals` package.
|
|
||||||
* - a single object as described in the "Specifying Globals" section of the ESLint documentation:
|
|
||||||
* https://eslint.org/docs/latest/use/configure/language-options#using-configuration-files
|
|
||||||
* - an array of zero or more elements, each of which can be either of the above
|
|
||||||
* @param {import('typescript-eslint').InfiniteDepthConfigWithExtends[]} moreConfigs Additional ESLint configurations
|
|
||||||
* to merge with the base configuration.
|
|
||||||
* @example
|
|
||||||
* // eslint.config.mjs
|
|
||||||
* export default makeScratchConfig({tsconfigRootDir: import.meta.dirname, globals: 'node'})
|
|
||||||
* @example
|
|
||||||
* // eslint.config.mjs
|
|
||||||
* export default [
|
|
||||||
* ...makeScratchConfig({tsconfigRootDir: import.meta.dirname, globals: 'browser'}),
|
|
||||||
* {
|
|
||||||
* // customization
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* @returns {import('typescript-eslint').ConfigArray} An ESLint configuration array.
|
|
||||||
*/
|
*/
|
||||||
const makeEslintConfig = ({ tsconfigRootDir, globals: globalsIn } = {}, ...moreConfigs) => {
|
const typeCheckedRules = tseslint.config(
|
||||||
const flattenedGlobals = flattenGlobals(globalsIn)
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// https://typescript-eslint.io/rules/no-unnecessary-condition/
|
||||||
|
'@typescript-eslint/no-unnecessary-condition': ['error'],
|
||||||
|
|
||||||
return tseslint.config(
|
// https://typescript-eslint.io/rules/require-await/
|
||||||
// Start with recommended rules from ESLint and TypeScript ESLint.
|
'@typescript-eslint/require-await': ['error'],
|
||||||
{
|
|
||||||
extends: [eslint.configs.recommended, tseslint.configs.recommended, tseslint.configs.stylistic],
|
|
||||||
languageOptions: {
|
|
||||||
...(globalsIn ? { globals: flattenedGlobals } : {}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// eslint-plugin-formatjs
|
},
|
||||||
{
|
)
|
||||||
plugins: {
|
|
||||||
formatjs,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'formatjs/no-offset': ['error'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eslint-plugin-html
|
|
||||||
{
|
|
||||||
files: htmlGlobs,
|
|
||||||
plugins: { html },
|
|
||||||
settings: {
|
|
||||||
'html/html-extensions': htmlExtensions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eslint-plugin-import
|
|
||||||
{
|
|
||||||
plugins: importPlugin.flatConfigs.recommended.plugins,
|
|
||||||
rules: {
|
|
||||||
'import/no-duplicates': 'error', // Forbid duplicate imports
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eslint-plugin-jsdoc
|
|
||||||
jsdoc.configs['flat/recommended-error'],
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
|
||||||
extends: [jsdoc.configs['flat/recommended-typescript-error']],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// If JSDoc comments are present, they must be informative (non-trivial).
|
|
||||||
// For example, the description "The foo." on a variable called "foo" is not informative.
|
|
||||||
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/informative-docs.md
|
|
||||||
'jsdoc/informative-docs': ['error'],
|
|
||||||
|
|
||||||
// Don't require JSDoc comments. Library authors should consider turning this on for external interfaces.
|
/**
|
||||||
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-jsdoc.md
|
* Scratch's recommended configuration when type information is not available.
|
||||||
'jsdoc/require-jsdoc': ['off'],
|
*/
|
||||||
},
|
const recommendedTypeFree = tseslint.config(typeFreeRules, eslintConfigPrettier)
|
||||||
},
|
|
||||||
// eslint-plugin-jsx-a11y
|
|
||||||
jsxA11y.flatConfigs.recommended,
|
|
||||||
// eslint-plugin-markdown
|
|
||||||
markdown.configs.recommended,
|
|
||||||
// eslint-plugin-react
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
react,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-danger.md
|
|
||||||
'react/no-danger': ['error'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md
|
/**
|
||||||
'react/self-closing-comp': ['error'],
|
* Scratch's recommended configuration when type information is available.
|
||||||
|
* These rules require additional configuration.
|
||||||
|
* @see https://typescript-eslint.io/getting-started/typed-linting/
|
||||||
|
*/
|
||||||
|
const recommendedTypeChecked = tseslint.config(typeFreeRules, typeCheckedRules, eslintConfigPrettier)
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md
|
/**
|
||||||
'react/jsx-boolean-value': ['error', 'never'],
|
* Scratch's recommended configuration for general use.
|
||||||
|
* Type-checked rules are enabled for files with known TypeScript extensions.
|
||||||
|
* If your project includes such files, you must include additional configuration.
|
||||||
|
* @see https://typescript-eslint.io/getting-started/typed-linting/
|
||||||
|
*/
|
||||||
|
const recommended = tseslint.config(
|
||||||
|
typeFreeRules,
|
||||||
|
{
|
||||||
|
files: typeScriptExtensions,
|
||||||
|
extends: [typeCheckedRules],
|
||||||
|
},
|
||||||
|
eslintConfigPrettier,
|
||||||
|
)
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-closing-bracket-location.md
|
// Helper to get type hints while conveniently merging and extending configurations
|
||||||
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
export { config } from 'typescript-eslint'
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md
|
// Our exported configurations
|
||||||
'react/jsx-curly-spacing': ['error'],
|
export { recommended, recommendedTypeChecked, recommendedTypeFree, legacy }
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-equals-spacing.md
|
|
||||||
'react/jsx-equals-spacing': ['error'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md
|
|
||||||
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-first-prop-new-line.md
|
|
||||||
'react/jsx-first-prop-new-line': ['error', 'multiline'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-handler-names.md
|
|
||||||
'react/jsx-handler-names': ['error', { checkLocalVariables: true, eventHandlerPrefix: false }],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-indent.md
|
|
||||||
'react/jsx-indent': ['error'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md
|
|
||||||
'react/jsx-no-bind': ['error', { ignoreRefs: true, allowArrowFunctions: true }],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-pascal-case.md
|
|
||||||
'react/jsx-pascal-case': ['error'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-tag-spacing.md
|
|
||||||
'react/jsx-tag-spacing': ['error'],
|
|
||||||
|
|
||||||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-wrap-multilines.md
|
|
||||||
'react/jsx-wrap-multilines': ['error'],
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eslint-plugin-react-hooks
|
|
||||||
{
|
|
||||||
extends: [reactHooks.configs['recommended-latest']],
|
|
||||||
rules: {
|
|
||||||
// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md#advanced-configuration
|
|
||||||
'react-hooks/exhaustive-deps': ['error', { additionalHooks: '^useAsync$' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// typescript-eslint
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// https://typescript-eslint.io/rules/no-non-null-asserted-nullish-coalescing/
|
|
||||||
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': ['error'],
|
|
||||||
|
|
||||||
// https://typescript-eslint.io/rules/no-useless-constructor/
|
|
||||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
|
||||||
|
|
||||||
// https://typescript-eslint.io/rules/no-non-null-assertion
|
|
||||||
'@typescript-eslint/no-non-null-assertion': ['error'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tsconfigRootDir
|
|
||||||
? {
|
|
||||||
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
|
||||||
extends: [tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// https://typescript-eslint.io/rules/no-unnecessary-condition/
|
|
||||||
'@typescript-eslint/no-unnecessary-condition': ['error'],
|
|
||||||
|
|
||||||
// https://typescript-eslint.io/rules/require-await/
|
|
||||||
'@typescript-eslint/require-await': ['error'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
// @eslint-community/eslint-plugin-eslint-comments
|
|
||||||
{
|
|
||||||
extends: [
|
|
||||||
// @ts-expect-error This plugin's recommended rules don't quite match the type `tseslint.config` expects.
|
|
||||||
eslintComments.recommended,
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
// require a description for eslint control comments other than `eslint-enable`
|
|
||||||
'@eslint-community/eslint-comments/require-description': ['error', { ignore: ['eslint-enable'] }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// @eslint/js
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// https://eslint.org/docs/latest/rules/arrow-body-style
|
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/no-duplicate-imports
|
|
||||||
'no-duplicate-imports': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/no-template-curly-in-string
|
|
||||||
'no-template-curly-in-string': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/no-useless-computed-key
|
|
||||||
'no-useless-computed-key': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/no-useless-rename
|
|
||||||
'no-useless-rename': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/prefer-arrow-callback
|
|
||||||
'prefer-arrow-callback': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/prefer-const#destructuring
|
|
||||||
'prefer-const': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/prefer-spread
|
|
||||||
'prefer-spread': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/require-atomic-updates
|
|
||||||
'require-atomic-updates': ['error'],
|
|
||||||
|
|
||||||
// https://eslint.org/docs/latest/rules/symbol-description
|
|
||||||
'symbol-description': ['error'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...moreConfigs,
|
|
||||||
// Keep `eslintConfigPrettier` last to turn off rules that conflict with Prettier
|
|
||||||
eslintConfigPrettier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { makeEslintConfig }
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { makeEslintConfig } from './eslint.mjs'
|
import * as eslintConfigScratch from './eslint.mjs'
|
||||||
import { makePrettierConfig } from './prettier.mjs'
|
import * as prettierConfigScratch from './prettier.mjs'
|
||||||
|
|
||||||
export { makeEslintConfig, makePrettierConfig }
|
export { eslintConfigScratch, prettierConfigScratch }
|
||||||
|
|
|
@ -4,7 +4,7 @@ import sortImports from '@trivago/prettier-plugin-sort-imports'
|
||||||
* @see https://prettier.io/docs/configuration
|
* @see https://prettier.io/docs/configuration
|
||||||
* @type {import("prettier").Config}
|
* @type {import("prettier").Config}
|
||||||
*/
|
*/
|
||||||
const prettierConfig = {
|
const recommended = {
|
||||||
// #region Prettier
|
// #region Prettier
|
||||||
arrowParens: 'avoid',
|
arrowParens: 'avoid',
|
||||||
bracketSameLine: false,
|
bracketSameLine: false,
|
||||||
|
@ -22,9 +22,4 @@ const prettierConfig = {
|
||||||
// #endregion @trivago/prettier-plugin-sort-imports
|
// #endregion @trivago/prettier-plugin-sort-imports
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export { recommended }
|
||||||
* @returns {import("prettier").Config} A Prettier configuration for Scratch style.
|
|
||||||
*/
|
|
||||||
const makePrettierConfig = () => prettierConfig
|
|
||||||
|
|
||||||
export { makePrettierConfig }
|
|
||||||
|
|
|
@ -3,13 +3,6 @@
|
||||||
"version": "10.0.12",
|
"version": "10.0.12",
|
||||||
"description": "Shareable ESLint config for Scratch",
|
"description": "Shareable ESLint config for Scratch",
|
||||||
"main": "./lib/index.mjs",
|
"main": "./lib/index.mjs",
|
||||||
"exports": {
|
|
||||||
".": "./lib/index.mjs",
|
|
||||||
"./legacy": "./lib/legacy/index.mjs",
|
|
||||||
"./legacy/es6": "./lib/legacy/web.mjs",
|
|
||||||
"./legacy/node": "./lib/legacy/node.mjs",
|
|
||||||
"./legacy/react": "./lib/legacy/react.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"format": "prettier --write . && eslint --fix",
|
"format": "prettier --write . && eslint --fix",
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { makePrettierConfig } from './lib/prettier.mjs'
|
import { prettierConfigScratch } from './lib/index.mjs'
|
||||||
|
|
||||||
export default makePrettierConfig()
|
export default prettierConfigScratch.recommended
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue