diff --git a/eslint.config.mjs b/eslint.config.mjs index b73cc2b8..df9a7bc1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,8 +41,15 @@ const config = tseslint.config( }, rules: { + 'import-x/extensions': ['error', 'ignorePackages'], 'import-x/no-dynamic-require': 'off', 'import-x/no-nodejs-modules': 'off', + 'import-x/no-useless-path-segments': [ + 'error', + { + noUselessIndex: false, + }, + ], 'jsdoc/check-tag-names': 'off', 'jsdoc/no-types': 'off', 'n/global-require': 'off', diff --git a/packages/base/src/index.mjs b/packages/base/src/index.mjs index 98480cf9..aaa00a74 100644 --- a/packages/base/src/index.mjs +++ b/packages/base/src/index.mjs @@ -4,11 +4,13 @@ import js from '@eslint/js'; import importX from 'eslint-plugin-import-x'; import jsdoc from 'eslint-plugin-jsdoc'; import prettier from 'eslint-plugin-prettier/recommended'; -// @ts-expect-error - `eslint-plugin-promise` doesn't have TypeScript types. +// @ts-ignore - `eslint-plugin-promise` doesn't have TypeScript types. import promise from 'eslint-plugin-promise'; import globals from 'globals'; import { createRequire } from 'module'; +import { createConfig } from './utils.mjs'; + // TODO: Use import attributes when ESLint supports them. const customRequire = createRequire(import.meta.url); const environmentRules = customRequire('./environment.json'); @@ -16,406 +18,408 @@ const environmentRules = customRequire('./environment.json'); /** * @type {import('eslint').Linter.Config[]} */ -const rules = [ - // Recommended ESLint configuration. - js.configs.recommended, +const rules = createConfig({ + name: '@metamask/eslint-config', - importX.flatConfigs.recommended, - jsdoc.configs['flat/recommended-error'], - prettier, - promise.configs['flat/recommended'], + extends: [ + // Recommended ESLint configuration. + js.configs.recommended, - { - name: '@metamask/eslint-config', + // Third-party plugin configurations. + importX.flatConfigs.recommended, + jsdoc.configs['flat/recommended-error'], + prettier, + promise.configs['flat/recommended'], + ], - languageOptions: { - // The `esXXXX` option under `env` is supposed to set the correct - // `ecmaVersion` option here, but we've had issues with it being - // overridden in the past and therefore set it explicitly. + languageOptions: { + // The `esXXXX` option under `env` is supposed to set the correct + // `ecmaVersion` option here, but we've had issues with it being + // overridden in the past and therefore set it explicitly. + ecmaVersion: 2022, + parserOptions: { ecmaVersion: 2022, - parserOptions: { - ecmaVersion: 2022, - }, + }, - // We want to default to 'script' and only use 'module' explicitly. - sourceType: 'script', + // We want to default to 'script' and only use 'module' explicitly. + sourceType: 'script', - globals: { - ...globals.es2022, - ...globals['shared-node-browser'], - }, + globals: { + ...globals.es2022, + ...globals['shared-node-browser'], }, + }, - rules: { - ...environmentRules, + rules: { + ...environmentRules, - /* Prettier rules */ - 'prettier/prettier': [ - 'error', - { - // All of these are defaults except singleQuote and endOfLine, but we specify them - // for explicitness - endOfLine: 'auto', - quoteProps: 'as-needed', - singleQuote: true, - tabWidth: 2, - trailingComma: 'all', - }, - { - // Allow consumers to override this Prettier config. - // This is the default, but we specify it for the sake of clarity. - usePrettierrc: true, - }, - ], + /* Prettier rules */ + 'prettier/prettier': [ + 'error', + { + // All of these are defaults except singleQuote and endOfLine, but we specify them + // for explicitness + endOfLine: 'auto', + quoteProps: 'as-needed', + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + }, + { + // Allow consumers to override this Prettier config. + // This is the default, but we specify it for the sake of clarity. + usePrettierrc: true, + }, + ], - curly: ['error', 'all'], - 'no-tabs': 'error', + curly: ['error', 'all'], + 'no-tabs': 'error', - /* Core rules */ - 'accessor-pairs': 'error', - 'array-callback-return': 'error', - 'block-scoped-var': 'error', - camelcase: [ - 'error', - { - properties: 'never', - allow: ['^UNSAFE_'], - }, - ], - 'consistent-return': 'error', - 'consistent-this': ['error', 'self'], - 'default-case': 'error', - 'default-param-last': 'error', - 'dot-notation': 'error', - eqeqeq: ['error', 'allow-null'], - 'func-name-matching': 'error', - 'grouped-accessor-pairs': 'error', - 'guard-for-in': 'error', - 'id-denylist': [ - // This sets this rule to 'error', the rest are the forbidden IDs. - 'error', - // These are basically all useless contractions. - 'buf', - 'cat', - 'err', - 'cb', - 'cfg', - 'hex', - 'int', - 'msg', - 'num', - 'opt', - 'sig', - ], - 'id-length': [ - 'error', - { - min: 2, - properties: 'never', - exceptionPatterns: ['_', 'a', 'b', 'i', 'j', 'k'], - }, - ], - 'lines-between-class-members': 'error', - 'new-cap': [ - 'error', - { - newIsCap: true, - capIsNew: false, - }, - ], - 'no-alert': 'error', - 'no-array-constructor': 'error', - 'no-bitwise': 'error', - 'no-buffer-constructor': 'error', - 'no-caller': 'error', - 'no-constructor-return': 'error', - 'no-div-regex': 'error', - 'no-else-return': 'error', - 'no-empty-function': 'error', - 'no-eq-null': 'error', - 'no-eval': 'error', - 'no-extend-native': 'error', - 'no-extra-bind': 'error', - 'no-extra-label': 'error', - 'no-implicit-coercion': 'error', - 'no-implicit-globals': 'off', - 'no-implied-eval': 'error', - 'no-inner-declarations': ['error', 'functions'], - 'no-invalid-this': 'error', - 'no-iterator': 'error', - 'no-label-var': 'error', - 'no-labels': [ - 'error', - { - allowLoop: false, - allowSwitch: false, - }, - ], - 'no-lone-blocks': 'error', - 'no-lonely-if': 'error', - 'no-loop-func': 'error', - 'no-multi-assign': 'error', - 'no-multi-str': 'error', - 'no-native-reassign': 'error', - 'no-negated-condition': 'error', - 'no-negated-in-lhs': 'error', - 'no-nested-ternary': 'error', - 'no-new': 'error', - 'no-new-func': 'error', - 'no-new-object': 'error', - 'no-new-wrappers': 'error', - 'no-octal-escape': 'error', - 'no-param-reassign': 'error', - 'no-plusplus': [ - 'error', - { - allowForLoopAfterthoughts: true, - }, - ], - 'no-proto': 'error', - 'no-restricted-syntax': [ - 'error', - { - selector: 'WithStatement', - message: 'With statements are not allowed', - }, - { - selector: `BinaryExpression[operator='in']`, - message: 'The "in" operator is not allowed', - }, - // Sequence expressions have potential gotchas with Prettier, and are also - // weird! - { - selector: 'SequenceExpression', - message: 'Sequence expressions are not allowed', - }, - ], - 'no-return-assign': ['error', 'except-parens'], - 'no-script-url': 'error', - 'no-self-compare': 'error', - 'no-shadow': ['error', { builtinGlobals: true }], - 'no-template-curly-in-string': 'error', - 'no-throw-literal': 'error', - 'no-undef-init': 'error', - 'no-unmodified-loop-condition': 'error', - 'no-unneeded-ternary': [ - 'error', - { - defaultAssignment: false, - }, - ], - 'no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTernary: true, - }, - ], - 'no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'all', - argsIgnorePattern: '[_]+', - ignoreRestSiblings: true, - }, - ], - 'no-use-before-define': [ - 'error', - { - functions: false, - }, - ], - 'no-useless-call': 'error', - 'no-useless-computed-key': 'error', - 'no-useless-concat': 'error', - 'no-useless-constructor': 'error', - 'no-useless-rename': 'error', - 'no-useless-return': 'error', - 'no-var': 'error', - 'no-void': 'error', - 'object-shorthand': 'error', - 'one-var': [ - 'error', - { - initialized: 'never', - }, - ], - 'operator-assignment': 'error', - 'padding-line-between-statements': [ - 'error', - { - blankLine: 'always', - prev: 'directive', - next: '*', - }, - { - blankLine: 'any', - prev: 'directive', - next: 'directive', - }, - ], - 'prefer-const': 'error', - 'prefer-destructuring': [ - 'error', - { - VariableDeclarator: { - array: false, - object: true, - }, - AssignmentExpression: { - array: false, - object: false, - }, - }, - { - enforceForRenamedProperties: false, + /* Core rules */ + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'block-scoped-var': 'error', + camelcase: [ + 'error', + { + properties: 'never', + allow: ['^UNSAFE_'], + }, + ], + 'consistent-return': 'error', + 'consistent-this': ['error', 'self'], + 'default-case': 'error', + 'default-param-last': 'error', + 'dot-notation': 'error', + eqeqeq: ['error', 'allow-null'], + 'func-name-matching': 'error', + 'grouped-accessor-pairs': 'error', + 'guard-for-in': 'error', + 'id-denylist': [ + // This sets this rule to 'error', the rest are the forbidden IDs. + 'error', + // These are basically all useless contractions. + 'buf', + 'cat', + 'err', + 'cb', + 'cfg', + 'hex', + 'int', + 'msg', + 'num', + 'opt', + 'sig', + ], + 'id-length': [ + 'error', + { + min: 2, + properties: 'never', + exceptionPatterns: ['_', 'a', 'b', 'i', 'j', 'k'], + }, + ], + 'lines-between-class-members': 'error', + 'new-cap': [ + 'error', + { + newIsCap: true, + capIsNew: false, + }, + ], + 'no-alert': 'error', + 'no-array-constructor': 'error', + 'no-bitwise': 'error', + 'no-buffer-constructor': 'error', + 'no-caller': 'error', + 'no-constructor-return': 'error', + 'no-div-regex': 'error', + 'no-else-return': 'error', + 'no-empty-function': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'off', + 'no-implied-eval': 'error', + 'no-inner-declarations': ['error', 'functions'], + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': [ + 'error', + { + allowLoop: false, + allowSwitch: false, + }, + ], + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-multi-assign': 'error', + 'no-multi-str': 'error', + 'no-native-reassign': 'error', + 'no-negated-condition': 'error', + 'no-negated-in-lhs': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'error', + 'no-plusplus': [ + 'error', + { + allowForLoopAfterthoughts: true, + }, + ], + 'no-proto': 'error', + 'no-restricted-syntax': [ + 'error', + { + selector: 'WithStatement', + message: 'With statements are not allowed', + }, + { + selector: `BinaryExpression[operator='in']`, + message: 'The "in" operator is not allowed', + }, + // Sequence expressions have potential gotchas with Prettier, and are also + // weird! + { + selector: 'SequenceExpression', + message: 'Sequence expressions are not allowed', + }, + ], + 'no-return-assign': ['error', 'except-parens'], + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-shadow': ['error', { builtinGlobals: true }], + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-unmodified-loop-condition': 'error', + 'no-unneeded-ternary': [ + 'error', + { + defaultAssignment: false, + }, + ], + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'all', + argsIgnorePattern: '[_]+', + ignoreRestSiblings: true, + }, + ], + 'no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'object-shorthand': 'error', + 'one-var': [ + 'error', + { + initialized: 'never', + }, + ], + 'operator-assignment': 'error', + 'padding-line-between-statements': [ + 'error', + { + blankLine: 'always', + prev: 'directive', + next: '*', + }, + { + blankLine: 'any', + prev: 'directive', + next: 'directive', + }, + ], + 'prefer-const': 'error', + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true, }, - ], - 'prefer-numeric-literals': 'error', - 'prefer-promise-reject-errors': 'error', - 'prefer-regex-literals': 'error', - 'prefer-rest-params': 'error', - 'prefer-spread': 'error', - 'prefer-template': 'error', - radix: 'error', - 'require-atomic-updates': 'error', - 'require-unicode-regexp': 'error', - 'spaced-comment': [ - 'error', - 'always', - { - markers: [ - 'global', - 'globals', - 'eslint', - 'eslint-disable', - '*package', - '!', - ',', - ], - exceptions: ['=', '-'], + AssignmentExpression: { + array: false, + object: false, }, - ], - 'symbol-description': 'error', - yoda: ['error', 'never'], + }, + { + enforceForRenamedProperties: false, + }, + ], + 'prefer-numeric-literals': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-regex-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + radix: 'error', + 'require-atomic-updates': 'error', + 'require-unicode-regexp': 'error', + 'spaced-comment': [ + 'error', + 'always', + { + markers: [ + 'global', + 'globals', + 'eslint', + 'eslint-disable', + '*package', + '!', + ',', + ], + exceptions: ['=', '-'], + }, + ], + 'symbol-description': 'error', + yoda: ['error', 'never'], - /* import plugin rules */ - 'import-x/extensions': [ - 'error', - 'never', - { - json: 'always', - }, - ], - 'import-x/first': 'error', - 'import-x/newline-after-import': 'error', - 'import-x/no-absolute-path': 'error', - 'import-x/no-amd': 'error', - 'import-x/no-anonymous-default-export': 'error', - 'import-x/no-duplicates': 'error', - 'import-x/no-dynamic-require': 'error', - 'import-x/no-extraneous-dependencies': 'error', - 'import-x/no-mutable-exports': 'error', - 'import-x/no-named-as-default': 'error', - 'import-x/no-named-as-default-member': 'error', - 'import-x/no-named-default': 'error', - 'import-x/no-nodejs-modules': 'error', - 'import-x/no-self-import': 'error', - 'import-x/no-unassigned-import': 'error', - 'import-x/no-unresolved': [ - 'error', - { - commonjs: true, - }, - ], - 'import-x/no-useless-path-segments': [ - 'error', - { - commonjs: true, - noUselessIndex: true, - }, - ], - 'import-x/no-webpack-loader-syntax': 'error', - 'import-x/order': [ - 'error', - { - // This means that there will always be a newline between the import - // groups as defined below. - 'newlines-between': 'always', + /* import plugin rules */ + 'import-x/extensions': [ + 'error', + 'never', + { + json: 'always', + }, + ], + 'import-x/first': 'error', + 'import-x/newline-after-import': 'error', + 'import-x/no-absolute-path': 'error', + 'import-x/no-amd': 'error', + 'import-x/no-anonymous-default-export': 'error', + 'import-x/no-duplicates': 'error', + 'import-x/no-dynamic-require': 'error', + 'import-x/no-extraneous-dependencies': 'error', + 'import-x/no-mutable-exports': 'error', + 'import-x/no-named-as-default': 'error', + 'import-x/no-named-as-default-member': 'error', + 'import-x/no-named-default': 'error', + 'import-x/no-nodejs-modules': 'error', + 'import-x/no-self-import': 'error', + 'import-x/no-unassigned-import': 'error', + 'import-x/no-unresolved': [ + 'error', + { + commonjs: true, + }, + ], + 'import-x/no-useless-path-segments': [ + 'error', + { + commonjs: true, + noUselessIndex: true, + }, + ], + 'import-x/no-webpack-loader-syntax': 'error', + 'import-x/order': [ + 'error', + { + // This means that there will always be a newline between the import + // groups as defined below. + 'newlines-between': 'always', - groups: [ - // "builtin" is Node.js modules that are built into the runtime, and - // "external" is everything else from node_modules. - ['builtin', 'external'], + groups: [ + // "builtin" is Node.js modules that are built into the runtime, and + // "external" is everything else from node_modules. + ['builtin', 'external'], - // "internal" is unused, but could be used for absolute imports from - // the project root. - ['internal', 'parent', 'sibling', 'index'], - ], + // "internal" is unused, but could be used for absolute imports from + // the project root. + ['internal', 'parent', 'sibling', 'index'], + ], - // Alphabetically sort the imports within each group. - alphabetize: { - order: 'asc', - caseInsensitive: true, - }, + // Alphabetically sort the imports within each group. + alphabetize: { + order: 'asc', + caseInsensitive: true, }, - ], - 'import-x/unambiguous': 'error', + }, + ], + 'import-x/unambiguous': 'error', - /* jsdoc plugin rules */ - 'jsdoc/check-access': 'error', - 'jsdoc/check-alignment': 'error', - 'jsdoc/check-line-alignment': 'error', - 'jsdoc/check-param-names': 'error', - 'jsdoc/check-property-names': 'error', - 'jsdoc/check-tag-names': 'error', - 'jsdoc/check-types': 'error', - 'jsdoc/check-values': 'error', - 'jsdoc/empty-tags': 'error', - 'jsdoc/implements-on-classes': 'error', - 'jsdoc/match-description': [ - 'error', - { tags: { param: true, returns: true } }, - ], - 'jsdoc/multiline-blocks': 'error', - 'jsdoc/no-bad-blocks': 'error', - 'jsdoc/no-defaults': 'error', - 'jsdoc/no-multi-asterisks': 'error', - 'jsdoc/require-asterisk-prefix': 'error', - 'jsdoc/require-description': 'error', - 'jsdoc/require-hyphen-before-param-description': [ - 'error', - 'always', - { tags: { returns: 'never', template: 'always', throws: 'never' } }, - ], - 'jsdoc/require-jsdoc': 'error', - 'jsdoc/require-param-name': 'error', - 'jsdoc/require-param': ['error', { unnamedRootBase: ['options'] }], - 'jsdoc/require-param-description': 'error', - 'jsdoc/require-param-type': 'error', - 'jsdoc/require-property': 'error', - 'jsdoc/require-property-description': 'error', - 'jsdoc/require-property-name': 'error', - 'jsdoc/require-property-type': 'error', - 'jsdoc/require-returns': 'error', - 'jsdoc/require-returns-check': 'error', - 'jsdoc/require-returns-description': 'error', - 'jsdoc/require-returns-type': 'error', - 'jsdoc/require-yields': 'error', - 'jsdoc/require-yields-check': 'error', - 'jsdoc/tag-lines': [ - 'error', - 'any', - { - startLines: 1, - }, - ], - 'jsdoc/valid-types': 'error', + /* jsdoc plugin rules */ + 'jsdoc/check-access': 'error', + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-line-alignment': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/check-types': 'error', + 'jsdoc/check-values': 'error', + 'jsdoc/empty-tags': 'error', + 'jsdoc/implements-on-classes': 'error', + 'jsdoc/match-description': [ + 'error', + { tags: { param: true, returns: true } }, + ], + 'jsdoc/multiline-blocks': 'error', + 'jsdoc/no-bad-blocks': 'error', + 'jsdoc/no-defaults': 'error', + 'jsdoc/no-multi-asterisks': 'error', + 'jsdoc/require-asterisk-prefix': 'error', + 'jsdoc/require-description': 'error', + 'jsdoc/require-hyphen-before-param-description': [ + 'error', + 'always', + { tags: { returns: 'never', template: 'always', throws: 'never' } }, + ], + 'jsdoc/require-jsdoc': 'error', + 'jsdoc/require-param-name': 'error', + 'jsdoc/require-param': ['error', { unnamedRootBase: ['options'] }], + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-param-type': 'error', + 'jsdoc/require-property': 'error', + 'jsdoc/require-property-description': 'error', + 'jsdoc/require-property-name': 'error', + 'jsdoc/require-property-type': 'error', + 'jsdoc/require-returns': 'error', + 'jsdoc/require-returns-check': 'error', + 'jsdoc/require-returns-description': 'error', + 'jsdoc/require-returns-type': 'error', + 'jsdoc/require-yields': 'error', + 'jsdoc/require-yields-check': 'error', + 'jsdoc/tag-lines': [ + 'error', + 'any', + { + startLines: 1, + }, + ], + 'jsdoc/valid-types': 'error', - // 'promise/no-multiple-resolved': 'error', - }, + // 'promise/no-multiple-resolved': 'error', }, -]; +}); +export { createConfig }; export default rules; diff --git a/packages/base/src/index.test.mjs b/packages/base/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/base/src/index.test.mjs +++ b/packages/base/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/base/src/utils.mjs b/packages/base/src/utils.mjs new file mode 100644 index 00000000..d0632646 --- /dev/null +++ b/packages/base/src/utils.mjs @@ -0,0 +1,105 @@ +/** + * @typedef {import('eslint').Linter.Config} Config + * @typedef {Config & { extends?: Config | Config[] | Config[][] }} ConfigWithExtends + */ + +/** + * Get an array from a value. If the value is already an array, it is returned + * as is. Otherwise, the value is wrapped in an array. + * + * @template Type + * @param {Type | Type[]} value - The value to convert to an array. + * @returns {Type[]} The value as an array. + */ +function getArray(value) { + return Array.isArray(value) ? value : [value]; +} + +/** + * Get a config object that extends another config object. + * + * @param {Config | Config[]} baseConfig - The base config object. + * @param {{ files?: string[]; ignores?: string[] }} extension - The extension + * object. + * @returns {Config | Config[]} The extended config object. + */ +function getExtendedConfig(baseConfig, extension) { + if (Array.isArray(baseConfig)) { + return baseConfig.map((base) => ({ ...base, ...extension })); + } + + return { ...baseConfig, ...extension }; +} + +/** + * Create a config object that extends other configs. + * + * ESLint 9 removed support for extending arrays of configs, so this function + * provides a workaround. It takes an array of config objects, where each object + * may have an `extends` property that is an array of other config objects. + * + * This function is inspired by the `config` function in the `typescript-eslint` + * package, but to avoid a dependency on that package, this function is + * implemented here. + * + * @param {ConfigWithExtends | ConfigWithExtends[]} configs - An array of config + * objects. + * @returns {Config[]} An array of config objects with all `extends` properties + * resolved. + * @example Basic usage. + * import { createConfig } from '@metamask/eslint-config'; + * import typescript from '@metamask/eslint-config-typescript'; + * + * const configs = createConfig([ + * { + * files: ['**\/*.ts'], + * extends: typescript, + * }, + * ]); + * + * export default configs; + * + * @example Multiple extends are supported as well. + * import { createConfig } from '@metamask/eslint-config'; + * import typescript from '@metamask/eslint-config-typescript'; + * import nodejs from '@metamask/eslint-config-nodejs'; + * + * const configs = createConfig([ + * { + * files: ['**\/*.ts'], + * extends: [typescript, nodejs], + * }, + * ]); + * + * export default configs; + */ +export function createConfig(configs) { + const configsArray = getArray(configs); + + return configsArray.flatMap((configWithExtends) => { + const { extends: extendsValue, ...originalConfig } = configWithExtends; + if (extendsValue === undefined) { + return originalConfig; + } + + const extension = { + ...(originalConfig.files && { files: originalConfig.files }), + ...(originalConfig.ignores && { ignores: originalConfig.ignores }), + }; + + if (Array.isArray(extendsValue)) { + if (extendsValue.length === 0) { + return originalConfig; + } + + return [ + ...extendsValue.flatMap((baseConfig) => + getExtendedConfig(baseConfig, extension), + ), + originalConfig, + ]; + } + + return [getExtendedConfig(extendsValue, extension), originalConfig]; + }); +} diff --git a/packages/base/src/utils.test.mjs b/packages/base/src/utils.test.mjs new file mode 100644 index 00000000..fb7b17cc --- /dev/null +++ b/packages/base/src/utils.test.mjs @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest'; + +import { createConfig } from './utils.mjs'; + +describe('createConfig', () => { + it('returns a config array for a single config', () => { + const configs = { files: ['**/*.js'] }; + + const result = createConfig(configs); + expect(result).toStrictEqual([configs]); + }); + + it('returns a config array for an array of configs', () => { + const configs = [{ files: ['**/*.js'] }, { ignores: ['node_modules'] }]; + + const result = createConfig(configs); + expect(result).toStrictEqual(configs); + }); + + it('adds the `files` value to extended configurations', () => { + const baseConfig = { rules: {} }; + const extendedConfig = { extends: [baseConfig], files: ['**/*.js'] }; + + const result = createConfig(extendedConfig); + expect(result).toStrictEqual([ + { files: ['**/*.js'], rules: {} }, + { files: ['**/*.js'] }, + ]); + }); + + it('adds the `ignore` value to extended configurations', () => { + const baseConfig = { files: ['**/*.js'] }; + const extendedConfig = { extends: [baseConfig], ignores: ['node_modules'] }; + + const result = createConfig(extendedConfig); + expect(result).toStrictEqual([ + { files: ['**/*.js'], ignores: ['node_modules'] }, + { ignores: ['node_modules'] }, + ]); + }); + + it('supports a config object as `extends` value', () => { + const baseConfig = { rules: {} }; + const extendedConfig = { extends: baseConfig, files: ['**/*.js'] }; + + const result = createConfig(extendedConfig); + expect(result).toStrictEqual([ + { files: ['**/*.js'], rules: {} }, + { files: ['**/*.js'] }, + ]); + }); + + it('supports a nested config array as `extends` value', () => { + const baseConfig = [ + { rules: { 'foo/bar': 'error' } }, + { languageOptions: {} }, + ]; + + const extendedConfig = { extends: [baseConfig], files: ['**/*.js'] }; + + const result = createConfig(extendedConfig); + expect(result).toStrictEqual([ + { files: ['**/*.js'], rules: { 'foo/bar': 'error' } }, + { files: ['**/*.js'], languageOptions: {} }, + { files: ['**/*.js'] }, + ]); + }); +}); diff --git a/packages/browser/src/index.test.mjs b/packages/browser/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/browser/src/index.test.mjs +++ b/packages/browser/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/commonjs/src/index.test.mjs b/packages/commonjs/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/commonjs/src/index.test.mjs +++ b/packages/commonjs/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/jest/src/index.test.mjs b/packages/jest/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/jest/src/index.test.mjs +++ b/packages/jest/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/mocha/src/index.test.mjs b/packages/mocha/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/mocha/src/index.test.mjs +++ b/packages/mocha/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/nodejs/src/index.test.mjs b/packages/nodejs/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/nodejs/src/index.test.mjs +++ b/packages/nodejs/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/typescript/src/index.test.mjs b/packages/typescript/src/index.test.mjs index df0bfdb0..41886b29 100644 --- a/packages/typescript/src/index.test.mjs +++ b/packages/typescript/src/index.test.mjs @@ -3,7 +3,7 @@ import globals from 'globals'; import { resolve } from 'path'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/vitest/src/index.test.mjs b/packages/vitest/src/index.test.mjs index 908ffbef..47f24a69 100644 --- a/packages/vitest/src/index.test.mjs +++ b/packages/vitest/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => {