import eslintConfigPrettier from 'eslint-config-prettier/flat' import formatjs from 'eslint-plugin-formatjs' import html from 'eslint-plugin-html' import htmlSettings from 'eslint-plugin-html/src/settings.js' import importPlugin from 'eslint-plugin-import' import jsdoc from 'eslint-plugin-jsdoc' import jsxA11y from 'eslint-plugin-jsx-a11y' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' import tseslint from 'typescript-eslint' import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs' import eslint from '@eslint/js' import markdown from '@eslint/markdown' 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, } // WARNING: eslint rules from `typescript-eslint`, even the "untyped" rules, assume that your code will be run through // `tsc` or equivalent for type checking. Using any rule set from `typescript-eslint` will, for example, turn off the // `no-undef` rule. That makes sense if you'll use TypeScript to catch undefined globals, but it could be dangerous // for plain JavaScript. // More information here: https://github.com/typescript-eslint/typescript-eslint/issues/8825#issuecomment-2033315610 /** * Convert an array of file extensions to an array of globs * @param {string[]} extArray - an array of file extensions, like `.foo` * @returns {string[]} an array of globs, like `** /*.foo` (without the space) */ const extArrayToGlobArray = extArray => extArray.map(ext => `**/*${ext}`) // See https://www.npmjs.com/package/eslint-plugin-html#user-content-settings const htmlSettingsDefault = htmlSettings.getSettings({}) const fileExtensions = (x => { x.allScript = [...x.javaScript, ...x.typeScript] return x })({ html: /** @type {string[]} */ (htmlSettingsDefault.htmlExtensions), javaScript: ['.js', '.jsx', '.mjs', '.cjs'], markdown: ['.md'], typeScript: ['.ts', '.tsx', '.mts', '.cts'], react: ['.jsx', '.tsx'], xml: /** @type {string[]} */ (htmlSettingsDefault.xmlExtensions), }) // This explicitly lists each entry so that we can get unused warnings const fileGlobs = { allScript: extArrayToGlobArray(fileExtensions.allScript), html: extArrayToGlobArray(fileExtensions.html), javaScript: extArrayToGlobArray(fileExtensions.javaScript), markdown: extArrayToGlobArray(fileExtensions.markdown), react: extArrayToGlobArray(fileExtensions.react), typeScript: extArrayToGlobArray(fileExtensions.typeScript), xml: extArrayToGlobArray(fileExtensions.xml), } /** * Rules for specific file types outside of the core JS/TS rule sets. */ const miscFileRules = tseslint.config([ // eslint-plugin-html { name: 'scratch/miscFileRules[eslint-plugin-html]', files: [...fileGlobs.html, ...fileGlobs.xml], plugins: { html }, settings: { 'html/html-extensions': fileExtensions.html, 'xml/xml-extensions': fileExtensions.xml, }, }, // eslint-plugin-markdown { name: 'scratch/miscFileRules[eslint-plugin-markdown]', files: fileGlobs.markdown, extends: [markdown.configs.recommended], language: 'markdown/gfm', // Github Flavored Markdown }, markdown.configs.processor, // Process script blocks inside Markdown files ]) /** * Rules recommended for all script files, whether or not type information is available or checked. */ const allScriptRules = tseslint.config([ // eslint-plugin-formatjs { name: 'scratch/allScriptRules[eslint-plugin-formatjs]', plugins: { formatjs, }, rules: { 'formatjs/no-offset': ['error'], }, }, // eslint-plugin-import { name: 'scratch/allScriptRules[eslint-plugin-import]', plugins: importPlugin.flatConfigs.recommended.plugins, rules: { 'import/no-duplicates': 'error', // Forbid duplicate imports }, }, // eslint-plugin-jsx-a11y { name: 'scratch/allScriptRules[eslint-plugin-jsx-a11y]', files: fileGlobs.react, extends: [jsxA11y.flatConfigs.recommended], }, // eslint-plugin-react { name: 'scratch/allScriptRules[eslint-plugin-react]', files: fileGlobs.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'], // 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 { name: 'scratch/allScriptRules[eslint-plugin-react-hooks]', files: fileGlobs.react, 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$' }], }, }, // @eslint-community/eslint-plugin-eslint-comments { name: 'scratch/allScriptRules[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 { name: 'scratch/allScriptRules[@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'], }, }, ]) /** * Additional rules recommended when type information is not available or checked. */ const typeFreeRules = tseslint.config([ { name: 'scratch/typeFreeRules[base]', extends: [eslint.configs.recommended], }, ...allScriptRules, { name: 'scratch/typeFreeRules[eslint-plugin-jsdoc]', extends: [jsdoc.configs['flat/recommended-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 'jsdoc/require-jsdoc': ['off'], }, }, ]) /** * Rules recommended when type information is available and checked. This configuration turns off some rules with the * assumption that other software, such as TypeScript, will flag those problems. For example, the `no-undef` rule is * disabled in this configuration. These rules include `allScriptRules`. * These rules require additional configuration. * @see https://typescript-eslint.io/getting-started/typed-linting/ */ const typeCheckedRules = tseslint.config([ { name: 'scratch/typeCheckedRules[base]', extends: [ eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, ], 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'], }, }, ...allScriptRules, { name: 'scratch/typeCheckedRules[eslint-plugin-jsdoc][1]', extends: [jsdoc.configs['flat/recommended-error']], }, { name: 'scratch/typeCheckedRules[eslint-plugin-jsdoc][2]', extends: [jsdoc.configs['flat/recommended-typescript-error']], }, { name: 'scratch/typeCheckedRules[eslint-plugin-jsdoc][3]', 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 'jsdoc/require-jsdoc': ['off'], }, }, // typescript-eslint { name: 'scratch/typeCheckedRules[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'], }, }, ]) /** * Scratch's recommended configuration when type information is not available. */ const recommendedTypeFree = tseslint.config(typeFreeRules, eslintConfigPrettier) /** * Scratch's recommended configuration when type information is available. * These rules require additional configuration. * WARNING: These rules do not specify the `files` property. * @see https://typescript-eslint.io/getting-started/typed-linting/ */ const recommendedTypeChecked = tseslint.config(typeCheckedRules, eslintConfigPrettier) /** * 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( { name: 'scratch/recommended', }, { files: fileGlobs.allScript, extends: [typeFreeRules], }, { files: fileGlobs.typeScript, extends: [typeCheckedRules], languageOptions: { parserOptions: { projectService: true, }, }, }, miscFileRules, eslintConfigPrettier, ) // Helper to get type hints while conveniently merging and extending configurations export { config } from 'typescript-eslint' // Our exported configurations export { recommended, recommendedTypeChecked, recommendedTypeFree, miscFileRules, legacy }