From 0c558437b58dfd308a89789c7a00d17379d94fbb Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Sun, 28 Jul 2024 01:42:17 +0200 Subject: [PATCH] feat: target ESLint v9 --- .eslintignore | 9 ---- eslint.config.js | 20 +++++++ lib/linter/.eslintrc.js | 6 +-- lib/linter/index.js | 52 +++++++++++++------ lib/modules/coverage.js | 40 +++++++++++--- lib/modules/lint.js | 2 +- lib/modules/transform.js | 2 +- lib/modules/typescript.js | 2 +- lib/runner.js | 2 + package.json | 18 +++---- test/cli.js | 3 +- test/coverage.js | 6 +-- .../{.eslintrc.js => eslint.config.js} | 8 +-- test/coverage/test-folder/test-name.js | 4 +- test/lint/eslint/esm/.eslintrc.cjs | 13 ----- test/lint/eslint/esm/eslint.config.cjs | 22 ++++++++ test/lint/eslint/typescript/.eslintrc.cjs | 5 -- test/lint/eslint/typescript/eslint.config.cjs | 12 +++++ test/lint/eslint/with_config/.eslintignore | 1 - test/lint/eslint/with_config/.eslintrc.js | 9 ---- test/lint/eslint/with_config/eslint.config.js | 15 ++++++ test/linters.js | 7 ++- test/reporters.js | 1 + test/runner.js | 2 + 24 files changed, 173 insertions(+), 88 deletions(-) delete mode 100644 .eslintignore create mode 100644 eslint.config.js rename test/coverage/test-folder/{.eslintrc.js => eslint.config.js} (65%) delete mode 100644 test/lint/eslint/esm/.eslintrc.cjs create mode 100644 test/lint/eslint/esm/eslint.config.cjs delete mode 100644 test/lint/eslint/typescript/.eslintrc.cjs create mode 100644 test/lint/eslint/typescript/eslint.config.cjs delete mode 100644 test/lint/eslint/with_config/.eslintignore delete mode 100644 test/lint/eslint/with_config/.eslintrc.js create mode 100644 test/lint/eslint/with_config/eslint.config.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7a280b04..00000000 --- a/.eslintignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules/ -test_runner/ -test/coverage/ -test/cli/ -test/cli_*/ -test/lint/ -test/override/ -test/plan/ -test/transform/ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..efedefd6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,20 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [ + { + ignores: [ + 'node_modules/', + 'test_runner/', + 'test/coverage/', + 'test/cli/', + 'test/cli_*/', + 'test/lint/', + 'test/override/', + 'test/plan/', + 'test/transform/' + ] + }, + ...HapiPlugin.configs.module +]; diff --git a/lib/linter/.eslintrc.js b/lib/linter/.eslintrc.js index b2d74046..e7b26db8 100755 --- a/lib/linter/.eslintrc.js +++ b/lib/linter/.eslintrc.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = { - extends: 'plugin:@hapi/module' -}; +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [...HapiPlugin.configs.module]; diff --git a/lib/linter/index.js b/lib/linter/index.js index 9f25bbe2..1deadf10 100755 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -1,7 +1,6 @@ 'use strict'; const Fs = require('fs'); -const Path = require('path'); const Eslint = require('eslint'); const Hoek = require('@hapi/hoek'); @@ -18,31 +17,47 @@ exports.lint = async function () { const options = process.argv[2] ? JSON.parse(process.argv[2]) : undefined; - if (!Fs.existsSync('.eslintrc.js') && - !Fs.existsSync('.eslintrc.cjs') && // Needed for projects with "type": "module" - !Fs.existsSync('.eslintrc.yaml') && - !Fs.existsSync('.eslintrc.yml') && - !Fs.existsSync('.eslintrc.json') && - !Fs.existsSync('.eslintrc')) { - configuration.overrideConfigFile = Path.join(__dirname, '.eslintrc.js'); + let usingDefault = false; + + if (!Fs.existsSync('eslint.config.js') && + !Fs.existsSync('eslint.config.cjs') && + !Fs.existsSync('eslint.config.mjs')) { + // No configuration file found, using the default one + usingDefault = true; + configuration.baseConfig = require('./.eslintrc.js'); + configuration.overrideConfigFile = true; } if (options) { Hoek.merge(configuration, options, true, false); } - if (!configuration.extensions) { - configuration.extensions = ['.js', '.cjs', '.mjs']; + // Only the default configuration should be altered, otherwise the user's configuration should be used as is + if (usingDefault) { + if (!configuration.extensions) { + const extensions = ['js', 'cjs', 'mjs']; + + if (configuration.typescript) { + extensions.push('ts'); + } - if (configuration.typescript) { - configuration.extensions.push('.ts'); + configuration.baseConfig.unshift({ + files: extensions.map((ext) => `**/*.${ext}`) + }); } - } - if (configuration.typescript) { - delete configuration.typescript; + if (configuration.ignores) { + configuration.baseConfig.unshift({ + ignores: configuration.ignores + }); + } } + delete configuration.extensions; + delete configuration.typescript; + delete configuration.ignores; + + let results; try { const eslint = new Eslint.ESLint(configuration); @@ -66,6 +81,13 @@ exports.lint = async function () { transformed.errors = result.messages.map((err) => { + if (err.messageTemplate === 'all-matched-files-ignored') { + return { + severity: 'ERROR', + message: err.message + }; + } + return { line: err.line, severity: err.severity === 1 ? 'WARNING' : 'ERROR', diff --git a/lib/modules/coverage.js b/lib/modules/coverage.js index 60c323bb..3753897b 100755 --- a/lib/modules/coverage.js +++ b/lib/modules/coverage.js @@ -16,8 +16,7 @@ const SourceMap = require('../source-map'); const Transform = require('./transform'); const internals = { - _state: Symbol.for('@hapi/lab/coverage/_state'), - eslint: new ESLint.ESLint({ baseConfig: Eslintrc }) + _state: Symbol.for('@hapi/lab/coverage/_state') }; @@ -111,7 +110,7 @@ internals.prime = function (extension, ctx) { require.extensions[extension] = function (localModule, filename) { // We never want to instrument eslint configs in order to avoid infinite recursion - if (Path.basename(filename, extension) !== '.eslintrc') { + if (!['.eslintrc', 'eslint.config'].includes(Path.basename(filename, extension))) { for (let i = 0; i < internals.state.patterns.length; ++i) { if (internals.state.patterns[i].test(filename.replace(/\\/g, '/'))) { return localModule._compile(internals.instrument(filename, ctx), filename); @@ -761,11 +760,40 @@ internals.file = async function (filename, data, options) { internals.context = async (options) => { + const filePath = Path.join(options.coveragePath || '', 'x.js'); + let calculated; + // The parserOptions are shared by all files for coverage purposes, based on // the effective eslint config for a hypothetical file {coveragePath}/x.js - const { parserOptions } = await internals.eslint.calculateConfigForFile( - Path.join(options.coveragePath || '', 'x.js') - ); + try { + // Let's try first with eslint's native configuration detection + const eslint = new ESLint.ESLint({ + ignore: false + }); + + calculated = await eslint.calculateConfigForFile(filePath); + } + catch (err) { + /* $lab:coverage:off$ */ + if (err.messageTemplate !== 'config-file-missing') { + throw err; + } + + // If the eslint config file is missing, we'll use the one provided by lab + const eslint = new ESLint.ESLint({ + overrideConfig: Eslintrc, + overrideConfigFile: true, + ignore: false + }); + + calculated = await eslint.calculateConfigForFile(filePath); + /* $lab:coverage:on$ */ + } + + const parserOptions = { + ...calculated.languageOptions, + ...calculated.languageOptions?.parserOptions + }; return { parserOptions }; }; diff --git a/lib/modules/lint.js b/lib/modules/lint.js index 2a7ad74c..8413b189 100755 --- a/lib/modules/lint.js +++ b/lib/modules/lint.js @@ -20,7 +20,7 @@ exports.lint = function (settings) { try { linterOptions = JSON.parse(settings['lint-options'] || '{}'); } - catch (err) { + catch { return reject(new Error('lint-options could not be parsed')); } diff --git a/lib/modules/transform.js b/lib/modules/transform.js index d80b3a62..55587cf1 100755 --- a/lib/modules/transform.js +++ b/lib/modules/transform.js @@ -73,7 +73,7 @@ exports.retrieveFile = function (path) { try { contents = Fs.readFileSync(path, 'utf8'); } - catch (e) { + catch { contents = null; } diff --git a/lib/modules/typescript.js b/lib/modules/typescript.js index a004ae1a..9b4072f8 100755 --- a/lib/modules/typescript.js +++ b/lib/modules/typescript.js @@ -14,7 +14,7 @@ internals.transform = function (content, fileName) { try { var { config, error } = Typescript.readConfigFile(configFile, Typescript.sys.readFile); } - catch (err) { + catch { throw new Error(`Cannot find a tsconfig file for ${fileName}`); } diff --git a/lib/runner.js b/lib/runner.js index 6369f4e8..d1c5510d 100755 --- a/lib/runner.js +++ b/lib/runner.js @@ -13,10 +13,12 @@ const internals = {}; // Prevent libraries like Sinon from clobbering global time functions +/* eslint-disable no-redeclare */ const Date = global.Date; const setTimeout = global.setTimeout; const clearTimeout = global.clearTimeout; const setImmediate = global.setImmediate; +/* eslint-enable no-redeclare */ Error.stackTraceLimit = Infinity; // Set Error stack size diff --git a/package.json b/package.json index 64d9b376..969bc281 100755 --- a/package.json +++ b/package.json @@ -13,19 +13,14 @@ "bin/lab", "lib" ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, "dependencies": { "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.0", + "@babel/eslint-parser": "^7.25.1", "@hapi/bossy": "^6.0.0", - "@hapi/eslint-plugin": "^6.0.0", + "@hapi/eslint-plugin": "^7.0.0", "@hapi/hoek": "^11.0.2", "diff": "^5.0.0", - "eslint": "8.x.x", + "eslint": "9.x.x", "find-rc": "4.x.x", "globby": "^11.1.0", "handlebars": "4.x.x", @@ -37,7 +32,7 @@ "will-call": "1.x.x" }, "peerDependencies": { - "@hapi/eslint-plugin": "^6.0.0", + "@hapi/eslint-plugin": "^7.0.0", "typescript": ">=3.6.5" }, "peerDependenciesMeta": { @@ -48,13 +43,14 @@ "devDependencies": { "@hapi/code": "^9.0.0", "@hapi/somever": "^4.0.0", + "@types/eslint": "^9.6.0", "@types/node": "^18.11.17", - "@typescript-eslint/parser": "^5.62.0", "cpr": "3.x.x", "lab-event-reporter": "1.x.x", "semver": "7.x.x", "tsconfig-paths": "^4.0.0", - "typescript": "^4.5.4" + "typescript": "^4.5.4", + "typescript-eslint": "^8.1.0" }, "bin": { "lab": "./bin/lab" diff --git a/test/cli.js b/test/cli.js index 8ee50564..9a94f691 100755 --- a/test/cli.js +++ b/test/cli.js @@ -3,6 +3,7 @@ // Load modules const ChildProcess = require('child_process'); +// eslint-disable-next-line no-redeclare const Crypto = require('crypto'); const Fs = require('fs'); const Http = require('http'); @@ -702,7 +703,7 @@ describe('CLI', () => { try { await unlink(outputPath); } - catch (err) { + catch { // Error is ok here } diff --git a/test/coverage.js b/test/coverage.js index 2cf34aab..f55b825b 100755 --- a/test/coverage.js +++ b/test/coverage.js @@ -566,19 +566,19 @@ describe('Coverage', () => { it('sorts file paths in report', async () => { const files = global.__$$labCov.files; - const paths = ['/a/b', '/a/b/c', '/a/c/b', '/a/c', '/a/b/c', '/a/b/a']; + const paths = ['./a/b', './a/b/c', './a/c/b', './a/c', './a/b/c', './a/b/a']; paths.forEach((path) => { files[path] = { source: [] }; }); - const cov = await Lab.coverage.analyze({ coveragePath: '/a' }); + const cov = await Lab.coverage.analyze({ coveragePath: './a' }); const sorted = cov.files.map((file) => { return file.filename; }); - expect(sorted).to.equal(['/a/b', '/a/c', '/a/b/a', '/a/b/c', '/a/c/b']); + expect(sorted).to.equal(['./a/b', './a/c', './a/b/a', './a/b/c', './a/c/b']); }); }); diff --git a/test/coverage/test-folder/.eslintrc.js b/test/coverage/test-folder/eslint.config.js similarity index 65% rename from test/coverage/test-folder/.eslintrc.js rename to test/coverage/test-folder/eslint.config.js index f20a20eb..7fa11d92 100644 --- a/test/coverage/test-folder/.eslintrc.js +++ b/test/coverage/test-folder/eslint.config.js @@ -1,9 +1,11 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + // this is a deliberately unused function that will reduce coverage percentage // if it ends up getting instrumented, giving us something to assert against const unusedMethod = () => { console.log('hello world') } -module.exports = { - extends: 'plugin:@hapi/module' -} +module.exports = [...HapiPlugin.configs.module] diff --git a/test/coverage/test-folder/test-name.js b/test/coverage/test-folder/test-name.js index 84e87723..d8e4c397 100644 --- a/test/coverage/test-folder/test-name.js +++ b/test/coverage/test-folder/test-name.js @@ -1,7 +1,7 @@ 'use strict'; // Load modules - +const EslintConfig = require('./eslint.config'); // Declare internals @@ -10,5 +10,5 @@ const internals = {}; exports.method = function () { - return; + return EslintConfig; }; diff --git a/test/lint/eslint/esm/.eslintrc.cjs b/test/lint/eslint/esm/.eslintrc.cjs deleted file mode 100644 index 0bda9be6..00000000 --- a/test/lint/eslint/esm/.eslintrc.cjs +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = { - parserOptions: { - sourceType: 'module' - }, - overrides: [ - { - files: ['*.cjs'], - parserOptions: { sourceType: 'script' } - } - ] -}; diff --git a/test/lint/eslint/esm/eslint.config.cjs b/test/lint/eslint/esm/eslint.config.cjs new file mode 100644 index 00000000..3c14a73f --- /dev/null +++ b/test/lint/eslint/esm/eslint.config.cjs @@ -0,0 +1,22 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); + +module.exports = [ + ...HapiPlugin.configs.module, + { + languageOptions: { + parserOptions: { + sourceType: 'module' + } + } + }, + { + files: ['*.cjs'], + languageOptions: { + parserOptions: { + sourceType: 'script' + } + } + } +]; diff --git a/test/lint/eslint/typescript/.eslintrc.cjs b/test/lint/eslint/typescript/.eslintrc.cjs deleted file mode 100644 index 968d9a06..00000000 --- a/test/lint/eslint/typescript/.eslintrc.cjs +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - parser: '@typescript-eslint/parser' -}; diff --git a/test/lint/eslint/typescript/eslint.config.cjs b/test/lint/eslint/typescript/eslint.config.cjs new file mode 100644 index 00000000..c97aa2f3 --- /dev/null +++ b/test/lint/eslint/typescript/eslint.config.cjs @@ -0,0 +1,12 @@ +'use strict'; + +const HapiPlugin = require('@hapi/eslint-plugin'); +const TsESLint = require('typescript-eslint'); + +module.exports = TsESLint.config( + { + files: ['**/*.ts'] + }, + ...HapiPlugin.configs.module, + TsESLint.configs.base +); diff --git a/test/lint/eslint/with_config/.eslintignore b/test/lint/eslint/with_config/.eslintignore deleted file mode 100644 index ec79d651..00000000 --- a/test/lint/eslint/with_config/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -*.ignore.* \ No newline at end of file diff --git a/test/lint/eslint/with_config/.eslintrc.js b/test/lint/eslint/with_config/.eslintrc.js deleted file mode 100644 index ca8a9023..00000000 --- a/test/lint/eslint/with_config/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = { - 'rules': { - 'eol-last': 2, - 'no-unused-vars': 0, - 'no-undef': 0 - } -}; diff --git a/test/lint/eslint/with_config/eslint.config.js b/test/lint/eslint/with_config/eslint.config.js new file mode 100644 index 00000000..99b3313f --- /dev/null +++ b/test/lint/eslint/with_config/eslint.config.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = [ + { + ignores: ['*.ignore.*'] + }, + { + 'rules': { + 'eol-last': 2, + 'no-unused-vars': 0, + 'no-undef': 0 + } + } +]; + diff --git a/test/linters.js b/test/linters.js index 5888ddcb..0dc67706 100755 --- a/test/linters.js +++ b/test/linters.js @@ -88,7 +88,7 @@ describe('Linters - eslint', () => { { line: 12, severity: 'WARNING', message: 'eol-last - Newline required at end of file but not found.' } ]); - const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, '.eslintrc.cjs')); + const checkedCjsFile = eslintResults.find(({ filename }) => filename === Path.join(path, 'eslint.config.cjs')); expect(checkedCjsFile.errors).to.be.empty(); }); @@ -131,7 +131,6 @@ describe('Linters - eslint', () => { const checkedFile = eslintResults.find(({ filename }) => filename.endsWith('.ts')); expect(checkedFile).to.include({ filename: Path.join(path, 'fail.ts') }); expect(checkedFile.errors).to.include([ - { line: 1, severity: 'ERROR', message: `strict - Use the global form of 'use strict'.` }, { line: 6, severity: 'ERROR', message: 'indent - Expected indentation of 4 spaces but found 1 tab.' }, { line: 6, severity: 'ERROR', message: 'semi - Missing semicolon.' } ]); @@ -195,7 +194,7 @@ describe('Linters - eslint', () => { it('should pass options and not find any files', async () => { - const lintOptions = JSON.stringify({ extensions: ['.jsx'] }); + const lintOptions = JSON.stringify({ extensions: ['.jsx'], ignores: ['**/*.js'] }); const path = Path.join(__dirname, 'lint', 'eslint', 'basic'); const result = await Linters.lint({ lintingPath: path, linter: 'eslint', 'lint-options': lintOptions }); @@ -203,7 +202,7 @@ describe('Linters - eslint', () => { const eslintResults = result.lint; expect(eslintResults).to.have.length(1); - expect(eslintResults[0].errors[0].message).to.contain('No files'); + expect(eslintResults[0].errors[0].message).to.contain('All files matched by \'.\' are ignored.'); }); it('should fix lint rules when --lint-fix used', async (flags) => { diff --git a/test/reporters.js b/test/reporters.js index 123491e2..9115a127 100755 --- a/test/reporters.js +++ b/test/reporters.js @@ -1,5 +1,6 @@ 'use strict'; +// eslint-disable-next-line no-redeclare const Crypto = require('crypto'); const Fs = require('fs/promises'); const Os = require('os'); diff --git a/test/runner.js b/test/runner.js index 218dbef9..83a16d49 100755 --- a/test/runner.js +++ b/test/runner.js @@ -25,9 +25,11 @@ const expect = Code.expect; // save references to timer globals +/* eslint-disable no-redeclare */ const setTimeout = global.setTimeout; const clearTimeout = global.clearTimeout; const setImmediate = global.setImmediate; +/* eslint-enable no-redeclare */ describe('Runner', () => {