From 0807ce6523bb6fcf597ebff586589f83ce293b39 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Sep 2024 13:52:22 +0200 Subject: [PATCH 1/5] Add util for creating extendable ESLint configurations --- eslint.config.mjs | 7 + packages/base/package.json | 6 +- packages/base/src/index.js | 425 ++++++++++++++++++ packages/base/src/index.mjs | 421 ----------------- .../src/{index.test.mjs => index.test.js} | 2 +- packages/base/src/utils.js | 105 +++++ packages/base/src/utils.test.js | 68 +++ packages/browser/src/index.test.mjs | 2 +- packages/commonjs/src/index.test.mjs | 2 +- packages/jest/src/index.test.mjs | 2 +- packages/mocha/src/index.test.mjs | 2 +- packages/nodejs/src/index.test.mjs | 2 +- packages/typescript/src/index.test.mjs | 2 +- 13 files changed, 615 insertions(+), 431 deletions(-) create mode 100644 packages/base/src/index.js delete mode 100644 packages/base/src/index.mjs rename packages/base/src/{index.test.mjs => index.test.js} (92%) create mode 100644 packages/base/src/utils.js create mode 100644 packages/base/src/utils.test.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 8dae7749..63bbbe34 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -49,8 +49,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/package.json b/packages/base/package.json index ce98e1b6..5afa6399 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -16,15 +16,15 @@ ".": { "import": { "types": "./src/index.d.mts", - "default": "./src/index.mjs" + "default": "./src/index.js" } } }, - "main": "./src/index.mjs", + "main": "src/index.js", "types": "./src/index.d.mts", "files": [ "src/", - "!src/**/*.test.mjs" + "!src/**/*.test.js" ], "scripts": { "lint:changelog": "auto-changelog validate", diff --git a/packages/base/src/index.js b/packages/base/src/index.js new file mode 100644 index 00000000..be5e994d --- /dev/null +++ b/packages/base/src/index.js @@ -0,0 +1,425 @@ +// @ts-check + +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-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.js'; + +// TODO: Use import attributes when ESLint supports them. +const customRequire = createRequire(import.meta.url); +const environmentRules = customRequire('./environment.json'); + +/** + * @type {import('eslint').Linter.Config[]} + */ +const rules = createConfig({ + name: '@metamask/eslint-config', + + extends: [ + // Recommended ESLint configuration. + js.configs.recommended, + + // 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. + ecmaVersion: 2022, + parserOptions: { + ecmaVersion: 2022, + }, + + // We want to default to 'script' and only use 'module' explicitly. + sourceType: 'script', + + globals: { + ...globals.es2022, + ...globals['shared-node-browser'], + }, + }, + + 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, + }, + ], + + 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, + }, + ], + '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', + + 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'], + ], + + // Alphabetically sort the imports within each group. + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + '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', + + // 'promise/no-multiple-resolved': 'error', + }, +}); + +export { createConfig }; +export default rules; diff --git a/packages/base/src/index.mjs b/packages/base/src/index.mjs deleted file mode 100644 index 98480cf9..00000000 --- a/packages/base/src/index.mjs +++ /dev/null @@ -1,421 +0,0 @@ -// @ts-check - -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. -import promise from 'eslint-plugin-promise'; -import globals from 'globals'; -import { createRequire } from 'module'; - -// TODO: Use import attributes when ESLint supports them. -const customRequire = createRequire(import.meta.url); -const environmentRules = customRequire('./environment.json'); - -/** - * @type {import('eslint').Linter.Config[]} - */ -const rules = [ - // Recommended ESLint configuration. - js.configs.recommended, - - importX.flatConfigs.recommended, - jsdoc.configs['flat/recommended-error'], - prettier, - promise.configs['flat/recommended'], - - { - name: '@metamask/eslint-config', - - 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, - }, - - // We want to default to 'script' and only use 'module' explicitly. - sourceType: 'script', - - globals: { - ...globals.es2022, - ...globals['shared-node-browser'], - }, - }, - - 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, - }, - ], - - 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, - }, - ], - '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', - - 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'], - ], - - // Alphabetically sort the imports within each group. - alphabetize: { - order: 'asc', - caseInsensitive: true, - }, - }, - ], - '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', - - // 'promise/no-multiple-resolved': 'error', - }, - }, -]; - -export default rules; diff --git a/packages/base/src/index.test.mjs b/packages/base/src/index.test.js similarity index 92% rename from packages/base/src/index.test.mjs rename to packages/base/src/index.test.js index 908ffbef..f30dee08 100644 --- a/packages/base/src/index.test.mjs +++ b/packages/base/src/index.test.js @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from '.'; +import config from './index.js'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/base/src/utils.js b/packages/base/src/utils.js new file mode 100644 index 00000000..d0632646 --- /dev/null +++ b/packages/base/src/utils.js @@ -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.js b/packages/base/src/utils.test.js new file mode 100644 index 00000000..d177d957 --- /dev/null +++ b/packages/base/src/utils.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest'; + +import { createConfig } from './utils.js'; + +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 () => { From e8fcf72917538f0234adab4110d01e69fdb0da51 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Sep 2024 13:57:35 +0200 Subject: [PATCH 2/5] Revert changes to file extensions --- packages/base/package.json | 6 +++--- packages/base/src/{index.js => index.mjs} | 2 +- packages/base/src/{index.test.js => index.test.mjs} | 2 +- packages/base/src/{utils.js => utils.mjs} | 0 packages/base/src/{utils.test.js => utils.test.mjs} | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename packages/base/src/{index.js => index.mjs} (99%) rename packages/base/src/{index.test.js => index.test.mjs} (92%) rename packages/base/src/{utils.js => utils.mjs} (100%) rename packages/base/src/{utils.test.js => utils.test.mjs} (97%) diff --git a/packages/base/package.json b/packages/base/package.json index 5afa6399..ce98e1b6 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -16,15 +16,15 @@ ".": { "import": { "types": "./src/index.d.mts", - "default": "./src/index.js" + "default": "./src/index.mjs" } } }, - "main": "src/index.js", + "main": "./src/index.mjs", "types": "./src/index.d.mts", "files": [ "src/", - "!src/**/*.test.js" + "!src/**/*.test.mjs" ], "scripts": { "lint:changelog": "auto-changelog validate", diff --git a/packages/base/src/index.js b/packages/base/src/index.mjs similarity index 99% rename from packages/base/src/index.js rename to packages/base/src/index.mjs index be5e994d..aaa00a74 100644 --- a/packages/base/src/index.js +++ b/packages/base/src/index.mjs @@ -9,7 +9,7 @@ import promise from 'eslint-plugin-promise'; import globals from 'globals'; import { createRequire } from 'module'; -import { createConfig } from './utils.js'; +import { createConfig } from './utils.mjs'; // TODO: Use import attributes when ESLint supports them. const customRequire = createRequire(import.meta.url); diff --git a/packages/base/src/index.test.js b/packages/base/src/index.test.mjs similarity index 92% rename from packages/base/src/index.test.js rename to packages/base/src/index.test.mjs index f30dee08..47f24a69 100644 --- a/packages/base/src/index.test.js +++ b/packages/base/src/index.test.mjs @@ -1,7 +1,7 @@ import { ESLint } from 'eslint'; import { describe, it, expect } from 'vitest'; -import config from './index.js'; +import config from './index.mjs'; describe('index', () => { it('is a valid ESLint config', async () => { diff --git a/packages/base/src/utils.js b/packages/base/src/utils.mjs similarity index 100% rename from packages/base/src/utils.js rename to packages/base/src/utils.mjs diff --git a/packages/base/src/utils.test.js b/packages/base/src/utils.test.mjs similarity index 97% rename from packages/base/src/utils.test.js rename to packages/base/src/utils.test.mjs index d177d957..fb7b17cc 100644 --- a/packages/base/src/utils.test.js +++ b/packages/base/src/utils.test.mjs @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { createConfig } from './utils.js'; +import { createConfig } from './utils.mjs'; describe('createConfig', () => { it('returns a config array for a single config', () => { From cb046f618052bc8dbb7b4509ca969010e0650267 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 25 Sep 2024 20:10:22 +0200 Subject: [PATCH 3/5] Add function to load config with child configs --- packages/base/src/file-system.mjs | 51 ++++++++++++ packages/base/src/file-system.test.mjs | 88 ++++++++++++++++++++ packages/base/src/utils.mjs | 111 +++++++++++++++++++++++++ packages/base/src/utils.test.mjs | 62 +++++++++++++- 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 packages/base/src/file-system.mjs create mode 100644 packages/base/src/file-system.test.mjs diff --git a/packages/base/src/file-system.mjs b/packages/base/src/file-system.mjs new file mode 100644 index 00000000..817ba5d5 --- /dev/null +++ b/packages/base/src/file-system.mjs @@ -0,0 +1,51 @@ +import { readdir } from 'fs/promises'; +import { join, sep } from 'path'; + +/** + * @typedef {import('eslint').Linter.Config} Config + * @typedef {{ path: string; config: Config }} ConfigFile + */ + +/** + * Config file names for ESLint, that are supported by the config loading + * functionality in this package. + * + * @type {string[]} + */ +const ESLINT_CONFIG_NAMES = [ + 'eslint.config.js', + 'eslint.config.cjs', + 'eslint.config.mjs', +]; + +/** + * Get all ESLint config files in the workspace. + * + * @param {string} workspaceRoot - The absolute path to the root directory of + * the workspace. + * @returns {Promise} A promise that resolves to an array of + * ESLint configs with their paths. + */ +export async function getConfigFiles(workspaceRoot) { + const files = await readdir(workspaceRoot, { + recursive: true, + withFileTypes: true, + }); + + return files.reduce(async (promise, file) => { + const accumulator = await promise; + if (!file.isFile() || !ESLINT_CONFIG_NAMES.includes(file.name)) { + return accumulator; + } + + const segments = file.parentPath.split(sep); + if (segments.includes('node_modules')) { + return accumulator; + } + + const path = join(file.parentPath, file.name); + const config = await import(path); + + return [...accumulator, { path, config: config.default }]; + }, Promise.resolve([])); +} diff --git a/packages/base/src/file-system.test.mjs b/packages/base/src/file-system.test.mjs new file mode 100644 index 00000000..834d609e --- /dev/null +++ b/packages/base/src/file-system.test.mjs @@ -0,0 +1,88 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { getConfigFiles } from './file-system.mjs'; + +vi.mock('fs/promises', (importOriginal) => ({ + ...importOriginal, + readdir: async () => [ + { isFile: () => true, parentPath: '/foo/bar', name: 'eslint.config.js' }, + { + isFile: () => true, + parentPath: '/foo/bar/baz', + name: 'eslint.config.cjs', + }, + { + isFile: () => true, + parentPath: '/foo/bar/baz/qux', + name: 'eslint.config.mjs', + }, + { + isFile: () => true, + parentPath: '/foo/bar/node_modules/package', + name: 'eslint.config.js', + }, + { + isFile: () => false, + parentPath: '/not/a/file', + name: 'eslint.config.js', + }, + ], +})); + +vi.mock('/foo/bar/eslint.config.js', () => ({ + default: { + rules: { + 'no-console': 'error', + }, + }, +})); + +vi.mock('/foo/bar/baz/eslint.config.cjs', () => ({ + default: { + rules: { + 'no-console': 'warn', + }, + }, +})); + +vi.mock('/foo/bar/baz/qux/eslint.config.mjs', () => ({ + default: { + rules: { + 'no-console': 'off', + }, + }, +})); + +describe('getConfigFiles', () => { + it('returns an array of ESLint config files with their paths', async () => { + const workspaceRoot = '/foo/bar'; + const configFiles = await getConfigFiles(workspaceRoot); + + expect(configFiles).toStrictEqual([ + { + path: '/foo/bar/eslint.config.js', + config: { + rules: { + 'no-console': 'error', + }, + }, + }, + { + path: '/foo/bar/baz/eslint.config.cjs', + config: { + rules: { + 'no-console': 'warn', + }, + }, + }, + { + path: '/foo/bar/baz/qux/eslint.config.mjs', + config: { + rules: { + 'no-console': 'off', + }, + }, + }, + ]); + }); +}); diff --git a/packages/base/src/utils.mjs b/packages/base/src/utils.mjs index d0632646..afe5a59e 100644 --- a/packages/base/src/utils.mjs +++ b/packages/base/src/utils.mjs @@ -1,6 +1,11 @@ +import { dirname, join, relative } from 'path'; + +import { getConfigFiles } from './file-system.mjs'; + /** * @typedef {import('eslint').Linter.Config} Config * @typedef {Config & { extends?: Config | Config[] | Config[][] }} ConfigWithExtends + * @typedef {{ path: string; config: Config }} ConfigFile */ /** @@ -103,3 +108,109 @@ export function createConfig(configs) { return [getExtendedConfig(extendsValue, extension), originalConfig]; }); } + +/** + * Get the files array, including the path for the workspace. + * + * @param {string[]} files - The files array. + * @param {string} workspacePath - The path to the workspace. + * @returns {string[]} The files array with the workspace path. + */ +function getWorkspaceFiles(files, workspacePath) { + return files.map((file) => join(workspacePath, file)); +} + +/** + * Get the config object for the child workspace with the correct paths. This + * updates the `files` and `ignores` properties with the correct paths relative + * to the workspace root. + * + * @param {ConfigFile} configFile - The config file object. + * @param {Config} configFile.config - The config object. + * @param {string} configFile.path - The path to the config file. + * @param {string} workspaceRoot - The absolute path to the root directory of + * the workspace. + * @returns {Config} The config object with the correct paths. + */ +function getWorkspaceConfig({ config, path }, workspaceRoot) { + const relativePath = relative(workspaceRoot, path); + const baseWorkspaceDirectory = dirname(relativePath); + + if (!config.files && !config.ignores) { + return { + ...config, + files: [join(baseWorkspaceDirectory, '**')], + }; + } + + const extension = { + ...(config.files && { + files: getWorkspaceFiles(config.files, baseWorkspaceDirectory), + }), + ...(config.ignores && { + ignores: getWorkspaceFiles(config.ignores, baseWorkspaceDirectory), + }), + }; + + return { + ...config, + ...extension, + }; +} + +/** + * Create a config object that is extendable through other config files on the + * file system inside the same workspace. + * + * This function is a wrapper around `createConfig`, but fetches the config + * objects from the file system, and merges them with the provided config + * objects. + * + * @param {ConfigWithExtends | ConfigWithExtends[]} configs - An array of config + * objects. + * @param {string} workspaceRoot - The absolute path to the root directory of + * the workspace, i.e., `import.meta.dirname`. + * @returns {Promise} A promise that resolves to 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'; + * + * // Loads all child workspace configs and merges them with the provided + * // config objects. + * const configs = createWorkspaceConfig([ + * { + * 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'; + * + * // Loads all child workspace configs and merges them with the provided + * // config objects. + * const configs = createConfig([ + * { + * files: ['**\/*.ts'], + * extends: [typescript, nodejs], + * }, + * ]); + * + * export default configs; + */ +export async function createWorkspaceConfig(configs, workspaceRoot) { + const baseConfig = createConfig(configs); + const workspaceConfigs = await getConfigFiles(workspaceRoot); + + return [ + ...baseConfig, + ...workspaceConfigs.map((config) => + getWorkspaceConfig(config, workspaceRoot), + ), + ]; +} diff --git a/packages/base/src/utils.test.mjs b/packages/base/src/utils.test.mjs index fb7b17cc..b612e188 100644 --- a/packages/base/src/utils.test.mjs +++ b/packages/base/src/utils.test.mjs @@ -1,6 +1,11 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; -import { createConfig } from './utils.mjs'; +import { getConfigFiles } from './file-system.mjs'; +import { createConfig, createWorkspaceConfig } from './utils.mjs'; + +vi.mock('./file-system.mjs', { + getConfigFiles: vi.fn(), +}); describe('createConfig', () => { it('returns a config array for a single config', () => { @@ -66,3 +71,56 @@ describe('createConfig', () => { ]); }); }); + +describe('createExtendableConfig', () => { + it('returns a config array', async () => { + vi.mocked(getConfigFiles).mockResolvedValue([]); + const configs = { files: ['**/*.js'] }; + + const result = await createWorkspaceConfig(configs); + expect(result).toStrictEqual([configs]); + }); + + it('returns a config array with extended configs', async () => { + vi.mocked(getConfigFiles).mockResolvedValue([ + { + path: '/workspace/child-a/config.js', + config: { + files: ['**/*.js'], + rules: { + 'some-rule': 'error', + }, + }, + }, + { + path: '/workspace/child-b/config.js', + config: { + files: ['**/*.ts', '**/*.tsx'], + rules: { + 'some-other-rule': 'error', + }, + }, + }, + ]); + + const configs = { extends: [{ files: ['**/*.js'] }], languageOptions: {} }; + + const result = await createWorkspaceConfig(configs, '/workspace'); + expect(result).toStrictEqual([ + { files: ['**/*.js'] }, + { languageOptions: {} }, + { + files: ['child-a/**/*.js'], + rules: { + 'some-rule': 'error', + }, + }, + { + files: ['child-b/**/*.ts', 'child-b/**/*.tsx'], + rules: { + 'some-other-rule': 'error', + }, + }, + ]); + }); +}); From 83114b85c82ebaa94df4f243ae52a8afbf227234 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Sep 2024 12:27:48 +0200 Subject: [PATCH 4/5] Revert "Add function to load config with child configs" This reverts commit cb046f618052bc8dbb7b4509ca969010e0650267. --- packages/base/src/file-system.mjs | 51 ------------ packages/base/src/file-system.test.mjs | 88 -------------------- packages/base/src/utils.mjs | 111 ------------------------- packages/base/src/utils.test.mjs | 62 +------------- 4 files changed, 2 insertions(+), 310 deletions(-) delete mode 100644 packages/base/src/file-system.mjs delete mode 100644 packages/base/src/file-system.test.mjs diff --git a/packages/base/src/file-system.mjs b/packages/base/src/file-system.mjs deleted file mode 100644 index 817ba5d5..00000000 --- a/packages/base/src/file-system.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import { readdir } from 'fs/promises'; -import { join, sep } from 'path'; - -/** - * @typedef {import('eslint').Linter.Config} Config - * @typedef {{ path: string; config: Config }} ConfigFile - */ - -/** - * Config file names for ESLint, that are supported by the config loading - * functionality in this package. - * - * @type {string[]} - */ -const ESLINT_CONFIG_NAMES = [ - 'eslint.config.js', - 'eslint.config.cjs', - 'eslint.config.mjs', -]; - -/** - * Get all ESLint config files in the workspace. - * - * @param {string} workspaceRoot - The absolute path to the root directory of - * the workspace. - * @returns {Promise} A promise that resolves to an array of - * ESLint configs with their paths. - */ -export async function getConfigFiles(workspaceRoot) { - const files = await readdir(workspaceRoot, { - recursive: true, - withFileTypes: true, - }); - - return files.reduce(async (promise, file) => { - const accumulator = await promise; - if (!file.isFile() || !ESLINT_CONFIG_NAMES.includes(file.name)) { - return accumulator; - } - - const segments = file.parentPath.split(sep); - if (segments.includes('node_modules')) { - return accumulator; - } - - const path = join(file.parentPath, file.name); - const config = await import(path); - - return [...accumulator, { path, config: config.default }]; - }, Promise.resolve([])); -} diff --git a/packages/base/src/file-system.test.mjs b/packages/base/src/file-system.test.mjs deleted file mode 100644 index 834d609e..00000000 --- a/packages/base/src/file-system.test.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { getConfigFiles } from './file-system.mjs'; - -vi.mock('fs/promises', (importOriginal) => ({ - ...importOriginal, - readdir: async () => [ - { isFile: () => true, parentPath: '/foo/bar', name: 'eslint.config.js' }, - { - isFile: () => true, - parentPath: '/foo/bar/baz', - name: 'eslint.config.cjs', - }, - { - isFile: () => true, - parentPath: '/foo/bar/baz/qux', - name: 'eslint.config.mjs', - }, - { - isFile: () => true, - parentPath: '/foo/bar/node_modules/package', - name: 'eslint.config.js', - }, - { - isFile: () => false, - parentPath: '/not/a/file', - name: 'eslint.config.js', - }, - ], -})); - -vi.mock('/foo/bar/eslint.config.js', () => ({ - default: { - rules: { - 'no-console': 'error', - }, - }, -})); - -vi.mock('/foo/bar/baz/eslint.config.cjs', () => ({ - default: { - rules: { - 'no-console': 'warn', - }, - }, -})); - -vi.mock('/foo/bar/baz/qux/eslint.config.mjs', () => ({ - default: { - rules: { - 'no-console': 'off', - }, - }, -})); - -describe('getConfigFiles', () => { - it('returns an array of ESLint config files with their paths', async () => { - const workspaceRoot = '/foo/bar'; - const configFiles = await getConfigFiles(workspaceRoot); - - expect(configFiles).toStrictEqual([ - { - path: '/foo/bar/eslint.config.js', - config: { - rules: { - 'no-console': 'error', - }, - }, - }, - { - path: '/foo/bar/baz/eslint.config.cjs', - config: { - rules: { - 'no-console': 'warn', - }, - }, - }, - { - path: '/foo/bar/baz/qux/eslint.config.mjs', - config: { - rules: { - 'no-console': 'off', - }, - }, - }, - ]); - }); -}); diff --git a/packages/base/src/utils.mjs b/packages/base/src/utils.mjs index afe5a59e..d0632646 100644 --- a/packages/base/src/utils.mjs +++ b/packages/base/src/utils.mjs @@ -1,11 +1,6 @@ -import { dirname, join, relative } from 'path'; - -import { getConfigFiles } from './file-system.mjs'; - /** * @typedef {import('eslint').Linter.Config} Config * @typedef {Config & { extends?: Config | Config[] | Config[][] }} ConfigWithExtends - * @typedef {{ path: string; config: Config }} ConfigFile */ /** @@ -108,109 +103,3 @@ export function createConfig(configs) { return [getExtendedConfig(extendsValue, extension), originalConfig]; }); } - -/** - * Get the files array, including the path for the workspace. - * - * @param {string[]} files - The files array. - * @param {string} workspacePath - The path to the workspace. - * @returns {string[]} The files array with the workspace path. - */ -function getWorkspaceFiles(files, workspacePath) { - return files.map((file) => join(workspacePath, file)); -} - -/** - * Get the config object for the child workspace with the correct paths. This - * updates the `files` and `ignores` properties with the correct paths relative - * to the workspace root. - * - * @param {ConfigFile} configFile - The config file object. - * @param {Config} configFile.config - The config object. - * @param {string} configFile.path - The path to the config file. - * @param {string} workspaceRoot - The absolute path to the root directory of - * the workspace. - * @returns {Config} The config object with the correct paths. - */ -function getWorkspaceConfig({ config, path }, workspaceRoot) { - const relativePath = relative(workspaceRoot, path); - const baseWorkspaceDirectory = dirname(relativePath); - - if (!config.files && !config.ignores) { - return { - ...config, - files: [join(baseWorkspaceDirectory, '**')], - }; - } - - const extension = { - ...(config.files && { - files: getWorkspaceFiles(config.files, baseWorkspaceDirectory), - }), - ...(config.ignores && { - ignores: getWorkspaceFiles(config.ignores, baseWorkspaceDirectory), - }), - }; - - return { - ...config, - ...extension, - }; -} - -/** - * Create a config object that is extendable through other config files on the - * file system inside the same workspace. - * - * This function is a wrapper around `createConfig`, but fetches the config - * objects from the file system, and merges them with the provided config - * objects. - * - * @param {ConfigWithExtends | ConfigWithExtends[]} configs - An array of config - * objects. - * @param {string} workspaceRoot - The absolute path to the root directory of - * the workspace, i.e., `import.meta.dirname`. - * @returns {Promise} A promise that resolves to 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'; - * - * // Loads all child workspace configs and merges them with the provided - * // config objects. - * const configs = createWorkspaceConfig([ - * { - * 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'; - * - * // Loads all child workspace configs and merges them with the provided - * // config objects. - * const configs = createConfig([ - * { - * files: ['**\/*.ts'], - * extends: [typescript, nodejs], - * }, - * ]); - * - * export default configs; - */ -export async function createWorkspaceConfig(configs, workspaceRoot) { - const baseConfig = createConfig(configs); - const workspaceConfigs = await getConfigFiles(workspaceRoot); - - return [ - ...baseConfig, - ...workspaceConfigs.map((config) => - getWorkspaceConfig(config, workspaceRoot), - ), - ]; -} diff --git a/packages/base/src/utils.test.mjs b/packages/base/src/utils.test.mjs index b612e188..fb7b17cc 100644 --- a/packages/base/src/utils.test.mjs +++ b/packages/base/src/utils.test.mjs @@ -1,11 +1,6 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; -import { getConfigFiles } from './file-system.mjs'; -import { createConfig, createWorkspaceConfig } from './utils.mjs'; - -vi.mock('./file-system.mjs', { - getConfigFiles: vi.fn(), -}); +import { createConfig } from './utils.mjs'; describe('createConfig', () => { it('returns a config array for a single config', () => { @@ -71,56 +66,3 @@ describe('createConfig', () => { ]); }); }); - -describe('createExtendableConfig', () => { - it('returns a config array', async () => { - vi.mocked(getConfigFiles).mockResolvedValue([]); - const configs = { files: ['**/*.js'] }; - - const result = await createWorkspaceConfig(configs); - expect(result).toStrictEqual([configs]); - }); - - it('returns a config array with extended configs', async () => { - vi.mocked(getConfigFiles).mockResolvedValue([ - { - path: '/workspace/child-a/config.js', - config: { - files: ['**/*.js'], - rules: { - 'some-rule': 'error', - }, - }, - }, - { - path: '/workspace/child-b/config.js', - config: { - files: ['**/*.ts', '**/*.tsx'], - rules: { - 'some-other-rule': 'error', - }, - }, - }, - ]); - - const configs = { extends: [{ files: ['**/*.js'] }], languageOptions: {} }; - - const result = await createWorkspaceConfig(configs, '/workspace'); - expect(result).toStrictEqual([ - { files: ['**/*.js'] }, - { languageOptions: {} }, - { - files: ['child-a/**/*.js'], - rules: { - 'some-rule': 'error', - }, - }, - { - files: ['child-b/**/*.ts', 'child-b/**/*.tsx'], - rules: { - 'some-other-rule': 'error', - }, - }, - ]); - }); -}); From 432a535629e495709aa8b4e825149f3ce8223831 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Thu, 26 Sep 2024 12:31:40 +0200 Subject: [PATCH 5/5] Fix lint issue --- packages/vitest/src/index.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 () => {