From 61215b60dfd4a2caa86715b3e135dcd337564ce3 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Sat, 23 Dec 2023 15:38:33 +0100 Subject: [PATCH 01/14] Make report an allow list --- lib/modules/types.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/modules/types.js b/lib/modules/types.js index a427e635..64418616 100755 --- a/lib/modules/types.js +++ b/lib/modules/types.js @@ -30,18 +30,7 @@ const internals = { ], report: [ - 2304, // Cannot find name - 2345, // Argument type is not assignable to parameter type - 2339, // Property does not exist on type - 2540, // Cannot assign to readonly property - 2322, // Type is not assignable to other type - 2314, // Generic type requires type arguments - 2554, // Expected arguments but got other - 2559, // Type T has no properties in common with type U - 2769, // No overload matches this call - 2673, // Constructor of class is private - 2674, // Constructor of class is protected - 2820 // Type T is not assignable to type U. Did you mean V? + 1005 // Syntax error ] }; @@ -162,7 +151,7 @@ internals.ignore = function (diagnostic, expectedErrors) { return true; } - if (!internals.report.includes(diagnostic.code)) { + if (internals.report.includes(diagnostic.code)) { return false; } From c81638a8d2885423a18279c86f5eed9649fad2ea Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Mon, 15 Jan 2024 14:08:45 +0100 Subject: [PATCH 02/14] 25.1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e92a43f8..8cd381fc 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/lab", "description": "Test utility", - "version": "25.1.3", + "version": "25.1.4", "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", From 8ed0636f88c95ad7a5a2e3fc0bcdf68685c8fea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominykas=20Bly=C5=BE=C4=97?= Date: Tue, 14 Nov 2023 10:17:08 +0200 Subject: [PATCH 03/14] feat: always lint .ts Closes https://github.com/hapijs/lab/issues/1065 I took the simpler approach here - an alternative would be to only add `.ts` when `--typescript` is set, but I figure there's probably no harm in always doing this? --- lib/linter/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/linter/index.js b/lib/linter/index.js index 4d72e328..7ea69f2c 100755 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -33,6 +33,10 @@ exports.lint = async function () { if (!configuration.extensions) { configuration.extensions = ['.js', '.cjs', '.mjs']; + + if (configuration.typescript) { + configuration.extensions.push('.ts'); + } } let results; From aaa16767a97799a59450117364509c948ada54b5 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Mon, 15 Jan 2024 14:12:15 +0100 Subject: [PATCH 04/14] 25.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cd381fc..f29fb3f5 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/lab", "description": "Test utility", - "version": "25.1.4", + "version": "25.2.0", "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", From 78ec46cefe355589048829de7896a2d3b7977251 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Fri, 26 Jul 2024 00:35:44 +0200 Subject: [PATCH 05/14] feat: add explicit extension support when using Typescript and ESM --- lib/cli.js | 26 ++++++++++++++++++++++++++ test/cli_typescript_esm/api.ts | 8 ++++++++ test/cli_typescript_esm/error.ts | 12 ++++++++++++ test/cli_typescript_esm/package.json | 1 + test/cli_typescript_esm/simple.ts | 20 ++++++++++++++++++++ test/cli_typescript_esm/tsconfig.json | 8 ++++++++ test/cli_typescript_esm/types.ts | 1 + test/reporters.js | 2 +- test/typescript.js | 12 ++++++++++++ 9 files changed, 89 insertions(+), 1 deletion(-) create mode 100755 test/cli_typescript_esm/api.ts create mode 100755 test/cli_typescript_esm/error.ts create mode 100644 test/cli_typescript_esm/package.json create mode 100755 test/cli_typescript_esm/simple.ts create mode 100755 test/cli_typescript_esm/tsconfig.json create mode 100755 test/cli_typescript_esm/types.ts diff --git a/lib/cli.js b/lib/cli.js index bedda40e..06638af9 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -81,6 +81,28 @@ exports.run = async function () { }; +internals.addTsEsmHook = function () { + + const Module = require('module'); + + const originalResolveFilename = Module._resolveFilename; + + Module._resolveFilename = function (request, parent, ...rest) { + + if (request.endsWith('.js') && (parent.filename.endsWith('.ts') || parent.filename.endsWith('.tsx'))) { + const target = Path.join(parent.path, request); + const tsEquivalent = `${target.slice(0, -3)}.ts`; + + if (!Fs.existsSync(target) && Fs.existsSync(tsEquivalent)) { + return tsEquivalent; + } + } + + return originalResolveFilename.call(this, request, parent, ...rest); + }; +}; + + internals.traverse = async function (paths, options) { let nextPath = null; @@ -141,6 +163,10 @@ internals.traverse = async function (paths, options) { const scripts = []; + if (options.typescript && testFiles.some(([, defaultToESM]) => defaultToESM)) { + internals.addTsEsmHook(); + } + for (const [unresolvedFile, defaultToESM] of testFiles) { global._labScriptRun = false; diff --git a/test/cli_typescript_esm/api.ts b/test/cli_typescript_esm/api.ts new file mode 100755 index 00000000..a86fad9a --- /dev/null +++ b/test/cli_typescript_esm/api.ts @@ -0,0 +1,8 @@ +export * from './types.js'; + +export function add(a: number, b: number) { + + return a + b; +} + +export const X = new (Date || Date)(); // $lab:coverage:ignore$ diff --git a/test/cli_typescript_esm/error.ts b/test/cli_typescript_esm/error.ts new file mode 100755 index 00000000..82f2f6a2 --- /dev/null +++ b/test/cli_typescript_esm/error.ts @@ -0,0 +1,12 @@ +import { expect } from '@hapi/code'; +import * as _Lab from '../../test_runner/index.js'; + +const { describe, it } = exports.lab = _Lab.script(); + +describe('Test CLI', () => { + + it('adds two numbers together', () => { + + expect(1 + 1).to.equal(4); + }); +}); diff --git a/test/cli_typescript_esm/package.json b/test/cli_typescript_esm/package.json new file mode 100644 index 00000000..5ffd9800 --- /dev/null +++ b/test/cli_typescript_esm/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/test/cli_typescript_esm/simple.ts b/test/cli_typescript_esm/simple.ts new file mode 100755 index 00000000..39fe9799 --- /dev/null +++ b/test/cli_typescript_esm/simple.ts @@ -0,0 +1,20 @@ +import { expect } from '@hapi/code'; +import * as _Lab from '../../test_runner/index.js'; + +import { add } from './api.js'; + + +const { describe, it } = exports.lab = _Lab.script(); + +describe('Test CLI', () => { + + it('adds two numbers together', () => { + + expect(add(1, 1)).to.equal(2); + }); + + it('subtracts two numbers', () => { + + expect(add(2, - 2)).to.equal(0); + }); +}); diff --git a/test/cli_typescript_esm/tsconfig.json b/test/cli_typescript_esm/tsconfig.json new file mode 100755 index 00000000..1dbf06b4 --- /dev/null +++ b/test/cli_typescript_esm/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "moduleResolution": "node", + "removeComments": true + } +} \ No newline at end of file diff --git a/test/cli_typescript_esm/types.ts b/test/cli_typescript_esm/types.ts new file mode 100755 index 00000000..67c8f1ab --- /dev/null +++ b/test/cli_typescript_esm/types.ts @@ -0,0 +1 @@ +export type Test = string; diff --git a/test/reporters.js b/test/reporters.js index 07ff107a..123491e2 100755 --- a/test/reporters.js +++ b/test/reporters.js @@ -123,7 +123,7 @@ describe('Reporter', () => { expect(code).to.equal(0); expect(output).to.equal(await Fs.readFile(filename, 'utf8')); - await Fs.rmdir(folder, { recursive: true }); + await Fs.rm(folder, { recursive: true }); }); it('outputs to a file with output is passed as an array and reporter is an array', async () => { diff --git a/test/typescript.js b/test/typescript.js index bccfe736..ce1610c8 100755 --- a/test/typescript.js +++ b/test/typescript.js @@ -32,6 +32,18 @@ describe('TypeScript', () => { expect(result.output).to.contain('2 tests complete'); }); + it('supports TypeScript with ESM', async () => { + + process.chdir(Path.join(__dirname, 'cli_typescript_esm')); + const result = await RunCli(['simple.ts', '-m', '2000', '--typescript']); + expect(result.errorOutput).to.equal(''); + expect(result.code).to.equal(0); + expect(result.output).to.contain('2 tests complete'); + + // Ensure scripts are run together, not independently + expect(result.output.split('Test duration').length - 1).to.equal(1); + }); + it('handles errors', async () => { process.chdir(Path.join(__dirname, 'cli_typescript')); From 7ea1b19832dbd3c50acba373a1104ba2406d94fe Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Fri, 26 Jul 2024 08:32:40 +0200 Subject: [PATCH 06/14] 25.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f29fb3f5..0baafb49 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/lab", "description": "Test utility", - "version": "25.2.0", + "version": "25.3.0", "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", From c306580075c28730c31727b895c7d6bc19dc3c8e Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Sat, 27 Jul 2024 11:10:56 +0200 Subject: [PATCH 07/14] fix: lint typescript files --- lib/linter/index.js | 4 ++++ lib/modules/lint.js | 4 ++++ package.json | 1 + test/lint/eslint/typescript/.eslintrc.cjs | 5 +++++ test/lint/eslint/typescript/fail.ts | 7 +++++++ test/linters.js | 19 +++++++++++++++++++ 6 files changed, 40 insertions(+) create mode 100644 test/lint/eslint/typescript/.eslintrc.cjs create mode 100755 test/lint/eslint/typescript/fail.ts diff --git a/lib/linter/index.js b/lib/linter/index.js index 7ea69f2c..9f25bbe2 100755 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -39,6 +39,10 @@ exports.lint = async function () { } } + if (configuration.typescript) { + delete configuration.typescript; + } + let results; try { const eslint = new Eslint.ESLint(configuration); diff --git a/lib/modules/lint.js b/lib/modules/lint.js index 99df20c4..2a7ad74c 100755 --- a/lib/modules/lint.js +++ b/lib/modules/lint.js @@ -26,6 +26,10 @@ exports.lint = function (settings) { linterOptions.fix = settings['lint-fix']; + if (settings.typescript) { + linterOptions.typescript = true; + } + const child = ChildProcess.fork(linterPath, [JSON.stringify(linterOptions)], { cwd: settings.lintingPath }); child.once('message', (message) => { diff --git a/package.json b/package.json index 0baafb49..ef179069 100755 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@hapi/code": "^9.0.0", "@hapi/somever": "^4.0.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", diff --git a/test/lint/eslint/typescript/.eslintrc.cjs b/test/lint/eslint/typescript/.eslintrc.cjs new file mode 100644 index 00000000..968d9a06 --- /dev/null +++ b/test/lint/eslint/typescript/.eslintrc.cjs @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + parser: '@typescript-eslint/parser' +}; diff --git a/test/lint/eslint/typescript/fail.ts b/test/lint/eslint/typescript/fail.ts new file mode 100755 index 00000000..96bd7d24 --- /dev/null +++ b/test/lint/eslint/typescript/fail.ts @@ -0,0 +1,7 @@ +const internals = {}; + + +export const method = function (value) { + + return value +}; diff --git a/test/linters.js b/test/linters.js index a642ae90..5888ddcb 100755 --- a/test/linters.js +++ b/test/linters.js @@ -118,6 +118,25 @@ describe('Linters - eslint', () => { ]); }); + it('should lint typescript files in a folder', async () => { + + const path = Path.join(__dirname, 'lint', 'eslint', 'typescript'); + const result = await Linters.lint({ lintingPath: path, linter: 'eslint', typescript: true }); + + expect(result).to.include('lint'); + + const eslintResults = result.lint; + expect(eslintResults).to.have.length(2); + + 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.' } + ]); + }); + it('displays success message if no issues found', async () => { const path = Path.join(__dirname, 'lint', 'eslint', 'clean'); From 801f15c2e8f7a93e469ff0df6b5b08bf74476630 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Sat, 27 Jul 2024 12:07:20 +0200 Subject: [PATCH 08/14] fix: coverage for arrow functions returning literals --- lib/modules/coverage.js | 2 +- test/coverage.js | 5 +++-- test/coverage/single-line-functions.js | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/modules/coverage.js b/lib/modules/coverage.js index 6fe2e1ef..60c323bb 100755 --- a/lib/modules/coverage.js +++ b/lib/modules/coverage.js @@ -307,7 +307,7 @@ internals.instrument = function (filename, ctx) { node.set(`(global.__$$labCov._statement(\'${filename}\',${left},${line},${node.left.source()})${node.operator}global.__$$labCov._statement(\'${filename}\',${right},${line},${node.right.source()}))`); } else if (node.parent?.type === 'ArrowFunctionExpression' && - node.type.includes('Expression')) { + (node.type.includes('Expression') || node.type === 'Literal')) { const id = addStatement(line, node, false); diff --git a/test/coverage.js b/test/coverage.js index f2635ff5..2cf34aab 100755 --- a/test/coverage.js +++ b/test/coverage.js @@ -484,12 +484,13 @@ describe('Coverage', () => { results.push(Test.method11(5, 10)); results.push(Test.method11(0, 10)); results.push(Test.method11Partial(5, 10)); + results.push(Test.method12()); const cov = await Lab.coverage.analyze({ coveragePath: Path.join(__dirname, 'coverage/single-line-functions') }); const source = cov.files[0].source; const missedLines = Object.keys(source).filter((lineNumber) => source[lineNumber].miss); - expect(results).to.equal([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 5, 10, 5]); - expect(missedLines).to.equal(['12', '15', '21', '27', '30', '33', '39', '46', '50', '53', '56']); + expect(results).to.equal([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 5, 10, 5, 42]); + expect(missedLines).to.equal(['12', '15', '21', '27', '30', '33', '39', '46', '50', '53', '56', '59']); }); it('should measure missing coverage on trailing function declarations correctly', async () => { diff --git a/test/coverage/single-line-functions.js b/test/coverage/single-line-functions.js index 103cd905..f00c1683 100644 --- a/test/coverage/single-line-functions.js +++ b/test/coverage/single-line-functions.js @@ -54,3 +54,6 @@ exports.method10NotCalled = (a, b) => exports.method9NotCalled(a, b); exports.method11 = (a, b) => a || b; exports.method11Partial = (a, b) => a || b; + +exports.method12 = () => 42; +exports.method12NotCalled = () => 42; From 945503a6ad796b73faca9f2bdf6b3209a7800392 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Sun, 28 Jul 2024 00:57:17 +0200 Subject: [PATCH 09/14] 25.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef179069..6ae808b5 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/lab", "description": "Test utility", - "version": "25.3.0", + "version": "25.3.1", "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", From dc502d46fc08e5b7d8d2428496262082aac3e08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominykas=20Bly=C5=BE=C4=97?= Date: Wed, 16 Oct 2024 09:11:51 +0100 Subject: [PATCH 10/14] feat: ignore new TypeScript global leak Ref: https://github.com/search?q=repo%3Amicrosoft%2Ftslib%20__rewriteRelativeImportExtension&type=code --- lib/modules/leaks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/modules/leaks.js b/lib/modules/leaks.js index 51e2cc06..a8116a8f 100755 --- a/lib/modules/leaks.js +++ b/lib/modules/leaks.js @@ -71,7 +71,8 @@ const internals = { '__propKey', '__setFunctionName', '__addDisposableResource', - '__disposeResources' + '__disposeResources', + '__rewriteRelativeImportExtension' ] }; From 4a2b35738af501b0c26d5fe2e404b8417e8aab56 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Thu, 17 Oct 2024 15:16:41 +0200 Subject: [PATCH 11/14] 25.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ae808b5..64d9b376 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hapi/lab", "description": "Test utility", - "version": "25.3.1", + "version": "25.3.2", "repository": "git://github.com/hapijs/lab", "main": "lib/index.js", "types": "lib/index.d.ts", From 17420d81490161873392a105d3ac2d194e3569c5 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 8 Nov 2023 12:38:16 +0100 Subject: [PATCH 12/14] chore: change CI target for next --- .github/workflows/ci-module.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 54426ca2..23bc524e 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -9,6 +9,4 @@ on: jobs: test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master - with: - min-node-version: 14 + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21 From c10b2ae3758d3d717899c8e78ecaec727bbc90af Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 8 Nov 2023 16:27:26 +0100 Subject: [PATCH 13/14] chore: add next branch to CI targets --- .github/workflows/ci-module.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 23bc524e..44369c8b 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - next pull_request: workflow_dispatch: From 0c558437b58dfd308a89789c7a00d17379d94fbb Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Sun, 28 Jul 2024 01:42:17 +0200 Subject: [PATCH 14/14] 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', () => {